From a4aea230ed6087a5fccc415d6456c536645e0df1 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Tue, 2 Apr 2024 18:43:51 +0100 Subject: [PATCH 001/495] fix compile error for multiple async method arguments (#4035) --- newsfragments/4035.fixed.md | 1 + pyo3-macros-backend/src/method.rs | 25 ++++-------- tests/test_coroutine.rs | 66 +++++++++++++++---------------- 3 files changed, 41 insertions(+), 51 deletions(-) create mode 100644 newsfragments/4035.fixed.md diff --git a/newsfragments/4035.fixed.md b/newsfragments/4035.fixed.md new file mode 100644 index 00000000000..5425c5cbaf7 --- /dev/null +++ b/newsfragments/4035.fixed.md @@ -0,0 +1 @@ +Fix compile error for `async fn` in `#[pymethods]` with a `&self` receiver and more than one additional argument. diff --git a/pyo3-macros-backend/src/method.rs b/pyo3-macros-backend/src/method.rs index 155c554025d..6af0ec97d04 100644 --- a/pyo3-macros-backend/src/method.rs +++ b/pyo3-macros-backend/src/method.rs @@ -522,33 +522,22 @@ impl<'a> FnSpec<'a> { Some(cls) => quote!(Some(<#cls as #pyo3_path::PyTypeInfo>::NAME)), None => quote!(None), }; - let evaluate_args = || -> (Vec, TokenStream) { - let mut arg_names = Vec::with_capacity(args.len()); - let mut evaluate_arg = quote! {}; - for arg in &args { - let arg_name = format_ident!("arg_{}", arg_names.len()); - arg_names.push(arg_name.clone()); - evaluate_arg.extend(quote! { - let #arg_name = #arg - }); - } - (arg_names, evaluate_arg) - }; + let arg_names = (0..args.len()) + .map(|i| format_ident!("arg_{}", i)) + .collect::>(); let future = match self.tp { FnType::Fn(SelfType::Receiver { mutable: false, .. }) => { - let (arg_name, evaluate_arg) = evaluate_args(); quote! {{ - #evaluate_arg; + #(let #arg_names = #args;)* let __guard = #pyo3_path::impl_::coroutine::RefGuard::<#cls>::new(&#pyo3_path::impl_::pymethods::BoundRef::ref_from_ptr(py, &_slf))?; - async move { function(&__guard, #(#arg_name),*).await } + async move { function(&__guard, #(#arg_names),*).await } }} } FnType::Fn(SelfType::Receiver { mutable: true, .. }) => { - let (arg_name, evaluate_arg) = evaluate_args(); quote! {{ - #evaluate_arg; + #(let #arg_names = #args;)* let mut __guard = #pyo3_path::impl_::coroutine::RefMutGuard::<#cls>::new(&#pyo3_path::impl_::pymethods::BoundRef::ref_from_ptr(py, &_slf))?; - async move { function(&mut __guard, #(#arg_name),*).await } + async move { function(&mut __guard, #(#arg_names),*).await } }} } _ => { diff --git a/tests/test_coroutine.rs b/tests/test_coroutine.rs index 0e698deb9af..4abba9f36b4 100644 --- a/tests/test_coroutine.rs +++ b/tests/test_coroutine.rs @@ -245,39 +245,6 @@ fn coroutine_panic() { }) } -#[test] -fn test_async_method_receiver_with_other_args() { - #[pyclass] - struct Value(i32); - #[pymethods] - impl Value { - #[new] - fn new() -> Self { - Self(0) - } - async fn get_value_plus_with(&self, v: i32) -> i32 { - self.0 + v - } - async fn set_value(&mut self, new_value: i32) -> i32 { - self.0 = new_value; - self.0 - } - } - - Python::with_gil(|gil| { - let test = r#" - import asyncio - - v = Value() - assert asyncio.run(v.get_value_plus_with(3)) == 3 - assert asyncio.run(v.set_value(10)) == 10 - assert asyncio.run(v.get_value_plus_with(1)) == 11 - "#; - let locals = [("Value", gil.get_type_bound::())].into_py_dict_bound(gil); - py_run!(gil, *locals, test); - }); -} - #[test] fn test_async_method_receiver() { #[pyclass] @@ -341,3 +308,36 @@ fn test_async_method_receiver() { assert!(IS_DROPPED.load(Ordering::SeqCst)); } + +#[test] +fn test_async_method_receiver_with_other_args() { + #[pyclass] + struct Value(i32); + #[pymethods] + impl Value { + #[new] + fn new() -> Self { + Self(0) + } + async fn get_value_plus_with(&self, v1: i32, v2: i32) -> i32 { + self.0 + v1 + v2 + } + async fn set_value(&mut self, new_value: i32) -> i32 { + self.0 = new_value; + self.0 + } + } + + Python::with_gil(|gil| { + let test = r#" + import asyncio + + v = Value() + assert asyncio.run(v.get_value_plus_with(3, 0)) == 3 + assert asyncio.run(v.set_value(10)) == 10 + assert asyncio.run(v.get_value_plus_with(1, 1)) == 12 + "#; + let locals = [("Value", gil.get_type_bound::())].into_py_dict_bound(gil); + py_run!(gil, *locals, test); + }); +} From 7a00b4d357c0250c2212e856782143f283a7c2bb Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Thu, 4 Apr 2024 21:08:51 +0200 Subject: [PATCH 002/495] add descriptive error msg for `__traverse__` receivers other than `&self` (#4045) * add descriptive error msg for `__traverse__` receivers other than `self` * add newsfragment * improve error message --- newsfragments/4045.fixed.md | 1 + pyo3-macros-backend/src/pymethod.rs | 21 ++++++++++-- tests/ui/traverse.rs | 50 ++++++++++++++++++++++++++--- tests/ui/traverse.stderr | 42 +++++++++++++----------- 4 files changed, 88 insertions(+), 26 deletions(-) create mode 100644 newsfragments/4045.fixed.md diff --git a/newsfragments/4045.fixed.md b/newsfragments/4045.fixed.md new file mode 100644 index 00000000000..6b2bbcfa01d --- /dev/null +++ b/newsfragments/4045.fixed.md @@ -0,0 +1 @@ +Add better error message on wrong receiver extraction in `__traverse__`. diff --git a/pyo3-macros-backend/src/pymethod.rs b/pyo3-macros-backend/src/pymethod.rs index ee7d3d7aaee..abe8c7ac8a3 100644 --- a/pyo3-macros-backend/src/pymethod.rs +++ b/pyo3-macros-backend/src/pymethod.rs @@ -434,9 +434,24 @@ fn impl_traverse_slot( let Ctx { pyo3_path } = ctx; if let (Some(py_arg), _) = split_off_python_arg(&spec.signature.arguments) { return Err(syn::Error::new_spanned(py_arg.ty, "__traverse__ may not take `Python`. \ - Usually, an implementation of `__traverse__` should do nothing but calls to `visit.call`. \ - Most importantly, safe access to the GIL is prohibited inside implementations of `__traverse__`, \ - i.e. `Python::with_gil` will panic.")); + Usually, an implementation of `__traverse__(&self, visit: PyVisit<'_>) -> Result<(), PyTraverseError>` \ + should do nothing but calls to `visit.call`. Most importantly, safe access to the GIL is prohibited \ + inside implementations of `__traverse__`, i.e. `Python::with_gil` will panic.")); + } + + // check that the receiver does not try to smuggle an (implicit) `Python` token into here + if let FnType::Fn(SelfType::TryFromBoundRef(span)) + | FnType::Fn(SelfType::Receiver { + mutable: true, + span, + }) = spec.tp + { + bail_spanned! { span => + "__traverse__ may not take a receiver other than `&self`. Usually, an implementation of \ + `__traverse__(&self, visit: PyVisit<'_>) -> Result<(), PyTraverseError>` \ + should do nothing but calls to `visit.call`. Most importantly, safe access to the GIL is prohibited \ + inside implementations of `__traverse__`, i.e. `Python::with_gil` will panic." + } } let rust_fn_ident = spec.name; diff --git a/tests/ui/traverse.rs b/tests/ui/traverse.rs index 034224951c9..0cf7170db21 100644 --- a/tests/ui/traverse.rs +++ b/tests/ui/traverse.rs @@ -1,13 +1,55 @@ use pyo3::prelude::*; -use pyo3::PyVisit; use pyo3::PyTraverseError; +use pyo3::PyVisit; #[pyclass] struct TraverseTriesToTakePyRef {} #[pymethods] impl TraverseTriesToTakePyRef { - fn __traverse__(slf: PyRef, visit: PyVisit) {} + fn __traverse__(slf: PyRef, visit: PyVisit) -> Result<(), PyTraverseError> { + Ok(()) + } +} + +#[pyclass] +struct TraverseTriesToTakePyRefMut {} + +#[pymethods] +impl TraverseTriesToTakePyRefMut { + fn __traverse__(slf: PyRefMut, visit: PyVisit) -> Result<(), PyTraverseError> { + Ok(()) + } +} + +#[pyclass] +struct TraverseTriesToTakeBound {} + +#[pymethods] +impl TraverseTriesToTakeBound { + fn __traverse__(slf: Bound<'_, Self>, visit: PyVisit) -> Result<(), PyTraverseError> { + Ok(()) + } +} + +#[pyclass] +struct TraverseTriesToTakeMutSelf {} + +#[pymethods] +impl TraverseTriesToTakeMutSelf { + fn __traverse__(&mut self, visit: PyVisit) -> Result<(), PyTraverseError> { + Ok(()) + } +} + +#[pyclass] +struct TraverseTriesToTakeSelf {} + +#[pymethods] +impl TraverseTriesToTakeSelf { + fn __traverse__(&self, visit: PyVisit) -> Result<(), PyTraverseError> { + Ok(()) + } } #[pyclass] @@ -19,9 +61,7 @@ impl Class { Ok(()) } - fn __clear__(&mut self) { - } + fn __clear__(&mut self) {} } - fn main() {} diff --git a/tests/ui/traverse.stderr b/tests/ui/traverse.stderr index 4448e67e13a..5b1d1b6b2ec 100644 --- a/tests/ui/traverse.stderr +++ b/tests/ui/traverse.stderr @@ -1,23 +1,29 @@ -error: __traverse__ may not take `Python`. Usually, an implementation of `__traverse__` should do nothing but calls to `visit.call`. Most importantly, safe access to the GIL is prohibited inside implementations of `__traverse__`, i.e. `Python::with_gil` will panic. - --> tests/ui/traverse.rs:18:32 +error: __traverse__ may not take a receiver other than `&self`. Usually, an implementation of `__traverse__(&self, visit: PyVisit<'_>) -> Result<(), PyTraverseError>` should do nothing but calls to `visit.call`. Most importantly, safe access to the GIL is prohibited inside implementations of `__traverse__`, i.e. `Python::with_gil` will panic. + --> tests/ui/traverse.rs:10:26 | -18 | fn __traverse__(&self, py: Python<'_>, visit: PyVisit<'_>) -> Result<(), PyTraverseError> { - | ^^^^^^^^^^ +10 | fn __traverse__(slf: PyRef, visit: PyVisit) -> Result<(), PyTraverseError> { + | ^^^^^ + +error: __traverse__ may not take a receiver other than `&self`. Usually, an implementation of `__traverse__(&self, visit: PyVisit<'_>) -> Result<(), PyTraverseError>` should do nothing but calls to `visit.call`. Most importantly, safe access to the GIL is prohibited inside implementations of `__traverse__`, i.e. `Python::with_gil` will panic. + --> tests/ui/traverse.rs:20:26 + | +20 | fn __traverse__(slf: PyRefMut, visit: PyVisit) -> Result<(), PyTraverseError> { + | ^^^^^^^^ -error[E0308]: mismatched types - --> tests/ui/traverse.rs:9:6 +error: __traverse__ may not take a receiver other than `&self`. Usually, an implementation of `__traverse__(&self, visit: PyVisit<'_>) -> Result<(), PyTraverseError>` should do nothing but calls to `visit.call`. Most importantly, safe access to the GIL is prohibited inside implementations of `__traverse__`, i.e. `Python::with_gil` will panic. + --> tests/ui/traverse.rs:30:26 | -8 | #[pymethods] - | ------------ arguments to this function are incorrect -9 | impl TraverseTriesToTakePyRef { - | ______^ -10 | | fn __traverse__(slf: PyRef, visit: PyVisit) {} - | |___________________^ expected fn pointer, found fn item +30 | fn __traverse__(slf: Bound<'_, Self>, visit: PyVisit) -> Result<(), PyTraverseError> { + | ^^^^^ + +error: __traverse__ may not take a receiver other than `&self`. Usually, an implementation of `__traverse__(&self, visit: PyVisit<'_>) -> Result<(), PyTraverseError>` should do nothing but calls to `visit.call`. Most importantly, safe access to the GIL is prohibited inside implementations of `__traverse__`, i.e. `Python::with_gil` will panic. + --> tests/ui/traverse.rs:40:21 | - = note: expected fn pointer `for<'a, 'b> fn(&'a TraverseTriesToTakePyRef, PyVisit<'b>) -> Result<(), PyTraverseError>` - found fn item `for<'a, 'b> fn(pyo3::PyRef<'a, TraverseTriesToTakePyRef, >, PyVisit<'b>) {TraverseTriesToTakePyRef::__traverse__}` -note: function defined here - --> src/impl_/pymethods.rs +40 | fn __traverse__(&mut self, visit: PyVisit) -> Result<(), PyTraverseError> { + | ^ + +error: __traverse__ may not take `Python`. Usually, an implementation of `__traverse__(&self, visit: PyVisit<'_>) -> Result<(), PyTraverseError>` should do nothing but calls to `visit.call`. Most importantly, safe access to the GIL is prohibited inside implementations of `__traverse__`, i.e. `Python::with_gil` will panic. + --> tests/ui/traverse.rs:60:32 | - | pub unsafe fn _call_traverse( - | ^^^^^^^^^^^^^^ +60 | fn __traverse__(&self, py: Python<'_>, visit: PyVisit<'_>) -> Result<(), PyTraverseError> { + | ^^^^^^^^^^ From 2f0869a6d643e790e71dd5a676a30a7e539e5138 Mon Sep 17 00:00:00 2001 From: Joseph Perez Date: Sun, 7 Apr 2024 00:36:52 +0200 Subject: [PATCH 003/495] fix: allow impl of Ungil for deprecated PyCell in nightly (#4053) --- src/marker.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/marker.rs b/src/marker.rs index 67119b5e55e..b1f2d399209 100644 --- a/src/marker.rs +++ b/src/marker.rs @@ -280,6 +280,7 @@ mod nightly { impl !Ungil for crate::PyAny {} // All the borrowing wrappers + #[allow(deprecated)] impl !Ungil for crate::PyCell {} impl !Ungil for crate::PyRef<'_, T> {} impl !Ungil for crate::PyRefMut<'_, T> {} From 47f9ef41747ff1e7d89f584797ea9fc0d88dd1d5 Mon Sep 17 00:00:00 2001 From: "Jeong, Heon" Date: Thu, 11 Apr 2024 14:07:56 -0700 Subject: [PATCH 004/495] Fix typo (#4052) Fix incorrect closing brackets --- guide/src/ecosystem/async-await.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/guide/src/ecosystem/async-await.md b/guide/src/ecosystem/async-await.md index 0319fa05063..9c0ad19bdef 100644 --- a/guide/src/ecosystem/async-await.md +++ b/guide/src/ecosystem/async-await.md @@ -120,7 +120,7 @@ Export an async function that makes use of `async-std`: use pyo3::{prelude::*, wrap_pyfunction}; #[pyfunction] -fn rust_sleep(py: Python<'_>) -> PyResult<&Bound<'_, PyAny>>> { +fn rust_sleep(py: Python<'_>) -> PyResult<&Bound<'_, PyAny>> { pyo3_asyncio::async_std::future_into_py(py, async { async_std::task::sleep(std::time::Duration::from_secs(1)).await; Ok(Python::with_gil(|py| py.None())) From 631c25f2f9ef2d2f73ea1510cc6aba0581b0af16 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 11 Apr 2024 21:08:44 +0000 Subject: [PATCH 005/495] build(deps): bump peaceiris/actions-gh-pages from 3 to 4 (#4062) Bumps [peaceiris/actions-gh-pages](https://github.com/peaceiris/actions-gh-pages) from 3 to 4. - [Release notes](https://github.com/peaceiris/actions-gh-pages/releases) - [Changelog](https://github.com/peaceiris/actions-gh-pages/blob/main/CHANGELOG.md) - [Commits](https://github.com/peaceiris/actions-gh-pages/compare/v3...v4) --- updated-dependencies: - dependency-name: peaceiris/actions-gh-pages dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/gh-pages.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/gh-pages.yml b/.github/workflows/gh-pages.yml index a9a7669054a..a101eb6ad25 100644 --- a/.github/workflows/gh-pages.yml +++ b/.github/workflows/gh-pages.yml @@ -46,7 +46,7 @@ jobs: - name: Deploy docs and the guide if: ${{ github.event_name == 'release' }} - uses: peaceiris/actions-gh-pages@v3 + uses: peaceiris/actions-gh-pages@v4 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: ./target/guide/ From 9a6b1962d39c0acd6ddc42b186151611018cbd69 Mon Sep 17 00:00:00 2001 From: Liam Date: Thu, 11 Apr 2024 22:11:51 +0100 Subject: [PATCH 006/495] Minor: Fix a typo in Contributing.md (#4066) --- Contributing.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Contributing.md b/Contributing.md index 29d1bb758a7..054099b431a 100644 --- a/Contributing.md +++ b/Contributing.md @@ -190,7 +190,7 @@ Second, there is a Python-based benchmark contained in the `pytests` subdirector You can view what code is and isn't covered by PyO3's tests. We aim to have 100% coverage - please check coverage and add tests if you notice a lack of coverage! -- First, ensure the llmv-cov cargo plugin is installed. You may need to run the plugin through cargo once before using it with `nox`. +- First, ensure the llvm-cov cargo plugin is installed. You may need to run the plugin through cargo once before using it with `nox`. ```shell cargo install cargo-llvm-cov cargo llvm-cov From c8b59d71176a371f9feaa8abfa884f9fd451fbb5 Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Fri, 12 Apr 2024 08:34:08 +0200 Subject: [PATCH 007/495] add `#[doc(hidden)]` to the Rust module created by `#[pymodule]` (#4067) * add `#[doc(hidden)]` to the Rust module created by `#[pymodule]` * add newsfragment --- newsfragments/4067.fixed.md | 1 + pyo3-macros-backend/src/module.rs | 1 + tests/test_compile_error.rs | 1 + tests/ui/pymodule_missing_docs.rs | 12 ++++++++++++ 4 files changed, 15 insertions(+) create mode 100644 newsfragments/4067.fixed.md create mode 100644 tests/ui/pymodule_missing_docs.rs diff --git a/newsfragments/4067.fixed.md b/newsfragments/4067.fixed.md new file mode 100644 index 00000000000..869b6addf15 --- /dev/null +++ b/newsfragments/4067.fixed.md @@ -0,0 +1 @@ +fixes `missing_docs` lint to trigger on documented `#[pymodule]` functions \ No newline at end of file diff --git a/pyo3-macros-backend/src/module.rs b/pyo3-macros-backend/src/module.rs index 080a279a88c..8ff720cc616 100644 --- a/pyo3-macros-backend/src/module.rs +++ b/pyo3-macros-backend/src/module.rs @@ -324,6 +324,7 @@ pub fn pymodule_function_impl(mut function: syn::ItemFn) -> Result Ok(quote! { #function + #[doc(hidden)] #vis mod #ident { #initialization } diff --git a/tests/test_compile_error.rs b/tests/test_compile_error.rs index 38b5c4727c6..44049620598 100644 --- a/tests/test_compile_error.rs +++ b/tests/test_compile_error.rs @@ -53,4 +53,5 @@ fn test_compile_errors() { #[cfg(feature = "experimental-async")] #[cfg(any(not(Py_LIMITED_API), Py_3_10))] // to avoid PyFunctionArgument for &str t.compile_fail("tests/ui/invalid_cancel_handle.rs"); + t.pass("tests/ui/pymodule_missing_docs.rs"); } diff --git a/tests/ui/pymodule_missing_docs.rs b/tests/ui/pymodule_missing_docs.rs new file mode 100644 index 00000000000..1b196fa65e0 --- /dev/null +++ b/tests/ui/pymodule_missing_docs.rs @@ -0,0 +1,12 @@ +#![deny(missing_docs)] +//! Some crate docs + +use pyo3::prelude::*; + +/// Some module documentation +#[pymodule] +pub fn python_module(_m: &Bound<'_, PyModule>) -> PyResult<()> { + Ok(()) +} + +fn main() {} From ee5216f406889a9b993bfbe2e9dd977027e57075 Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Fri, 12 Apr 2024 08:34:27 +0200 Subject: [PATCH 008/495] fix declarative-modules compile error (#4054) * fix declarative-modules compile error * add newsfragment --- newsfragments/4054.fixed.md | 1 + pyo3-macros-backend/src/pyclass.rs | 2 +- tests/test_declarative_module.rs | 11 +++++++++++ 3 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 newsfragments/4054.fixed.md diff --git a/newsfragments/4054.fixed.md b/newsfragments/4054.fixed.md new file mode 100644 index 00000000000..4b8da92ca4d --- /dev/null +++ b/newsfragments/4054.fixed.md @@ -0,0 +1 @@ +Fixes a compile error when exporting a `#[pyclass]` living in a different Rust module using the declarative-module feature. diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index cf8d9aae801..aff23b879f5 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -1614,7 +1614,7 @@ impl<'a> PyClassImplsBuilder<'a> { quote! { impl #cls { #[doc(hidden)] - const _PYO3_DEF: #pyo3_path::impl_::pymodule::AddClassToModule = #pyo3_path::impl_::pymodule::AddClassToModule::new(); + pub const _PYO3_DEF: #pyo3_path::impl_::pymodule::AddClassToModule = #pyo3_path::impl_::pymodule::AddClassToModule::new(); } } } diff --git a/tests/test_declarative_module.rs b/tests/test_declarative_module.rs index 5f8e2c9fa55..8e432c3ae58 100644 --- a/tests/test_declarative_module.rs +++ b/tests/test_declarative_module.rs @@ -9,6 +9,13 @@ use pyo3::types::PyBool; #[path = "../src/tests/common.rs"] mod common; +mod some_module { + use pyo3::prelude::*; + + #[pyclass] + pub struct SomePyClass; +} + #[pyclass] struct ValueClass { value: usize, @@ -50,6 +57,10 @@ mod declarative_module { #[pymodule_export] use super::{declarative_module2, double, MyError, ValueClass as Value}; + // test for #4036 + #[pymodule_export] + use super::some_module::SomePyClass; + #[pymodule] mod inner { use super::*; From 30348b4d3fc433508cdc9b71ae7a973ec5e40235 Mon Sep 17 00:00:00 2001 From: messense Date: Sat, 13 Apr 2024 15:43:06 +0800 Subject: [PATCH 009/495] Link libpython for AIX target (#4073) --- newsfragments/4073.fixed.md | 1 + pyo3-build-config/src/impl_.rs | 2 ++ 2 files changed, 3 insertions(+) create mode 100644 newsfragments/4073.fixed.md diff --git a/newsfragments/4073.fixed.md b/newsfragments/4073.fixed.md new file mode 100644 index 00000000000..0f77647e42d --- /dev/null +++ b/newsfragments/4073.fixed.md @@ -0,0 +1 @@ +fixes undefined symbol errors when building extension module on AIX by linking `libpython` diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index d5373db9655..2ee68503faa 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -775,6 +775,8 @@ pub fn is_linking_libpython() -> bool { /// Must be called from a PyO3 crate build script. fn is_linking_libpython_for_target(target: &Triple) -> bool { target.operating_system == OperatingSystem::Windows + // See https://github.com/PyO3/pyo3/issues/4068#issuecomment-2051159852 + || target.operating_system == OperatingSystem::Aix || target.environment == Environment::Android || target.environment == Environment::Androideabi || !is_extension_module() From 4e5167db4241e9c003c922b148fefd870eb0edad Mon Sep 17 00:00:00 2001 From: Adam Reichold Date: Sat, 13 Apr 2024 09:57:13 +0200 Subject: [PATCH 010/495] Extend guide on interaction between method receivers and lifetime elision. (#4069) --- guide/src/class.md | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/guide/src/class.md b/guide/src/class.md index f353cc4787e..b5ef95cb2f7 100644 --- a/guide/src/class.md +++ b/guide/src/class.md @@ -998,6 +998,44 @@ impl MyClass { Note that `text_signature` on `#[new]` is not compatible with compilation in `abi3` mode until Python 3.10 or greater. +### Method receivers and lifetime elision + +PyO3 supports writing instance methods using the normal method receivers for shared `&self` and unique `&mut self` references. This interacts with [lifetime elision][lifetime-elision] insofar as the lifetime of a such a receiver is assigned to all elided output lifetime parameters. + +This is a good default for general Rust code where return values are more likely to borrow from the receiver than from the other arguments, if they contain any lifetimes at all. However, when returning bound references `Bound<'py, T>` in PyO3-based code, the GIL lifetime `'py` should usually be derived from a GIL token `py: Python<'py>` passed as an argument instead of the receiver. + +Specifically, signatures like + +```rust,ignore +fn frobnicate(&self, py: Python) -> Bound; +``` + +will not work as they are inferred as + +```rust,ignore +fn frobnicate<'a, 'py>(&'a self, py: Python<'py>) -> Bound<'a, Foo>; +``` + +instead of the intended + +```rust,ignore +fn frobnicate<'a, 'py>(&'a self, py: Python<'py>) -> Bound<'py, Foo>; +``` + +and should usually be written as + +```rust,ignore +fn frobnicate<'py>(&self, py: Python<'py>) -> Bound<'py, Foo>; +``` + +The same problem does not exist for `#[pyfunction]`s as the special case for receiver lifetimes does not apply and indeed a signature like + +```rust,ignore +fn frobnicate(bar: &Bar, py: Python) -> Bound; +``` + +will yield compiler error [E0106 "missing lifetime specifier"][compiler-error-e0106]. + ## `#[pyclass]` enums Enum support in PyO3 comes in two flavors, depending on what kind of variants the enum has: simple and complex. @@ -1329,3 +1367,6 @@ impl pyo3::impl_::pyclass::PyClassImpl for MyClass { [classattr]: https://docs.python.org/3/tutorial/classes.html#class-and-instance-variables [`multiple-pymethods`]: features.md#multiple-pymethods + +[lifetime-elision]: https://doc.rust-lang.org/reference/lifetime-elision.html +[compiler-error-e0106]: https://doc.rust-lang.org/error_codes/E0106.html From 721100a9e981391de784022bf564285f772314ea Mon Sep 17 00:00:00 2001 From: Adam Reichold Date: Sat, 13 Apr 2024 14:59:44 +0200 Subject: [PATCH 011/495] Suppress non_local_definitions lint as we often want the non-local effects in macro code (#4074) * Resolve references to legacy numerical constants and use the associated constants instead * Suppress non_local_definitions lint as we often want the non-local effects in macro code --- pyo3-ffi/src/pyport.rs | 4 ++-- pyo3-macros-backend/src/module.rs | 2 ++ pyo3-macros-backend/src/pyfunction.rs | 1 + pyo3-macros-backend/src/pymethod.rs | 1 + src/callback.rs | 7 +------ src/conversions/std/num.rs | 28 +++++++++++++-------------- src/impl_/pyclass.rs | 1 + src/pycell/impl_.rs | 2 +- src/types/any.rs | 1 + tests/test_proto_methods.rs | 2 +- 10 files changed, 25 insertions(+), 24 deletions(-) diff --git a/pyo3-ffi/src/pyport.rs b/pyo3-ffi/src/pyport.rs index 741b0db7bf8..a144c67fb1b 100644 --- a/pyo3-ffi/src/pyport.rs +++ b/pyo3-ffi/src/pyport.rs @@ -11,8 +11,8 @@ pub type Py_ssize_t = ::libc::ssize_t; pub type Py_hash_t = Py_ssize_t; pub type Py_uhash_t = ::libc::size_t; -pub const PY_SSIZE_T_MIN: Py_ssize_t = std::isize::MIN as Py_ssize_t; -pub const PY_SSIZE_T_MAX: Py_ssize_t = std::isize::MAX as Py_ssize_t; +pub const PY_SSIZE_T_MIN: Py_ssize_t = isize::MIN as Py_ssize_t; +pub const PY_SSIZE_T_MAX: Py_ssize_t = isize::MAX as Py_ssize_t; #[cfg(target_endian = "big")] pub const PY_BIG_ENDIAN: usize = 1; diff --git a/pyo3-macros-backend/src/module.rs b/pyo3-macros-backend/src/module.rs index 8ff720cc616..3153279a2b8 100644 --- a/pyo3-macros-backend/src/module.rs +++ b/pyo3-macros-backend/src/module.rs @@ -249,6 +249,7 @@ pub fn pymodule_module_impl(mut module: syn::ItemMod) -> Result { #initialization + #[allow(unknown_lints, non_local_definitions)] impl MakeDef { const fn make_def() -> #pyo3_path::impl_::pymodule::ModuleDef { use #pyo3_path::impl_::pymodule as impl_; @@ -333,6 +334,7 @@ pub fn pymodule_function_impl(mut function: syn::ItemFn) -> Result // this avoids complications around the fact that the generated module has a different scope // (and `super` doesn't always refer to the outer scope, e.g. if the `#[pymodule] is // inside a function body) + #[allow(unknown_lints, non_local_definitions)] impl #ident::MakeDef { const fn make_def() -> #pyo3_path::impl_::pymodule::ModuleDef { fn __pyo3_pymodule(module: &#pyo3_path::Bound<'_, #pyo3_path::types::PyModule>) -> #pyo3_path::PyResult<()> { diff --git a/pyo3-macros-backend/src/pyfunction.rs b/pyo3-macros-backend/src/pyfunction.rs index 9f9557a3664..7c355533b83 100644 --- a/pyo3-macros-backend/src/pyfunction.rs +++ b/pyo3-macros-backend/src/pyfunction.rs @@ -273,6 +273,7 @@ pub fn impl_wrap_pyfunction( // this avoids complications around the fact that the generated module has a different scope // (and `super` doesn't always refer to the outer scope, e.g. if the `#[pyfunction] is // inside a function body) + #[allow(unknown_lints, non_local_definitions)] impl #name::MakeDef { const _PYO3_DEF: #pyo3_path::impl_::pymethods::PyMethodDef = #methoddef; } diff --git a/pyo3-macros-backend/src/pymethod.rs b/pyo3-macros-backend/src/pymethod.rs index abe8c7ac8a3..7a4c54db9da 100644 --- a/pyo3-macros-backend/src/pymethod.rs +++ b/pyo3-macros-backend/src/pymethod.rs @@ -366,6 +366,7 @@ pub fn impl_py_method_def_new( #deprecations use #pyo3_path::impl_::pyclass::*; + #[allow(unknown_lints, non_local_definitions)] impl PyClassNewTextSignature<#cls> for PyClassImplCollector<#cls> { #[inline] fn new_text_signature(self) -> ::std::option::Option<&'static str> { diff --git a/src/callback.rs b/src/callback.rs index a56b268aa1e..1e446039904 100644 --- a/src/callback.rs +++ b/src/callback.rs @@ -4,7 +4,6 @@ use crate::err::{PyErr, PyResult}; use crate::exceptions::PyOverflowError; use crate::ffi::{self, Py_hash_t}; use crate::{IntoPy, PyObject, Python}; -use std::isize; use std::os::raw::c_int; /// A type which can be the return type of a python C-API callback @@ -85,11 +84,7 @@ impl IntoPyCallbackOutput<()> for () { impl IntoPyCallbackOutput for usize { #[inline] fn convert(self, _py: Python<'_>) -> PyResult { - if self <= (isize::MAX as usize) { - Ok(self as isize) - } else { - Err(PyOverflowError::new_err(())) - } + self.try_into().map_err(|_err| PyOverflowError::new_err(())) } } diff --git a/src/conversions/std/num.rs b/src/conversions/std/num.rs index 44843141440..e2072d210e0 100644 --- a/src/conversions/std/num.rs +++ b/src/conversions/std/num.rs @@ -445,7 +445,7 @@ mod test_128bit_integers { #[test] fn test_i128_max() { Python::with_gil(|py| { - let v = std::i128::MAX; + let v = i128::MAX; let obj = v.to_object(py); assert_eq!(v, obj.extract::(py).unwrap()); assert_eq!(v as u128, obj.extract::(py).unwrap()); @@ -456,7 +456,7 @@ mod test_128bit_integers { #[test] fn test_i128_min() { Python::with_gil(|py| { - let v = std::i128::MIN; + let v = i128::MIN; let obj = v.to_object(py); assert_eq!(v, obj.extract::(py).unwrap()); assert!(obj.extract::(py).is_err()); @@ -467,7 +467,7 @@ mod test_128bit_integers { #[test] fn test_u128_max() { Python::with_gil(|py| { - let v = std::u128::MAX; + let v = u128::MAX; let obj = v.to_object(py); assert_eq!(v, obj.extract::(py).unwrap()); assert!(obj.extract::(py).is_err()); @@ -495,7 +495,7 @@ mod test_128bit_integers { #[test] fn test_nonzero_i128_max() { Python::with_gil(|py| { - let v = NonZeroI128::new(std::i128::MAX).unwrap(); + let v = NonZeroI128::new(i128::MAX).unwrap(); let obj = v.to_object(py); assert_eq!(v, obj.extract::(py).unwrap()); assert_eq!( @@ -509,7 +509,7 @@ mod test_128bit_integers { #[test] fn test_nonzero_i128_min() { Python::with_gil(|py| { - let v = NonZeroI128::new(std::i128::MIN).unwrap(); + let v = NonZeroI128::new(i128::MIN).unwrap(); let obj = v.to_object(py); assert_eq!(v, obj.extract::(py).unwrap()); assert!(obj.extract::(py).is_err()); @@ -520,7 +520,7 @@ mod test_128bit_integers { #[test] fn test_nonzero_u128_max() { Python::with_gil(|py| { - let v = NonZeroU128::new(std::u128::MAX).unwrap(); + let v = NonZeroU128::new(u128::MAX).unwrap(); let obj = v.to_object(py); assert_eq!(v, obj.extract::(py).unwrap()); assert!(obj.extract::(py).is_err()); @@ -573,7 +573,7 @@ mod tests { #[test] fn test_u32_max() { Python::with_gil(|py| { - let v = std::u32::MAX; + let v = u32::MAX; let obj = v.to_object(py); assert_eq!(v, obj.extract::(py).unwrap()); assert_eq!(u64::from(v), obj.extract::(py).unwrap()); @@ -584,7 +584,7 @@ mod tests { #[test] fn test_i64_max() { Python::with_gil(|py| { - let v = std::i64::MAX; + let v = i64::MAX; let obj = v.to_object(py); assert_eq!(v, obj.extract::(py).unwrap()); assert_eq!(v as u64, obj.extract::(py).unwrap()); @@ -595,7 +595,7 @@ mod tests { #[test] fn test_i64_min() { Python::with_gil(|py| { - let v = std::i64::MIN; + let v = i64::MIN; let obj = v.to_object(py); assert_eq!(v, obj.extract::(py).unwrap()); assert!(obj.extract::(py).is_err()); @@ -606,7 +606,7 @@ mod tests { #[test] fn test_u64_max() { Python::with_gil(|py| { - let v = std::u64::MAX; + let v = u64::MAX; let obj = v.to_object(py); assert_eq!(v, obj.extract::(py).unwrap()); assert!(obj.extract::(py).is_err()); @@ -664,7 +664,7 @@ mod tests { #[test] fn test_nonzero_u32_max() { Python::with_gil(|py| { - let v = NonZeroU32::new(std::u32::MAX).unwrap(); + let v = NonZeroU32::new(u32::MAX).unwrap(); let obj = v.to_object(py); assert_eq!(v, obj.extract::(py).unwrap()); assert_eq!(NonZeroU64::from(v), obj.extract::(py).unwrap()); @@ -675,7 +675,7 @@ mod tests { #[test] fn test_nonzero_i64_max() { Python::with_gil(|py| { - let v = NonZeroI64::new(std::i64::MAX).unwrap(); + let v = NonZeroI64::new(i64::MAX).unwrap(); let obj = v.to_object(py); assert_eq!(v, obj.extract::(py).unwrap()); assert_eq!( @@ -689,7 +689,7 @@ mod tests { #[test] fn test_nonzero_i64_min() { Python::with_gil(|py| { - let v = NonZeroI64::new(std::i64::MIN).unwrap(); + let v = NonZeroI64::new(i64::MIN).unwrap(); let obj = v.to_object(py); assert_eq!(v, obj.extract::(py).unwrap()); assert!(obj.extract::(py).is_err()); @@ -700,7 +700,7 @@ mod tests { #[test] fn test_nonzero_u64_max() { Python::with_gil(|py| { - let v = NonZeroU64::new(std::u64::MAX).unwrap(); + let v = NonZeroU64::new(u64::MAX).unwrap(); let obj = v.to_object(py); assert_eq!(v, obj.extract::(py).unwrap()); assert!(obj.extract::(py).is_err()); diff --git a/src/impl_/pyclass.rs b/src/impl_/pyclass.rs index 1a144f736e0..1302834ca4b 100644 --- a/src/impl_/pyclass.rs +++ b/src/impl_/pyclass.rs @@ -852,6 +852,7 @@ slot_fragment_trait! { #[macro_export] macro_rules! generate_pyclass_richcompare_slot { ($cls:ty) => {{ + #[allow(unknown_lints, non_local_definitions)] impl $cls { #[allow(non_snake_case)] unsafe extern "C" fn __pymethod___richcmp____( diff --git a/src/pycell/impl_.rs b/src/pycell/impl_.rs index 378bec04993..1bdd44b9e9c 100644 --- a/src/pycell/impl_.rs +++ b/src/pycell/impl_.rs @@ -55,7 +55,7 @@ struct BorrowFlag(usize); impl BorrowFlag { pub(crate) const UNUSED: BorrowFlag = BorrowFlag(0); - const HAS_MUTABLE_BORROW: BorrowFlag = BorrowFlag(usize::max_value()); + const HAS_MUTABLE_BORROW: BorrowFlag = BorrowFlag(usize::MAX); const fn increment(self) -> Self { Self(self.0 + 1) } diff --git a/src/types/any.rs b/src/types/any.rs index ab4f5727623..a5ea4c80d4c 100644 --- a/src/types/any.rs +++ b/src/types/any.rs @@ -2491,6 +2491,7 @@ class SimpleClass: #[cfg(feature = "macros")] #[test] + #[allow(unknown_lints, non_local_definitions)] fn test_hasattr_error() { use crate::exceptions::PyValueError; use crate::prelude::*; diff --git a/tests/test_proto_methods.rs b/tests/test_proto_methods.rs index dd707990c0b..c5d7306086d 100644 --- a/tests/test_proto_methods.rs +++ b/tests/test_proto_methods.rs @@ -3,7 +3,7 @@ use pyo3::exceptions::{PyAttributeError, PyIndexError, PyValueError}; use pyo3::types::{PyDict, PyList, PyMapping, PySequence, PySlice, PyType}; use pyo3::{prelude::*, py_run}; -use std::{isize, iter}; +use std::iter; #[path = "../src/tests/common.rs"] mod common; From cc7e16f4d6c1c4e9a19d0b514a4ef8ece6d14776 Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Sun, 14 Apr 2024 16:19:57 +0200 Subject: [PATCH 012/495] Refactoring of `FnArg` (#4033) * refactor `FnArg` * add UI tests * use enum variant types * add comment * remove dead code * remove last FIXME * review feedback davidhewitt --- pyo3-macros-backend/src/method.rs | 179 ++++++++++++++---- pyo3-macros-backend/src/params.rs | 172 +++++++---------- pyo3-macros-backend/src/pyclass.rs | 30 +-- .../src/pyfunction/signature.rs | 110 +++++++---- pyo3-macros-backend/src/pymethod.rs | 53 +++--- tests/ui/invalid_cancel_handle.rs | 6 + tests/ui/invalid_cancel_handle.stderr | 6 + tests/ui/invalid_pyfunctions.rs | 10 +- tests/ui/invalid_pyfunctions.stderr | 20 +- 9 files changed, 348 insertions(+), 238 deletions(-) diff --git a/pyo3-macros-backend/src/method.rs b/pyo3-macros-backend/src/method.rs index 6af0ec97d04..982cf62946e 100644 --- a/pyo3-macros-backend/src/method.rs +++ b/pyo3-macros-backend/src/method.rs @@ -6,7 +6,7 @@ use syn::{ext::IdentExt, spanned::Spanned, Ident, Result}; use crate::utils::Ctx; use crate::{ - attributes::{TextSignatureAttribute, TextSignatureAttributeValue}, + attributes::{FromPyWithAttribute, TextSignatureAttribute, TextSignatureAttributeValue}, deprecations::{Deprecation, Deprecations}, params::{impl_arg_params, Holders}, pyfunction::{ @@ -17,19 +17,109 @@ use crate::{ }; #[derive(Clone, Debug)] -pub struct FnArg<'a> { +pub struct RegularArg<'a> { pub name: &'a syn::Ident, pub ty: &'a syn::Type, - pub optional: Option<&'a syn::Type>, - pub default: Option, - pub py: bool, - pub attrs: PyFunctionArgPyO3Attributes, - pub is_varargs: bool, - pub is_kwargs: bool, - pub is_cancel_handle: bool, + pub from_py_with: Option, + pub default_value: Option, + pub option_wrapped_type: Option<&'a syn::Type>, +} + +/// Pythons *args argument +#[derive(Clone, Debug)] +pub struct VarargsArg<'a> { + pub name: &'a syn::Ident, + pub ty: &'a syn::Type, +} + +/// Pythons **kwarg argument +#[derive(Clone, Debug)] +pub struct KwargsArg<'a> { + pub name: &'a syn::Ident, + pub ty: &'a syn::Type, +} + +#[derive(Clone, Debug)] +pub struct CancelHandleArg<'a> { + pub name: &'a syn::Ident, + pub ty: &'a syn::Type, +} + +#[derive(Clone, Debug)] +pub struct PyArg<'a> { + pub name: &'a syn::Ident, + pub ty: &'a syn::Type, +} + +#[derive(Clone, Debug)] +pub enum FnArg<'a> { + Regular(RegularArg<'a>), + VarArgs(VarargsArg<'a>), + KwArgs(KwargsArg<'a>), + Py(PyArg<'a>), + CancelHandle(CancelHandleArg<'a>), } impl<'a> FnArg<'a> { + pub fn name(&self) -> &'a syn::Ident { + match self { + FnArg::Regular(RegularArg { name, .. }) => name, + FnArg::VarArgs(VarargsArg { name, .. }) => name, + FnArg::KwArgs(KwargsArg { name, .. }) => name, + FnArg::Py(PyArg { name, .. }) => name, + FnArg::CancelHandle(CancelHandleArg { name, .. }) => name, + } + } + + pub fn ty(&self) -> &'a syn::Type { + match self { + FnArg::Regular(RegularArg { ty, .. }) => ty, + FnArg::VarArgs(VarargsArg { ty, .. }) => ty, + FnArg::KwArgs(KwargsArg { ty, .. }) => ty, + FnArg::Py(PyArg { ty, .. }) => ty, + FnArg::CancelHandle(CancelHandleArg { ty, .. }) => ty, + } + } + + #[allow(clippy::wrong_self_convention)] + pub fn from_py_with(&self) -> Option<&FromPyWithAttribute> { + if let FnArg::Regular(RegularArg { from_py_with, .. }) = self { + from_py_with.as_ref() + } else { + None + } + } + + pub fn to_varargs_mut(&mut self) -> Result<&mut Self> { + if let Self::Regular(RegularArg { + name, + ty, + option_wrapped_type: None, + .. + }) = self + { + *self = Self::VarArgs(VarargsArg { name, ty }); + Ok(self) + } else { + bail_spanned!(self.name().span() => "args cannot be optional") + } + } + + pub fn to_kwargs_mut(&mut self) -> Result<&mut Self> { + if let Self::Regular(RegularArg { + name, + ty, + option_wrapped_type: Some(..), + .. + }) = self + { + *self = Self::KwArgs(KwargsArg { name, ty }); + Ok(self) + } else { + bail_spanned!(self.name().span() => "kwargs must be Option<_>") + } + } + /// Transforms a rust fn arg parsed with syn into a method::FnArg pub fn parse(arg: &'a mut syn::FnArg) -> Result { match arg { @@ -41,32 +131,43 @@ impl<'a> FnArg<'a> { bail_spanned!(cap.ty.span() => IMPL_TRAIT_ERR); } - let arg_attrs = PyFunctionArgPyO3Attributes::from_attrs(&mut cap.attrs)?; + let PyFunctionArgPyO3Attributes { + from_py_with, + cancel_handle, + } = PyFunctionArgPyO3Attributes::from_attrs(&mut cap.attrs)?; let ident = match &*cap.pat { syn::Pat::Ident(syn::PatIdent { ident, .. }) => ident, other => return Err(handle_argument_error(other)), }; - let is_cancel_handle = arg_attrs.cancel_handle.is_some(); + if utils::is_python(&cap.ty) { + return Ok(Self::Py(PyArg { + name: ident, + ty: &cap.ty, + })); + } - Ok(FnArg { + if cancel_handle.is_some() { + // `PyFunctionArgPyO3Attributes::from_attrs` validates that + // only compatible attributes are specified, either + // `cancel_handle` or `from_py_with`, dublicates and any + // combination of the two are already rejected. + return Ok(Self::CancelHandle(CancelHandleArg { + name: ident, + ty: &cap.ty, + })); + } + + Ok(Self::Regular(RegularArg { name: ident, ty: &cap.ty, - optional: utils::option_type_argument(&cap.ty), - default: None, - py: utils::is_python(&cap.ty), - attrs: arg_attrs, - is_varargs: false, - is_kwargs: false, - is_cancel_handle, - }) + from_py_with, + default_value: None, + option_wrapped_type: utils::option_type_argument(&cap.ty), + })) } } } - - pub fn is_regular(&self) -> bool { - !self.py && !self.is_cancel_handle && !self.is_kwargs && !self.is_varargs - } } fn handle_argument_error(pat: &syn::Pat) -> syn::Error { @@ -492,12 +593,14 @@ impl<'a> FnSpec<'a> { .signature .arguments .iter() - .filter(|arg| arg.is_cancel_handle); + .filter(|arg| matches!(arg, FnArg::CancelHandle(..))); let cancel_handle = cancel_handle_iter.next(); - if let Some(arg) = cancel_handle { - ensure_spanned!(self.asyncness.is_some(), arg.name.span() => "`cancel_handle` attribute can only be used with `async fn`"); - if let Some(arg2) = cancel_handle_iter.next() { - bail_spanned!(arg2.name.span() => "`cancel_handle` may only be specified once"); + if let Some(FnArg::CancelHandle(CancelHandleArg { name, .. })) = cancel_handle { + ensure_spanned!(self.asyncness.is_some(), name.span() => "`cancel_handle` attribute can only be used with `async fn`"); + if let Some(FnArg::CancelHandle(CancelHandleArg { name, .. })) = + cancel_handle_iter.next() + { + bail_spanned!(name.span() => "`cancel_handle` may only be specified once"); } } @@ -605,14 +708,10 @@ impl<'a> FnSpec<'a> { .signature .arguments .iter() - .map(|arg| { - if arg.py { - quote!(py) - } else if arg.is_cancel_handle { - quote!(__cancel_handle) - } else { - unreachable!() - } + .map(|arg| match arg { + FnArg::Py(..) => quote!(py), + FnArg::CancelHandle(..) => quote!(__cancel_handle), + _ => unreachable!("`CallingConvention::Noargs` should not contain any arguments (reaching Python) except for `self`, which is handled below."), }) .collect(); let call = rust_call(args, &mut holders); @@ -635,7 +734,7 @@ impl<'a> FnSpec<'a> { } CallingConvention::Fastcall => { let mut holders = Holders::new(); - let (arg_convert, args) = impl_arg_params(self, cls, true, &mut holders, ctx)?; + let (arg_convert, args) = impl_arg_params(self, cls, true, &mut holders, ctx); let call = rust_call(args, &mut holders); let init_holders = holders.init_holders(ctx); let check_gil_refs = holders.check_gil_refs(); @@ -660,7 +759,7 @@ impl<'a> FnSpec<'a> { } CallingConvention::Varargs => { let mut holders = Holders::new(); - let (arg_convert, args) = impl_arg_params(self, cls, false, &mut holders, ctx)?; + let (arg_convert, args) = impl_arg_params(self, cls, false, &mut holders, ctx); let call = rust_call(args, &mut holders); let init_holders = holders.init_holders(ctx); let check_gil_refs = holders.check_gil_refs(); @@ -684,7 +783,7 @@ impl<'a> FnSpec<'a> { } CallingConvention::TpNew => { let mut holders = Holders::new(); - let (arg_convert, args) = impl_arg_params(self, cls, false, &mut holders, ctx)?; + let (arg_convert, args) = impl_arg_params(self, cls, false, &mut holders, ctx); let self_arg = self .tp .self_arg(cls, ExtractErrorMode::Raise, &mut holders, ctx); diff --git a/pyo3-macros-backend/src/params.rs b/pyo3-macros-backend/src/params.rs index fa50d260986..d9f77fa07bc 100644 --- a/pyo3-macros-backend/src/params.rs +++ b/pyo3-macros-backend/src/params.rs @@ -1,13 +1,12 @@ use crate::utils::Ctx; use crate::{ - method::{FnArg, FnSpec}, + method::{FnArg, FnSpec, RegularArg}, pyfunction::FunctionSignature, quotes::some_wrap, }; use proc_macro2::{Span, TokenStream}; -use quote::{quote, quote_spanned}; +use quote::{format_ident, quote, quote_spanned}; use syn::spanned::Spanned; -use syn::Result; pub struct Holders { holders: Vec, @@ -60,16 +59,7 @@ impl Holders { pub fn is_forwarded_args(signature: &FunctionSignature<'_>) -> bool { matches!( signature.arguments.as_slice(), - [ - FnArg { - is_varargs: true, - .. - }, - FnArg { - is_kwargs: true, - .. - }, - ] + [FnArg::VarArgs(..), FnArg::KwArgs(..),] ) } @@ -90,7 +80,7 @@ pub fn impl_arg_params( fastcall: bool, holders: &mut Holders, ctx: &Ctx, -) -> Result<(TokenStream, Vec)> { +) -> (TokenStream, Vec) { let args_array = syn::Ident::new("output", Span::call_site()); let Ctx { pyo3_path } = ctx; @@ -100,9 +90,8 @@ pub fn impl_arg_params( .iter() .enumerate() .filter_map(|(i, arg)| { - let from_py_with = &arg.attrs.from_py_with.as_ref()?.value; - let from_py_with_holder = - syn::Ident::new(&format!("from_py_with_{}", i), Span::call_site()); + let from_py_with = &arg.from_py_with()?.value; + let from_py_with_holder = format_ident!("from_py_with_{}", i); Some(quote_spanned! { from_py_with.span() => let e = #pyo3_path::impl_::deprecations::GilRefs::new(); let #from_py_with_holder = #pyo3_path::impl_::deprecations::inspect_fn(#from_py_with, &e); @@ -119,28 +108,16 @@ pub fn impl_arg_params( .arguments .iter() .enumerate() - .map(|(i, arg)| { - let from_py_with = - syn::Ident::new(&format!("from_py_with_{}", i), Span::call_site()); - let arg_value = quote!(#args_array[0].as_deref()); - - impl_arg_param(arg, from_py_with, arg_value, holders, ctx).map(|tokens| { - check_arg_for_gil_refs( - tokens, - holders.push_gil_refs_checker(arg.ty.span()), - ctx, - ) - }) - }) - .collect::>()?; - return Ok(( + .map(|(i, arg)| impl_arg_param(arg, i, &mut 0, holders, ctx)) + .collect(); + return ( quote! { let _args = #pyo3_path::impl_::pymethods::BoundRef::ref_from_ptr(py, &_args); let _kwargs = #pyo3_path::impl_::pymethods::BoundRef::ref_from_ptr_or_opt(py, &_kwargs); #from_py_with }, arg_convert, - )); + ); }; let positional_parameter_names = &spec.signature.python_signature.positional_parameters; @@ -171,18 +148,8 @@ pub fn impl_arg_params( .arguments .iter() .enumerate() - .map(|(i, arg)| { - let from_py_with = syn::Ident::new(&format!("from_py_with_{}", i), Span::call_site()); - let arg_value = quote!(#args_array[#option_pos].as_deref()); - if arg.is_regular() { - option_pos += 1; - } - - impl_arg_param(arg, from_py_with, arg_value, holders, ctx).map(|tokens| { - check_arg_for_gil_refs(tokens, holders.push_gil_refs_checker(arg.ty.span()), ctx) - }) - }) - .collect::>()?; + .map(|(i, arg)| impl_arg_param(arg, i, &mut option_pos, holders, ctx)) + .collect(); let args_handler = if spec.signature.python_signature.varargs.is_some() { quote! { #pyo3_path::impl_::extract_argument::TupleVarargs } @@ -224,7 +191,7 @@ pub fn impl_arg_params( }; // create array of arguments, and then parse - Ok(( + ( quote! { const DESCRIPTION: #pyo3_path::impl_::extract_argument::FunctionDescription = #pyo3_path::impl_::extract_argument::FunctionDescription { cls_name: #cls_name, @@ -239,18 +206,64 @@ pub fn impl_arg_params( #from_py_with }, param_conversion, - )) + ) +} + +fn impl_arg_param( + arg: &FnArg<'_>, + pos: usize, + option_pos: &mut usize, + holders: &mut Holders, + ctx: &Ctx, +) -> TokenStream { + let Ctx { pyo3_path } = ctx; + let args_array = syn::Ident::new("output", Span::call_site()); + + match arg { + FnArg::Regular(arg) => { + let from_py_with = format_ident!("from_py_with_{}", pos); + let arg_value = quote!(#args_array[#option_pos].as_deref()); + *option_pos += 1; + let tokens = impl_regular_arg_param(arg, from_py_with, arg_value, holders, ctx); + check_arg_for_gil_refs(tokens, holders.push_gil_refs_checker(arg.ty.span()), ctx) + } + FnArg::VarArgs(arg) => { + let holder = holders.push_holder(arg.name.span()); + let name_str = arg.name.to_string(); + quote_spanned! { arg.name.span() => + #pyo3_path::impl_::extract_argument::extract_argument( + &_args, + &mut #holder, + #name_str + )? + } + } + FnArg::KwArgs(arg) => { + let holder = holders.push_holder(arg.name.span()); + let name_str = arg.name.to_string(); + quote_spanned! { arg.name.span() => + #pyo3_path::impl_::extract_argument::extract_optional_argument( + _kwargs.as_deref(), + &mut #holder, + #name_str, + || ::std::option::Option::None + )? + } + } + FnArg::Py(..) => quote! { py }, + FnArg::CancelHandle(..) => quote! { __cancel_handle }, + } } /// Re option_pos: The option slice doesn't contain the py: Python argument, so the argument /// index and the index in option diverge when using py: Python -pub(crate) fn impl_arg_param( - arg: &FnArg<'_>, +pub(crate) fn impl_regular_arg_param( + arg: &RegularArg<'_>, from_py_with: syn::Ident, arg_value: TokenStream, // expected type: Option<&'a Bound<'py, PyAny>> holders: &mut Holders, ctx: &Ctx, -) -> Result { +) -> TokenStream { let Ctx { pyo3_path } = ctx; let pyo3_path = pyo3_path.to_tokens_spanned(arg.ty.span()); @@ -260,64 +273,19 @@ pub(crate) fn impl_arg_param( ($($tokens:tt)*) => { quote_spanned!(arg.ty.span() => $($tokens)*) } } - if arg.py { - return Ok(quote! { py }); - } - - if arg.is_cancel_handle { - return Ok(quote! { __cancel_handle }); - } - - let name = arg.name; - let name_str = name.to_string(); - - if arg.is_varargs { - ensure_spanned!( - arg.optional.is_none(), - arg.name.span() => "args cannot be optional" - ); - let holder = holders.push_holder(arg.ty.span()); - return Ok(quote_arg_span! { - #pyo3_path::impl_::extract_argument::extract_argument( - &_args, - &mut #holder, - #name_str - )? - }); - } else if arg.is_kwargs { - ensure_spanned!( - arg.optional.is_some(), - arg.name.span() => "kwargs must be Option<_>" - ); - let holder = holders.push_holder(arg.name.span()); - return Ok(quote_arg_span! { - #pyo3_path::impl_::extract_argument::extract_optional_argument( - _kwargs.as_deref(), - &mut #holder, - #name_str, - || ::std::option::Option::None - )? - }); - } - - let mut default = arg.default.as_ref().map(|expr| quote!(#expr)); + let name_str = arg.name.to_string(); + let mut default = arg.default_value.as_ref().map(|expr| quote!(#expr)); // Option arguments have special treatment: the default should be specified _without_ the // Some() wrapper. Maybe this should be changed in future?! - if arg.optional.is_some() { + if arg.option_wrapped_type.is_some() { default = Some(default.map_or_else( || quote!(::std::option::Option::None), |tokens| some_wrap(tokens, ctx), )); } - let tokens = if arg - .attrs - .from_py_with - .as_ref() - .map(|attr| &attr.value) - .is_some() - { + if arg.from_py_with.is_some() { if let Some(default) = default { quote_arg_span! { #pyo3_path::impl_::extract_argument::from_py_with_with_default( @@ -339,7 +307,7 @@ pub(crate) fn impl_arg_param( )? } } - } else if arg.optional.is_some() { + } else if arg.option_wrapped_type.is_some() { let holder = holders.push_holder(arg.name.span()); quote_arg_span! { #pyo3_path::impl_::extract_argument::extract_optional_argument( @@ -374,7 +342,5 @@ pub(crate) fn impl_arg_param( #name_str )? } - }; - - Ok(tokens) + } } diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index aff23b879f5..d9c84655b42 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -7,7 +7,7 @@ use crate::attributes::{ }; use crate::deprecations::Deprecations; use crate::konst::{ConstAttributes, ConstSpec}; -use crate::method::{FnArg, FnSpec}; +use crate::method::{FnArg, FnSpec, PyArg, RegularArg}; use crate::pyimpl::{gen_py_const, PyClassMethodsType}; use crate::pymethod::{ impl_py_getter_def, impl_py_setter_def, MethodAndMethodDef, MethodAndSlotDef, PropertyType, @@ -1143,36 +1143,22 @@ fn complex_enum_struct_variant_new<'a>( let arg_py_type: syn::Type = parse_quote!(#pyo3_path::Python<'_>); let args = { - let mut no_pyo3_attrs = vec![]; - let attrs = crate::pyfunction::PyFunctionArgPyO3Attributes::from_attrs(&mut no_pyo3_attrs)?; - let mut args = vec![ // py: Python<'_> - FnArg { + FnArg::Py(PyArg { name: &arg_py_ident, ty: &arg_py_type, - optional: None, - default: None, - py: true, - attrs: attrs.clone(), - is_varargs: false, - is_kwargs: false, - is_cancel_handle: false, - }, + }), ]; for field in &variant.fields { - args.push(FnArg { + args.push(FnArg::Regular(RegularArg { name: field.ident, ty: field.ty, - optional: None, - default: None, - py: false, - attrs: attrs.clone(), - is_varargs: false, - is_kwargs: false, - is_cancel_handle: false, - }); + from_py_with: None, + default_value: None, + option_wrapped_type: None, + })); } args }; diff --git a/pyo3-macros-backend/src/pyfunction/signature.rs b/pyo3-macros-backend/src/pyfunction/signature.rs index baf01285658..3daa79c89f5 100644 --- a/pyo3-macros-backend/src/pyfunction/signature.rs +++ b/pyo3-macros-backend/src/pyfunction/signature.rs @@ -10,7 +10,7 @@ use syn::{ use crate::{ attributes::{kw, KeywordAttribute}, - method::FnArg, + method::{FnArg, RegularArg}, }; pub struct Signature { @@ -351,36 +351,39 @@ impl<'a> FunctionSignature<'a> { let mut next_non_py_argument_checked = |name: &syn::Ident| { for fn_arg in args_iter.by_ref() { - if fn_arg.py { - // If the user incorrectly tried to include py: Python in the - // signature, give a useful error as a hint. - ensure_spanned!( - name != fn_arg.name, - name.span() => "arguments of type `Python` must not be part of the signature" - ); - // Otherwise try next argument. - continue; - } - if fn_arg.is_cancel_handle { - // If the user incorrectly tried to include cancel: CoroutineCancel in the - // signature, give a useful error as a hint. - ensure_spanned!( - name != fn_arg.name, - name.span() => "`cancel_handle` argument must not be part of the signature" - ); - // Otherwise try next argument. - continue; + match fn_arg { + crate::method::FnArg::Py(..) => { + // If the user incorrectly tried to include py: Python in the + // signature, give a useful error as a hint. + ensure_spanned!( + name != fn_arg.name(), + name.span() => "arguments of type `Python` must not be part of the signature" + ); + // Otherwise try next argument. + continue; + } + crate::method::FnArg::CancelHandle(..) => { + // If the user incorrectly tried to include cancel: CoroutineCancel in the + // signature, give a useful error as a hint. + ensure_spanned!( + name != fn_arg.name(), + name.span() => "`cancel_handle` argument must not be part of the signature" + ); + // Otherwise try next argument. + continue; + } + _ => { + ensure_spanned!( + name == fn_arg.name(), + name.span() => format!( + "expected argument from function definition `{}` but got argument `{}`", + fn_arg.name().unraw(), + name.unraw(), + ) + ); + return Ok(fn_arg); + } } - - ensure_spanned!( - name == fn_arg.name, - name.span() => format!( - "expected argument from function definition `{}` but got argument `{}`", - fn_arg.name.unraw(), - name.unraw(), - ) - ); - return Ok(fn_arg); } bail_spanned!( name.span() => "signature entry does not have a corresponding function argument" @@ -398,7 +401,15 @@ impl<'a> FunctionSignature<'a> { arg.span(), )?; if let Some((_, default)) = &arg.eq_and_default { - fn_arg.default = Some(default.clone()); + if let FnArg::Regular(arg) = fn_arg { + arg.default_value = Some(default.clone()); + } else { + unreachable!( + "`Python` and `CancelHandle` are already handled above and `*args`/`**kwargs` are \ + parsed and transformed below. Because the have to come last and are only allowed \ + once, this has to be a regular argument." + ); + } } } SignatureItem::VarargsSep(sep) => { @@ -406,12 +417,12 @@ impl<'a> FunctionSignature<'a> { } SignatureItem::Varargs(varargs) => { let fn_arg = next_non_py_argument_checked(&varargs.ident)?; - fn_arg.is_varargs = true; + fn_arg.to_varargs_mut()?; parse_state.add_varargs(&mut python_signature, varargs)?; } SignatureItem::Kwargs(kwargs) => { let fn_arg = next_non_py_argument_checked(&kwargs.ident)?; - fn_arg.is_kwargs = true; + fn_arg.to_kwargs_mut()?; parse_state.add_kwargs(&mut python_signature, kwargs)?; } SignatureItem::PosargsSep(sep) => { @@ -421,9 +432,11 @@ impl<'a> FunctionSignature<'a> { } // Ensure no non-py arguments remain - if let Some(arg) = args_iter.find(|arg| !arg.py && !arg.is_cancel_handle) { + if let Some(arg) = + args_iter.find(|arg| !matches!(arg, FnArg::Py(..) | FnArg::CancelHandle(..))) + { bail_spanned!( - attribute.kw.span() => format!("missing signature entry for argument `{}`", arg.name) + attribute.kw.span() => format!("missing signature entry for argument `{}`", arg.name()) ); } @@ -439,15 +452,20 @@ impl<'a> FunctionSignature<'a> { let mut python_signature = PythonSignature::default(); for arg in &arguments { // Python<'_> arguments don't show in Python signature - if arg.py || arg.is_cancel_handle { + if matches!(arg, FnArg::Py(..) | FnArg::CancelHandle(..)) { continue; } - if arg.optional.is_none() { + if let FnArg::Regular(RegularArg { + ty, + option_wrapped_type: None, + .. + }) = arg + { // This argument is required, all previous arguments must also have been required ensure_spanned!( python_signature.required_positional_parameters == python_signature.positional_parameters.len(), - arg.ty.span() => "required arguments after an `Option<_>` argument are ambiguous\n\ + ty.span() => "required arguments after an `Option<_>` argument are ambiguous\n\ = help: add a `#[pyo3(signature)]` annotation on this function to unambiguously specify the default values for all optional parameters" ); @@ -457,7 +475,7 @@ impl<'a> FunctionSignature<'a> { python_signature .positional_parameters - .push(arg.name.unraw().to_string()); + .push(arg.name().unraw().to_string()); } Ok(Self { @@ -469,8 +487,12 @@ impl<'a> FunctionSignature<'a> { fn default_value_for_parameter(&self, parameter: &str) -> String { let mut default = "...".to_string(); - if let Some(fn_arg) = self.arguments.iter().find(|arg| arg.name == parameter) { - if let Some(arg_default) = fn_arg.default.as_ref() { + if let Some(fn_arg) = self.arguments.iter().find(|arg| arg.name() == parameter) { + if let FnArg::Regular(RegularArg { + default_value: Some(arg_default), + .. + }) = fn_arg + { match arg_default { // literal values syn::Expr::Lit(syn::ExprLit { lit, .. }) => match lit { @@ -496,7 +518,11 @@ impl<'a> FunctionSignature<'a> { // others, unsupported yet so defaults to `...` _ => {} } - } else if fn_arg.optional.is_some() { + } else if let FnArg::Regular(RegularArg { + option_wrapped_type: Some(..), + .. + }) = fn_arg + { // functions without a `#[pyo3(signature = (...))]` option // will treat trailing `Option` arguments as having a default of `None` default = "None".to_string(); diff --git a/pyo3-macros-backend/src/pymethod.rs b/pyo3-macros-backend/src/pymethod.rs index 7a4c54db9da..aac804316f8 100644 --- a/pyo3-macros-backend/src/pymethod.rs +++ b/pyo3-macros-backend/src/pymethod.rs @@ -1,8 +1,8 @@ use std::borrow::Cow; use crate::attributes::{NameAttribute, RenamingRule}; -use crate::method::{CallingConvention, ExtractErrorMode}; -use crate::params::{check_arg_for_gil_refs, impl_arg_param, Holders}; +use crate::method::{CallingConvention, ExtractErrorMode, PyArg}; +use crate::params::{check_arg_for_gil_refs, impl_regular_arg_param, Holders}; use crate::utils::Ctx; use crate::utils::PythonDoc; use crate::{ @@ -487,7 +487,7 @@ fn impl_py_class_attribute( let (py_arg, args) = split_off_python_arg(&spec.signature.arguments); ensure_spanned!( args.is_empty(), - args[0].ty.span() => "#[classattr] can only have one argument (of type pyo3::Python)" + args[0].ty().span() => "#[classattr] can only have one argument (of type pyo3::Python)" ); let name = &spec.name; @@ -537,7 +537,7 @@ fn impl_call_setter( bail_spanned!(spec.name.span() => "setter function expected to have one argument"); } else if args.len() > 1 { bail_spanned!( - args[1].ty.span() => + args[1].ty().span() => "setter function can have at most two arguments ([pyo3::Python,] and value)" ); } @@ -607,7 +607,7 @@ pub fn impl_py_setter_def( let (_, args) = split_off_python_arg(&spec.signature.arguments); let value_arg = &args[0]; let (from_py_with, ident) = if let Some(from_py_with) = - &value_arg.attrs.from_py_with.as_ref().map(|f| &f.value) + &value_arg.from_py_with().as_ref().map(|f| &f.value) { let ident = syn::Ident::new("from_py_with", from_py_with.span()); ( @@ -622,20 +622,21 @@ pub fn impl_py_setter_def( (quote!(), syn::Ident::new("dummy", Span::call_site())) }; - let extract = impl_arg_param( - &args[0], + let arg = if let FnArg::Regular(arg) = &value_arg { + arg + } else { + bail_spanned!(value_arg.name().span() => "The #[setter] value argument can't be *args, **kwargs or `cancel_handle`."); + }; + + let tokens = impl_regular_arg_param( + arg, ident, quote!(::std::option::Option::Some(_value.into())), &mut holders, ctx, - ) - .map(|tokens| { - check_arg_for_gil_refs( - tokens, - holders.push_gil_refs_checker(value_arg.ty.span()), - ctx, - ) - })?; + ); + let extract = + check_arg_for_gil_refs(tokens, holders.push_gil_refs_checker(arg.ty.span()), ctx); quote! { #from_py_with let _val = #extract; @@ -721,7 +722,7 @@ fn impl_call_getter( let slf = self_type.receiver(cls, ExtractErrorMode::Raise, holders, ctx); ensure_spanned!( args.is_empty(), - args[0].ty.span() => "getter function can only have one argument (of type pyo3::Python)" + args[0].ty().span() => "getter function can only have one argument (of type pyo3::Python)" ); let name = &spec.name; @@ -843,9 +844,9 @@ pub fn impl_py_getter_def( } /// Split an argument of pyo3::Python from the front of the arg list, if present -fn split_off_python_arg<'a>(args: &'a [FnArg<'a>]) -> (Option<&FnArg<'_>>, &[FnArg<'_>]) { +fn split_off_python_arg<'a>(args: &'a [FnArg<'a>]) -> (Option<&PyArg<'_>>, &[FnArg<'_>]) { match args { - [py, args @ ..] if utils::is_python(py.ty) => (Some(py), args), + [FnArg::Py(py), args @ ..] => (Some(py), args), args => (None, args), } } @@ -1052,14 +1053,14 @@ impl Ty { ctx: &Ctx, ) -> TokenStream { let Ctx { pyo3_path } = ctx; - let name_str = arg.name.unraw().to_string(); + let name_str = arg.name().unraw().to_string(); match self { Ty::Object => extract_object( extract_error_mode, holders, &name_str, quote! { #ident }, - arg.ty.span(), + arg.ty().span(), ctx ), Ty::MaybeNullObject => extract_object( @@ -1073,7 +1074,7 @@ impl Ty { #ident } }, - arg.ty.span(), + arg.ty().span(), ctx ), Ty::NonNullObject => extract_object( @@ -1081,7 +1082,7 @@ impl Ty { holders, &name_str, quote! { #ident.as_ptr() }, - arg.ty.span(), + arg.ty().span(), ctx ), Ty::IPowModulo => extract_object( @@ -1089,7 +1090,7 @@ impl Ty { holders, &name_str, quote! { #ident.as_ptr() }, - arg.ty.span(), + arg.ty().span(), ctx ), Ty::CompareOp => extract_error_mode.handle_error( @@ -1100,7 +1101,7 @@ impl Ty { ctx ), Ty::PySsizeT => { - let ty = arg.ty; + let ty = arg.ty(); extract_error_mode.handle_error( quote! { ::std::convert::TryInto::<#ty>::try_into(#ident).map_err(|e| #pyo3_path::exceptions::PyValueError::new_err(e.to_string())) @@ -1523,12 +1524,12 @@ fn extract_proto_arguments( let mut non_python_args = 0; for arg in &spec.signature.arguments { - if arg.py { + if let FnArg::Py(..) = arg { args.push(quote! { py }); } else { let ident = syn::Ident::new(&format!("arg{}", non_python_args), Span::call_site()); let conversions = proto_args.get(non_python_args) - .ok_or_else(|| err_spanned!(arg.ty.span() => format!("Expected at most {} non-python arguments", proto_args.len())))? + .ok_or_else(|| err_spanned!(arg.ty().span() => format!("Expected at most {} non-python arguments", proto_args.len())))? .extract(&ident, arg, extract_error_mode, holders, ctx); non_python_args += 1; args.push(conversions); diff --git a/tests/ui/invalid_cancel_handle.rs b/tests/ui/invalid_cancel_handle.rs index 59076b14418..cff6c5dcbad 100644 --- a/tests/ui/invalid_cancel_handle.rs +++ b/tests/ui/invalid_cancel_handle.rs @@ -19,4 +19,10 @@ async fn cancel_handle_wrong_type(#[pyo3(cancel_handle)] _param: String) {} #[pyfunction] async fn missing_cancel_handle_attribute(_param: pyo3::coroutine::CancelHandle) {} +#[pyfunction] +async fn cancel_handle_and_from_py_with( + #[pyo3(cancel_handle, from_py_with = "cancel_handle")] _param: pyo3::coroutine::CancelHandle, +) { +} + fn main() {} diff --git a/tests/ui/invalid_cancel_handle.stderr b/tests/ui/invalid_cancel_handle.stderr index ffd0b3fd0da..41a2c0854b7 100644 --- a/tests/ui/invalid_cancel_handle.stderr +++ b/tests/ui/invalid_cancel_handle.stderr @@ -16,6 +16,12 @@ error: `cancel_handle` attribute can only be used with `async fn` 14 | fn cancel_handle_synchronous(#[pyo3(cancel_handle)] _param: String) {} | ^^^^^^ +error: `from_py_with` and `cancel_handle` cannot be specified together + --> tests/ui/invalid_cancel_handle.rs:24:12 + | +24 | #[pyo3(cancel_handle, from_py_with = "cancel_handle")] _param: pyo3::coroutine::CancelHandle, + | ^^^^^^^^^^^^^ + error[E0308]: mismatched types --> tests/ui/invalid_cancel_handle.rs:16:1 | diff --git a/tests/ui/invalid_pyfunctions.rs b/tests/ui/invalid_pyfunctions.rs index eaa241c074b..1a95c9e4a34 100644 --- a/tests/ui/invalid_pyfunctions.rs +++ b/tests/ui/invalid_pyfunctions.rs @@ -1,5 +1,5 @@ use pyo3::prelude::*; -use pyo3::types::PyString; +use pyo3::types::{PyDict, PyString, PyTuple}; #[pyfunction] fn generic_function(value: T) {} @@ -16,6 +16,14 @@ fn destructured_argument((a, b): (i32, i32)) {} #[pyfunction] fn function_with_required_after_option(_opt: Option, _x: i32) {} +#[pyfunction] +#[pyo3(signature=(*args))] +fn function_with_optional_args(args: Option>) {} + +#[pyfunction] +#[pyo3(signature=(**kwargs))] +fn function_with_required_kwargs(kwargs: Bound<'_, PyDict>) {} + #[pyfunction(pass_module)] fn pass_module_but_no_arguments<'py>() {} diff --git a/tests/ui/invalid_pyfunctions.stderr b/tests/ui/invalid_pyfunctions.stderr index 299b20687cb..893d7cbec2c 100644 --- a/tests/ui/invalid_pyfunctions.stderr +++ b/tests/ui/invalid_pyfunctions.stderr @@ -29,16 +29,28 @@ error: required arguments after an `Option<_>` argument are ambiguous 17 | fn function_with_required_after_option(_opt: Option, _x: i32) {} | ^^^ +error: args cannot be optional + --> tests/ui/invalid_pyfunctions.rs:21:32 + | +21 | fn function_with_optional_args(args: Option>) {} + | ^^^^ + +error: kwargs must be Option<_> + --> tests/ui/invalid_pyfunctions.rs:25:34 + | +25 | fn function_with_required_kwargs(kwargs: Bound<'_, PyDict>) {} + | ^^^^^^ + error: expected `&PyModule` or `Py` as first argument with `pass_module` - --> tests/ui/invalid_pyfunctions.rs:20:37 + --> tests/ui/invalid_pyfunctions.rs:28:37 | -20 | fn pass_module_but_no_arguments<'py>() {} +28 | fn pass_module_but_no_arguments<'py>() {} | ^^ error[E0277]: the trait bound `&str: From>` is not satisfied - --> tests/ui/invalid_pyfunctions.rs:24:13 + --> tests/ui/invalid_pyfunctions.rs:32:13 | -24 | string: &str, +32 | string: &str, | ^ the trait `From>` is not implemented for `&str`, which is required by `BoundRef<'_, '_, pyo3::prelude::PyModule>: Into<_>` | = help: the following other types implement trait `From`: From 8ed5c17b9311c53d94c8fd21f2763fc602979779 Mon Sep 17 00:00:00 2001 From: David Matos Date: Sun, 14 Apr 2024 22:07:17 +0200 Subject: [PATCH 013/495] Allow use of scientific notation on rust decimal (#4079) * Allow use of scientific notation on rust decimal * Add newsfragment * Pull out var * Fix clippy warning * Modify let bindings location --- newsfragments/4079.added.md | 1 + src/conversions/rust_decimal.rs | 24 ++++++++++++++++++++++-- 2 files changed, 23 insertions(+), 2 deletions(-) create mode 100644 newsfragments/4079.added.md diff --git a/newsfragments/4079.added.md b/newsfragments/4079.added.md new file mode 100644 index 00000000000..afe26728f9a --- /dev/null +++ b/newsfragments/4079.added.md @@ -0,0 +1 @@ +Added support for scientific notation in `Decimal` conversion diff --git a/src/conversions/rust_decimal.rs b/src/conversions/rust_decimal.rs index b75cd875128..782ca2e80f0 100644 --- a/src/conversions/rust_decimal.rs +++ b/src/conversions/rust_decimal.rs @@ -64,8 +64,11 @@ impl FromPyObject<'_> for Decimal { if let Ok(val) = obj.extract() { Ok(Decimal::new(val, 0)) } else { - Decimal::from_str(&obj.str()?.to_cow()?) - .map_err(|e| PyValueError::new_err(e.to_string())) + let py_str = &obj.str()?; + let rs_str = &py_str.to_cow()?; + Decimal::from_str(rs_str).or_else(|_| { + Decimal::from_scientific(rs_str).map_err(|e| PyValueError::new_err(e.to_string())) + }) } } } @@ -194,6 +197,23 @@ mod test_rust_decimal { }) } + #[test] + fn test_scientific_notation() { + Python::with_gil(|py| { + let locals = PyDict::new_bound(py); + py.run_bound( + "import decimal\npy_dec = decimal.Decimal(\"1e3\")", + None, + Some(&locals), + ) + .unwrap(); + let py_dec = locals.get_item("py_dec").unwrap().unwrap(); + let roundtripped: Decimal = py_dec.extract().unwrap(); + let rs_dec = Decimal::from_scientific("1e3").unwrap(); + assert_eq!(rs_dec, roundtripped); + }) + } + #[test] fn test_infinity() { Python::with_gil(|py| { From a5201c04afc73605ac3ef467a42d3052af5feb14 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Sun, 14 Apr 2024 22:38:40 +0100 Subject: [PATCH 014/495] Deprecate the `PySet::empty` gil-ref constructor (#4082) * Deprecate the `PySet::empty` gil-ref constructor * add newsfragment --- newsfragments/4082.changed.md | 1 + src/types/set.rs | 9 ++++++++- 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 newsfragments/4082.changed.md diff --git a/newsfragments/4082.changed.md b/newsfragments/4082.changed.md new file mode 100644 index 00000000000..231ea0ae576 --- /dev/null +++ b/newsfragments/4082.changed.md @@ -0,0 +1 @@ +Deprecate the `PySet::empty()` gil-ref constructor. diff --git a/src/types/set.rs b/src/types/set.rs index 4f1fcf8499f..f648bc2be1f 100644 --- a/src/types/set.rs +++ b/src/types/set.rs @@ -58,7 +58,14 @@ impl PySet { } /// Deprecated form of [`PySet::empty_bound`]. - pub fn empty(py: Python<'_>) -> PyResult<&'_ PySet> { + #[cfg_attr( + not(feature = "gil-refs"), + deprecated( + since = "0.21.2", + note = "`PySet::empty` will be replaced by `PySet::empty_bound` in a future PyO3 version" + ) + )] + pub fn empty(py: Python<'_>) -> PyResult<&PySet> { Self::empty_bound(py).map(Bound::into_gil_ref) } From 2ad2a3f208a92d3a6e11909b621edd8d87b356e4 Mon Sep 17 00:00:00 2001 From: David Matos Date: Tue, 16 Apr 2024 10:17:41 +0200 Subject: [PATCH 015/495] docs: Make contributing.md slightly more clear for newer contributors (#4080) * docs: Make contributing.md slightly more clear for newer contributors * Remove accidental backticks Rearrange overview of commands * Placed on wrong line * Add extra overview command --- .github/pull_request_template.md | 2 +- Contributing.md | 22 +++++++++++++++++++--- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index b344525cabe..11375e966b3 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -8,6 +8,6 @@ Please consider adding the following to your pull request: - docs to all new functions and / or detail in the guide - tests for all new or changed functions -PyO3's CI pipeline will check your pull request. To run its tests +PyO3's CI pipeline will check your pull request, thus make sure you have checked the `Contributing.md` guidelines. To run most of its tests locally, you can run ```nox```. See ```nox --list-sessions``` for a list of supported actions. diff --git a/Contributing.md b/Contributing.md index 054099b431a..111e814ac8f 100644 --- a/Contributing.md +++ b/Contributing.md @@ -92,9 +92,7 @@ Here are a few things to note when you are writing PRs. ### Continuous Integration -The PyO3 repo uses GitHub Actions. PRs are blocked from merging if CI is not successful. - -Formatting, linting and tests are checked for all Rust and Python code. In addition, all warnings in Rust code are disallowed (using `RUSTFLAGS="-D warnings"`). +The PyO3 repo uses GitHub Actions. PRs are blocked from merging if CI is not successful. Formatting, linting and tests are checked for all Rust and Python code. In addition, all warnings in Rust code are disallowed (using `RUSTFLAGS="-D warnings"`). Tests run with all supported Python versions with the latest stable Rust compiler, as well as for Python 3.9 with the minimum supported Rust version. @@ -103,6 +101,24 @@ If you are adding a new feature, you should add it to the `full` feature in our You can run these tests yourself with `nox`. Use `nox -l` to list the full set of subcommands you can run. +#### Linting Python code +`nox -s ruff` + +#### Linting Rust code +`nox -s rustfmt` + +#### Semver checks +`cargo semver-checks check-release` + +#### Clippy +`nox -s clippy-all` + +#### Tests +`cargo test --features full` + +#### Check all conditional compilation +`nox -s check-feature-powerset` + #### UI Tests PyO3 uses [`trybuild`][trybuild] to develop UI tests to capture error messages from the Rust compiler for some of the macro functionality. From 03f59eaf45c5786e7a9c591b8f81b1243a50b5ef Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Tue, 16 Apr 2024 22:12:18 +0200 Subject: [PATCH 016/495] fix declarative module compile error with `create_exception!` (#4086) * fix declarative module compile error with `create_exception!` * add newsfragment --- newsfragments/4086.fixed.md | 1 + src/types/mod.rs | 2 +- tests/test_declarative_module.rs | 8 ++++++++ 3 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 newsfragments/4086.fixed.md diff --git a/newsfragments/4086.fixed.md b/newsfragments/4086.fixed.md new file mode 100644 index 00000000000..e9cae7733f9 --- /dev/null +++ b/newsfragments/4086.fixed.md @@ -0,0 +1 @@ +Fixes a compile error when exporting an exception created with `create_exception!` living in a different Rust module using the `declarative-module` feature. \ No newline at end of file diff --git a/src/types/mod.rs b/src/types/mod.rs index a03d01b301a..e7aead7fbe5 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -252,7 +252,7 @@ macro_rules! pyobject_native_type_info( impl $name { #[doc(hidden)] - const _PYO3_DEF: $crate::impl_::pymodule::AddTypeToModule = $crate::impl_::pymodule::AddTypeToModule::new(); + pub const _PYO3_DEF: $crate::impl_::pymodule::AddTypeToModule = $crate::impl_::pymodule::AddTypeToModule::new(); } }; ); diff --git a/tests/test_declarative_module.rs b/tests/test_declarative_module.rs index 8e432c3ae58..2e46f4a64d1 100644 --- a/tests/test_declarative_module.rs +++ b/tests/test_declarative_module.rs @@ -10,10 +10,14 @@ use pyo3::types::PyBool; mod common; mod some_module { + use pyo3::create_exception; + use pyo3::exceptions::PyException; use pyo3::prelude::*; #[pyclass] pub struct SomePyClass; + + create_exception!(some_module, SomeException, PyException); } #[pyclass] @@ -61,6 +65,10 @@ mod declarative_module { #[pymodule_export] use super::some_module::SomePyClass; + // test for #4036 + #[pymodule_export] + use super::some_module::SomeException; + #[pymodule] mod inner { use super::*; From 9761abf3a543b3e3abfa0c41613331103ad79e13 Mon Sep 17 00:00:00 2001 From: Bruno Kolenbrander <59372212+mejrs@users.noreply.github.com> Date: Wed, 17 Apr 2024 03:07:11 +0200 Subject: [PATCH 017/495] Specify higher target-lexicon version (#4087) --- pyo3-build-config/Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyo3-build-config/Cargo.toml b/pyo3-build-config/Cargo.toml index 1eb269c2132..a0942831340 100644 --- a/pyo3-build-config/Cargo.toml +++ b/pyo3-build-config/Cargo.toml @@ -13,11 +13,11 @@ edition = "2021" [dependencies] once_cell = "1" python3-dll-a = { version = "0.2.6", optional = true } -target-lexicon = "0.12" +target-lexicon = "0.12.14" [build-dependencies] python3-dll-a = { version = "0.2.6", optional = true } -target-lexicon = "0.12" +target-lexicon = "0.12.14" [features] default = [] From 03c50a18392cdb183aa059d65d74dbca9fdc3ec3 Mon Sep 17 00:00:00 2001 From: Jacob Zhong Date: Thu, 18 Apr 2024 15:33:07 +0800 Subject: [PATCH 018/495] Change the types of `PySliceIndices` and `PySlice::indices (#3761) * Change the type of `PySliceIndices::slicelength` and `PySlice::indices()` * Fix example * Fix fmt --- examples/getitem/src/lib.rs | 5 ++--- newsfragments/3761.changed.md | 1 + src/types/slice.rs | 23 +++++++++++++---------- 3 files changed, 16 insertions(+), 13 deletions(-) create mode 100644 newsfragments/3761.changed.md diff --git a/examples/getitem/src/lib.rs b/examples/getitem/src/lib.rs index c3c662ab92f..ce162a70bf9 100644 --- a/examples/getitem/src/lib.rs +++ b/examples/getitem/src/lib.rs @@ -2,7 +2,6 @@ use pyo3::exceptions::PyTypeError; use pyo3::prelude::*; use pyo3::types::PySlice; -use std::os::raw::c_long; #[derive(FromPyObject)] enum IntOrSlice<'py> { @@ -29,7 +28,7 @@ impl ExampleContainer { } else if let Ok(slice) = key.downcast::() { // METHOD 1 - the use PySliceIndices to help with bounds checking and for cases when only start or end are provided // in this case the start/stop/step all filled in to give valid values based on the max_length given - let index = slice.indices(self.max_length as c_long).unwrap(); + let index = slice.indices(self.max_length as isize).unwrap(); let _delta = index.stop - index.start; // METHOD 2 - Do the getattr manually really only needed if you have some special cases for stop/_step not being present @@ -62,7 +61,7 @@ impl ExampleContainer { fn __setitem__(&self, idx: IntOrSlice, value: u32) -> PyResult<()> { match idx { IntOrSlice::Slice(slice) => { - let index = slice.indices(self.max_length as c_long).unwrap(); + let index = slice.indices(self.max_length as isize).unwrap(); println!( "Got a slice! {}-{}, step: {}, value: {}", index.start, index.stop, index.step, value diff --git a/newsfragments/3761.changed.md b/newsfragments/3761.changed.md new file mode 100644 index 00000000000..fd0847211d8 --- /dev/null +++ b/newsfragments/3761.changed.md @@ -0,0 +1 @@ +Change the type of `PySliceIndices::slicelength` and the `length` parameter of `PySlice::indices()`. diff --git a/src/types/slice.rs b/src/types/slice.rs index 8e7545208dc..b6895d09e10 100644 --- a/src/types/slice.rs +++ b/src/types/slice.rs @@ -1,13 +1,12 @@ use crate::err::{PyErr, PyResult}; -use crate::ffi::{self, Py_ssize_t}; +use crate::ffi; use crate::ffi_ptr_ext::FfiPtrExt; use crate::types::any::PyAnyMethods; use crate::{Bound, PyAny, PyNativeType, PyObject, Python, ToPyObject}; -use std::os::raw::c_long; /// Represents a Python `slice`. /// -/// Only `c_long` indices supported at the moment by the `PySlice` object. +/// Only `isize` indices supported at the moment by the `PySlice` object. #[repr(transparent)] pub struct PySlice(PyAny); @@ -22,13 +21,17 @@ pyobject_native_type!( #[derive(Debug, Eq, PartialEq)] pub struct PySliceIndices { /// Start of the slice + /// + /// It can be -1 when the step is negative, otherwise it's non-negative. pub start: isize, /// End of the slice + /// + /// It can be -1 when the step is negative, otherwise it's non-negative. pub stop: isize, /// Increment to use when iterating the slice from `start` to `stop`. pub step: isize, /// The length of the slice calculated from the original input sequence. - pub slicelength: isize, + pub slicelength: usize, } impl PySliceIndices { @@ -94,7 +97,7 @@ impl PySlice { /// assuming a sequence of length `length`, and stores the length of the /// slice in its `slicelength` member. #[inline] - pub fn indices(&self, length: c_long) -> PyResult { + pub fn indices(&self, length: isize) -> PyResult { self.as_borrowed().indices(length) } } @@ -109,12 +112,11 @@ pub trait PySliceMethods<'py>: crate::sealed::Sealed { /// Retrieves the start, stop, and step indices from the slice object, /// assuming a sequence of length `length`, and stores the length of the /// slice in its `slicelength` member. - fn indices(&self, length: c_long) -> PyResult; + fn indices(&self, length: isize) -> PyResult; } impl<'py> PySliceMethods<'py> for Bound<'py, PySlice> { - fn indices(&self, length: c_long) -> PyResult { - // non-negative Py_ssize_t should always fit into Rust usize + fn indices(&self, length: isize) -> PyResult { unsafe { let mut slicelength: isize = 0; let mut start: isize = 0; @@ -122,7 +124,7 @@ impl<'py> PySliceMethods<'py> for Bound<'py, PySlice> { let mut step: isize = 0; let r = ffi::PySlice_GetIndicesEx( self.as_ptr(), - length as Py_ssize_t, + length, &mut start, &mut stop, &mut step, @@ -133,7 +135,8 @@ impl<'py> PySliceMethods<'py> for Bound<'py, PySlice> { start, stop, step, - slicelength, + // non-negative isize should always fit into usize + slicelength: slicelength as _, }) } else { Err(PyErr::fetch(self.py())) From e64eb7290338352e6344545bfc836d186f315c6e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 18 Apr 2024 08:58:51 +0000 Subject: [PATCH 019/495] build(deps): update chrono-tz requirement from >= 0.6, < 0.9 to >= 0.6, < 0.10 (#4061) * build(deps): update chrono-tz requirement from >= 0.6, < 0.9 to >= 0.6, < 0.10 Updates the requirements on [chrono-tz](https://github.com/chronotope/chrono-tz) to permit the latest version. - [Release notes](https://github.com/chronotope/chrono-tz/releases) - [Changelog](https://github.com/chronotope/chrono-tz/blob/main/CHANGELOG.md) - [Commits](https://github.com/chronotope/chrono-tz/compare/v0.6.0...v0.9.0) --- updated-dependencies: - dependency-name: chrono-tz dependency-type: direct:production ... Signed-off-by: dependabot[bot] * newsfragment --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: David Hewitt --- Cargo.toml | 4 ++-- newsfragments/4061.packaging.md | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 newsfragments/4061.packaging.md diff --git a/Cargo.toml b/Cargo.toml index fdd1fa9d29a..789d52c6356 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,7 +35,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 } -chrono-tz = { version = ">= 0.6, < 0.9", default-features = false, optional = true } +chrono-tz = { version = ">= 0.6, < 0.10", default-features = false, optional = true } either = { version = "1.9", optional = true } eyre = { version = ">= 0.4, < 0.7", optional = true } hashbrown = { version = ">= 0.9, < 0.15", optional = true } @@ -49,7 +49,7 @@ smallvec = { version = "1.0", optional = true } [dev-dependencies] assert_approx_eq = "1.1.0" chrono = "0.4.25" -chrono-tz = ">= 0.6, < 0.9" +chrono-tz = ">= 0.6, < 0.10" # Required for "and $N others" normalization trybuild = ">=1.0.70" proptest = { version = "1.0", default-features = false, features = ["std"] } diff --git a/newsfragments/4061.packaging.md b/newsfragments/4061.packaging.md new file mode 100644 index 00000000000..5e51f50290d --- /dev/null +++ b/newsfragments/4061.packaging.md @@ -0,0 +1 @@ +Extend range of supported versions of `chrono-tz` optional dependency to include version 0.10. From 2c205d4586bef8f839eb6a55eb91b905074ddce9 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Thu, 18 Apr 2024 09:59:02 +0100 Subject: [PATCH 020/495] release notes for 0.21.2 (#4091) --- CHANGELOG.md | 17 ++++++++++++++++- Cargo.toml | 8 ++++---- README.md | 4 ++-- 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/4035.fixed.md | 1 - newsfragments/4045.fixed.md | 1 - newsfragments/4054.fixed.md | 1 - newsfragments/4067.fixed.md | 1 - newsfragments/4073.fixed.md | 1 - newsfragments/4082.changed.md | 1 - pyo3-build-config/Cargo.toml | 2 +- pyo3-ffi/Cargo.toml | 4 ++-- pyo3-macros-backend/Cargo.toml | 4 ++-- pyo3-macros/Cargo.toml | 4 ++-- pyproject.toml | 2 +- 19 files changed, 35 insertions(+), 26 deletions(-) delete mode 100644 newsfragments/4035.fixed.md delete mode 100644 newsfragments/4045.fixed.md delete mode 100644 newsfragments/4054.fixed.md delete mode 100644 newsfragments/4067.fixed.md delete mode 100644 newsfragments/4073.fixed.md delete mode 100644 newsfragments/4082.changed.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f4ce218021..86055a9a80c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,20 @@ To see unreleased changes, please see the [CHANGELOG on the main branch guide](h +## [0.21.2] - 2024-04-16 + +### Changed + +- Deprecate the `PySet::empty()` gil-ref constructor. [#4082](https://github.com/PyO3/pyo3/pull/4082) + +### Fixed + +- Fix compile error for `async fn` in `#[pymethods]` with a `&self` receiver and more than one additional argument. [#4035](https://github.com/PyO3/pyo3/pull/4035) +- Improve error message for wrong receiver type in `__traverse__`. [#4045](https://github.com/PyO3/pyo3/pull/4045) +- Fix compile error when exporting a `#[pyclass]` living in a different Rust module using the `experimental-declarative-modules` feature. [#4054](https://github.com/PyO3/pyo3/pull/4054) +- Fix `missing_docs` lint triggering on documented `#[pymodule]` functions. [#4067](https://github.com/PyO3/pyo3/pull/4067) +- Fix undefined symbol errors for extension modules on AIX (by linking `libpython`). [#4073](https://github.com/PyO3/pyo3/pull/4073) + ## [0.21.1] - 2024-04-01 ### Added @@ -1731,7 +1745,8 @@ Yanked - Initial release -[Unreleased]: https://github.com/pyo3/pyo3/compare/v0.21.1...HEAD +[Unreleased]: https://github.com/pyo3/pyo3/compare/v0.21.2...HEAD +[0.21.2]: https://github.com/pyo3/pyo3/compare/v0.21.1...v0.21.2 [0.21.1]: https://github.com/pyo3/pyo3/compare/v0.21.0...v0.21.1 [0.21.0]: https://github.com/pyo3/pyo3/compare/v0.20.3...v0.21.0 [0.21.0-beta.0]: https://github.com/pyo3/pyo3/compare/v0.20.3...v0.21.0-beta.0 diff --git a/Cargo.toml b/Cargo.toml index 789d52c6356..90bcc03c80c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3" -version = "0.21.1" +version = "0.21.2" description = "Bindings to Python interpreter" authors = ["PyO3 Project and Contributors "] readme = "README.md" @@ -22,10 +22,10 @@ memoffset = "0.9" portable-atomic = "1.0" # ffi bindings to the python interpreter, split into a separate crate so they can be used independently -pyo3-ffi = { path = "pyo3-ffi", version = "=0.21.1" } +pyo3-ffi = { path = "pyo3-ffi", version = "=0.21.2" } # support crates for macros feature -pyo3-macros = { path = "pyo3-macros", version = "=0.21.1", optional = true } +pyo3-macros = { path = "pyo3-macros", version = "=0.21.2", optional = true } indoc = { version = "2.0.1", optional = true } unindent = { version = "0.2.1", optional = true } @@ -60,7 +60,7 @@ rayon = "1.6.1" futures = "0.3.28" [build-dependencies] -pyo3-build-config = { path = "pyo3-build-config", version = "=0.21.1", features = ["resolve-config"] } +pyo3-build-config = { path = "pyo3-build-config", version = "=0.21.2", features = ["resolve-config"] } [features] default = ["macros"] diff --git a/README.md b/README.md index 8e7e2f75bca..4da2447e8c4 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ name = "string_sum" crate-type = ["cdylib"] [dependencies] -pyo3 = { version = "0.21.1", features = ["extension-module"] } +pyo3 = { version = "0.21.2", 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.21.1" +version = "0.21.2" features = ["auto-initialize"] ``` diff --git a/examples/decorator/.template/pre-script.rhai b/examples/decorator/.template/pre-script.rhai index 1dc689ef6d4..37372854cd8 100644 --- a/examples/decorator/.template/pre-script.rhai +++ b/examples/decorator/.template/pre-script.rhai @@ -1,4 +1,4 @@ -variable::set("PYO3_VERSION", "0.21.1"); +variable::set("PYO3_VERSION", "0.21.2"); file::rename(".template/Cargo.toml", "Cargo.toml"); file::rename(".template/pyproject.toml", "pyproject.toml"); file::delete(".template"); diff --git a/examples/maturin-starter/.template/pre-script.rhai b/examples/maturin-starter/.template/pre-script.rhai index 1dc689ef6d4..37372854cd8 100644 --- a/examples/maturin-starter/.template/pre-script.rhai +++ b/examples/maturin-starter/.template/pre-script.rhai @@ -1,4 +1,4 @@ -variable::set("PYO3_VERSION", "0.21.1"); +variable::set("PYO3_VERSION", "0.21.2"); file::rename(".template/Cargo.toml", "Cargo.toml"); file::rename(".template/pyproject.toml", "pyproject.toml"); file::delete(".template"); diff --git a/examples/plugin/.template/pre-script.rhai b/examples/plugin/.template/pre-script.rhai index 78adb883f43..7c2f375fbfb 100644 --- a/examples/plugin/.template/pre-script.rhai +++ b/examples/plugin/.template/pre-script.rhai @@ -1,4 +1,4 @@ -variable::set("PYO3_VERSION", "0.21.1"); +variable::set("PYO3_VERSION", "0.21.2"); file::rename(".template/Cargo.toml", "Cargo.toml"); file::rename(".template/plugin_api/Cargo.toml", "plugin_api/Cargo.toml"); file::delete(".template"); diff --git a/examples/setuptools-rust-starter/.template/pre-script.rhai b/examples/setuptools-rust-starter/.template/pre-script.rhai index 212f62f76fe..dd2950665eb 100644 --- a/examples/setuptools-rust-starter/.template/pre-script.rhai +++ b/examples/setuptools-rust-starter/.template/pre-script.rhai @@ -1,4 +1,4 @@ -variable::set("PYO3_VERSION", "0.21.1"); +variable::set("PYO3_VERSION", "0.21.2"); file::rename(".template/Cargo.toml", "Cargo.toml"); file::rename(".template/setup.cfg", "setup.cfg"); file::delete(".template"); diff --git a/examples/word-count/.template/pre-script.rhai b/examples/word-count/.template/pre-script.rhai index 1dc689ef6d4..37372854cd8 100644 --- a/examples/word-count/.template/pre-script.rhai +++ b/examples/word-count/.template/pre-script.rhai @@ -1,4 +1,4 @@ -variable::set("PYO3_VERSION", "0.21.1"); +variable::set("PYO3_VERSION", "0.21.2"); file::rename(".template/Cargo.toml", "Cargo.toml"); file::rename(".template/pyproject.toml", "pyproject.toml"); file::delete(".template"); diff --git a/newsfragments/4035.fixed.md b/newsfragments/4035.fixed.md deleted file mode 100644 index 5425c5cbaf7..00000000000 --- a/newsfragments/4035.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Fix compile error for `async fn` in `#[pymethods]` with a `&self` receiver and more than one additional argument. diff --git a/newsfragments/4045.fixed.md b/newsfragments/4045.fixed.md deleted file mode 100644 index 6b2bbcfa01d..00000000000 --- a/newsfragments/4045.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Add better error message on wrong receiver extraction in `__traverse__`. diff --git a/newsfragments/4054.fixed.md b/newsfragments/4054.fixed.md deleted file mode 100644 index 4b8da92ca4d..00000000000 --- a/newsfragments/4054.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Fixes a compile error when exporting a `#[pyclass]` living in a different Rust module using the declarative-module feature. diff --git a/newsfragments/4067.fixed.md b/newsfragments/4067.fixed.md deleted file mode 100644 index 869b6addf15..00000000000 --- a/newsfragments/4067.fixed.md +++ /dev/null @@ -1 +0,0 @@ -fixes `missing_docs` lint to trigger on documented `#[pymodule]` functions \ No newline at end of file diff --git a/newsfragments/4073.fixed.md b/newsfragments/4073.fixed.md deleted file mode 100644 index 0f77647e42d..00000000000 --- a/newsfragments/4073.fixed.md +++ /dev/null @@ -1 +0,0 @@ -fixes undefined symbol errors when building extension module on AIX by linking `libpython` diff --git a/newsfragments/4082.changed.md b/newsfragments/4082.changed.md deleted file mode 100644 index 231ea0ae576..00000000000 --- a/newsfragments/4082.changed.md +++ /dev/null @@ -1 +0,0 @@ -Deprecate the `PySet::empty()` gil-ref constructor. diff --git a/pyo3-build-config/Cargo.toml b/pyo3-build-config/Cargo.toml index a0942831340..60bf9a13ef9 100644 --- a/pyo3-build-config/Cargo.toml +++ b/pyo3-build-config/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3-build-config" -version = "0.21.1" +version = "0.21.2" 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 64753976fab..8f7767254f1 100644 --- a/pyo3-ffi/Cargo.toml +++ b/pyo3-ffi/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3-ffi" -version = "0.21.1" +version = "0.21.2" description = "Python-API bindings for the PyO3 ecosystem" authors = ["PyO3 Project and Contributors "] keywords = ["pyo3", "python", "cpython", "ffi"] @@ -38,7 +38,7 @@ abi3-py312 = ["abi3", "pyo3-build-config/abi3-py312"] generate-import-lib = ["pyo3-build-config/python3-dll-a"] [build-dependencies] -pyo3-build-config = { path = "../pyo3-build-config", version = "=0.21.1", features = ["resolve-config"] } +pyo3-build-config = { path = "../pyo3-build-config", version = "=0.21.2", features = ["resolve-config"] } [lints] workspace = true diff --git a/pyo3-macros-backend/Cargo.toml b/pyo3-macros-backend/Cargo.toml index 6ca4eeade8c..f2675f2b75e 100644 --- a/pyo3-macros-backend/Cargo.toml +++ b/pyo3-macros-backend/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3-macros-backend" -version = "0.21.1" +version = "0.21.2" description = "Code generation for PyO3 package" authors = ["PyO3 Project and Contributors "] keywords = ["pyo3", "python", "cpython", "ffi"] @@ -16,7 +16,7 @@ edition = "2021" [dependencies] heck = "0.4" proc-macro2 = { version = "1", default-features = false } -pyo3-build-config = { path = "../pyo3-build-config", version = "=0.21.1", features = ["resolve-config"] } +pyo3-build-config = { path = "../pyo3-build-config", version = "=0.21.2", features = ["resolve-config"] } quote = { version = "1", default-features = false } [dependencies.syn] diff --git a/pyo3-macros/Cargo.toml b/pyo3-macros/Cargo.toml index 39a4b9198c6..690924c76a5 100644 --- a/pyo3-macros/Cargo.toml +++ b/pyo3-macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3-macros" -version = "0.21.1" +version = "0.21.2" description = "Proc macros for PyO3 package" authors = ["PyO3 Project and Contributors "] keywords = ["pyo3", "python", "cpython", "ffi"] @@ -22,7 +22,7 @@ experimental-declarative-modules = [] proc-macro2 = { version = "1", default-features = false } quote = "1" syn = { version = "2", features = ["full", "extra-traits"] } -pyo3-macros-backend = { path = "../pyo3-macros-backend", version = "=0.21.1" } +pyo3-macros-backend = { path = "../pyo3-macros-backend", version = "=0.21.2" } [lints] workspace = true diff --git a/pyproject.toml b/pyproject.toml index d474753ccd1..9a70116f301 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ [tool.towncrier] filename = "CHANGELOG.md" -version = "0.21.1" +version = "0.21.2" start_string = "\n" template = ".towncrier.template.md" title_format = "## [{version}] - {project_date}" From b11174e96d85de439141670aa1b65cc0f5f6bcd1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 18 Apr 2024 11:22:34 +0100 Subject: [PATCH 021/495] Update heck requirement from 0.4 to 0.5 (#3966) * Update heck requirement from 0.4 to 0.5 --- updated-dependencies: - dependency-name: heck dependency-type: direct:production ... Signed-off-by: dependabot[bot] * newsfragment --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: David Hewitt --- newsfragments/3966.packaging.md | 1 + pyo3-macros-backend/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 newsfragments/3966.packaging.md diff --git a/newsfragments/3966.packaging.md b/newsfragments/3966.packaging.md new file mode 100644 index 00000000000..81220bc4e85 --- /dev/null +++ b/newsfragments/3966.packaging.md @@ -0,0 +1 @@ +Update `heck` dependency to 0.5. diff --git a/pyo3-macros-backend/Cargo.toml b/pyo3-macros-backend/Cargo.toml index f2675f2b75e..c2ffd53b0fc 100644 --- a/pyo3-macros-backend/Cargo.toml +++ b/pyo3-macros-backend/Cargo.toml @@ -14,7 +14,7 @@ edition = "2021" # not to depend on proc-macro itself. # See https://github.com/PyO3/pyo3/pull/810 for more. [dependencies] -heck = "0.4" +heck = "0.5" proc-macro2 = { version = "1", default-features = false } pyo3-build-config = { path = "../pyo3-build-config", version = "=0.21.2", features = ["resolve-config"] } quote = { version = "1", default-features = false } From d42c00d21dbff0fdf688ba49ed3ab3a41aad2bd7 Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Fri, 19 Apr 2024 09:24:26 +0200 Subject: [PATCH 022/495] feature gate deprecated APIs for `PySet` (#4096) --- src/types/set.rs | 60 +++++++++++++++++++++++------------------------- 1 file changed, 29 insertions(+), 31 deletions(-) diff --git a/src/types/set.rs b/src/types/set.rs index f648bc2be1f..83938f3bf42 100644 --- a/src/types/set.rs +++ b/src/types/set.rs @@ -31,12 +31,10 @@ pyobject_native_type_core!( impl PySet { /// Deprecated form of [`PySet::new_bound`]. - #[cfg_attr( - not(feature = "gil-refs"), - deprecated( - since = "0.21.0", - note = "`PySet::new` will be replaced by `PySet::new_bound` in a future PyO3 version" - ) + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`PySet::new` will be replaced by `PySet::new_bound` in a future PyO3 version" )] #[inline] pub fn new<'a, 'p, T: ToPyObject + 'a>( @@ -58,12 +56,10 @@ impl PySet { } /// Deprecated form of [`PySet::empty_bound`]. - #[cfg_attr( - not(feature = "gil-refs"), - deprecated( - since = "0.21.2", - note = "`PySet::empty` will be replaced by `PySet::empty_bound` in a future PyO3 version" - ) + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.2", + note = "`PySet::empty` will be replaced by `PySet::empty_bound` in a future PyO3 version" )] pub fn empty(py: Python<'_>) -> PyResult<&PySet> { Self::empty_bound(py).map(Bound::into_gil_ref) @@ -396,27 +392,29 @@ pub(crate) fn new_from_iter( } #[cfg(test)] -#[cfg_attr(not(feature = "gil-refs"), allow(deprecated))] mod tests { use super::PySet; - use crate::{Python, ToPyObject}; + use crate::{ + types::{PyAnyMethods, PySetMethods}, + Python, ToPyObject, + }; use std::collections::HashSet; #[test] fn test_set_new() { Python::with_gil(|py| { - let set = PySet::new(py, &[1]).unwrap(); + let set = PySet::new_bound(py, &[1]).unwrap(); assert_eq!(1, set.len()); let v = vec![1]; - assert!(PySet::new(py, &[v]).is_err()); + assert!(PySet::new_bound(py, &[v]).is_err()); }); } #[test] fn test_set_empty() { Python::with_gil(|py| { - let set = PySet::empty(py).unwrap(); + let set = PySet::empty_bound(py).unwrap(); assert_eq!(0, set.len()); assert!(set.is_empty()); }); @@ -427,11 +425,11 @@ mod tests { Python::with_gil(|py| { let mut v = HashSet::new(); let ob = v.to_object(py); - let set: &PySet = ob.downcast(py).unwrap(); + let set = ob.downcast_bound::(py).unwrap(); assert_eq!(0, set.len()); v.insert(7); let ob = v.to_object(py); - let set2: &PySet = ob.downcast(py).unwrap(); + let set2 = ob.downcast_bound::(py).unwrap(); assert_eq!(1, set2.len()); }); } @@ -439,7 +437,7 @@ mod tests { #[test] fn test_set_clear() { Python::with_gil(|py| { - let set = PySet::new(py, &[1]).unwrap(); + let set = PySet::new_bound(py, &[1]).unwrap(); assert_eq!(1, set.len()); set.clear(); assert_eq!(0, set.len()); @@ -449,7 +447,7 @@ mod tests { #[test] fn test_set_contains() { Python::with_gil(|py| { - let set = PySet::new(py, &[1]).unwrap(); + let set = PySet::new_bound(py, &[1]).unwrap(); assert!(set.contains(1).unwrap()); }); } @@ -457,7 +455,7 @@ mod tests { #[test] fn test_set_discard() { Python::with_gil(|py| { - let set = PySet::new(py, &[1]).unwrap(); + let set = PySet::new_bound(py, &[1]).unwrap(); assert!(!set.discard(2).unwrap()); assert_eq!(1, set.len()); @@ -472,7 +470,7 @@ mod tests { #[test] fn test_set_add() { Python::with_gil(|py| { - let set = PySet::new(py, &[1, 2]).unwrap(); + let set = PySet::new_bound(py, &[1, 2]).unwrap(); set.add(1).unwrap(); // Add a dupliated element assert!(set.contains(1).unwrap()); }); @@ -481,13 +479,13 @@ mod tests { #[test] fn test_set_pop() { Python::with_gil(|py| { - let set = PySet::new(py, &[1]).unwrap(); + let set = PySet::new_bound(py, &[1]).unwrap(); let val = set.pop(); assert!(val.is_some()); let val2 = set.pop(); assert!(val2.is_none()); assert!(py - .eval("print('Exception state should not be set.')", None, None) + .eval_bound("print('Exception state should not be set.')", None, None) .is_ok()); }); } @@ -495,7 +493,7 @@ mod tests { #[test] fn test_set_iter() { Python::with_gil(|py| { - let set = PySet::new(py, &[1]).unwrap(); + let set = PySet::new_bound(py, &[1]).unwrap(); for el in set { assert_eq!(1i32, el.extract::<'_, i32>().unwrap()); @@ -520,9 +518,9 @@ mod tests { #[should_panic] fn test_set_iter_mutation() { Python::with_gil(|py| { - let set = PySet::new(py, &[1, 2, 3, 4, 5]).unwrap(); + let set = PySet::new_bound(py, &[1, 2, 3, 4, 5]).unwrap(); - for _ in set { + for _ in &set { let _ = set.add(42); } }); @@ -532,9 +530,9 @@ mod tests { #[should_panic] fn test_set_iter_mutation_same_len() { Python::with_gil(|py| { - let set = PySet::new(py, &[1, 2, 3, 4, 5]).unwrap(); + let set = PySet::new_bound(py, &[1, 2, 3, 4, 5]).unwrap(); - for item in set { + for item in &set { let item: i32 = item.extract().unwrap(); let _ = set.del_item(item); let _ = set.add(item + 10); @@ -545,7 +543,7 @@ mod tests { #[test] fn test_set_iter_size_hint() { Python::with_gil(|py| { - let set = PySet::new(py, &[1]).unwrap(); + let set = PySet::new_bound(py, &[1]).unwrap(); let mut iter = set.iter(); // Exact size From cd28e1408e9f4d2eee4832e0123b8771d11b4731 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Fri, 19 Apr 2024 12:44:36 +0100 Subject: [PATCH 023/495] add `#[track_caller]` to all `Py`/`Bound`/`Borrowed` methods which panic (#4098) --- newsfragments/4098.changed.md | 1 + src/err/mod.rs | 1 + src/ffi_ptr_ext.rs | 2 ++ src/instance.rs | 9 +++++++++ src/pycell.rs | 2 ++ 5 files changed, 15 insertions(+) create mode 100644 newsfragments/4098.changed.md diff --git a/newsfragments/4098.changed.md b/newsfragments/4098.changed.md new file mode 100644 index 00000000000..5df526a52e3 --- /dev/null +++ b/newsfragments/4098.changed.md @@ -0,0 +1 @@ +Add `#[track_caller]` to all `Py`, `Bound<'py, T>` and `Borrowed<'a, 'py, T>` methods which can panic. diff --git a/src/err/mod.rs b/src/err/mod.rs index 8dd16b26b47..a61c8c62d31 100644 --- a/src/err/mod.rs +++ b/src/err/mod.rs @@ -1091,6 +1091,7 @@ fn display_downcast_error( ) } +#[track_caller] pub fn panic_after_error(_py: Python<'_>) -> ! { unsafe { ffi::PyErr_Print(); diff --git a/src/ffi_ptr_ext.rs b/src/ffi_ptr_ext.rs index 3ca8671f1f6..183b0e3734e 100644 --- a/src/ffi_ptr_ext.rs +++ b/src/ffi_ptr_ext.rs @@ -39,6 +39,7 @@ impl FfiPtrExt for *mut ffi::PyObject { } #[inline] + #[track_caller] unsafe fn assume_owned(self, py: Python<'_>) -> Bound<'_, PyAny> { Bound::from_owned_ptr(py, self) } @@ -57,6 +58,7 @@ impl FfiPtrExt for *mut ffi::PyObject { } #[inline] + #[track_caller] unsafe fn assume_borrowed<'a>(self, py: Python<'_>) -> Borrowed<'a, '_, PyAny> { Borrowed::from_ptr(py, self) } diff --git a/src/instance.rs b/src/instance.rs index 88a550ffd30..b2715abe2b9 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -104,6 +104,7 @@ impl<'py> Bound<'py, PyAny> { /// - `ptr` must be a valid pointer to a Python object /// - `ptr` must be an owned Python reference, as the `Bound<'py, PyAny>` will assume ownership #[inline] + #[track_caller] pub unsafe fn from_owned_ptr(py: Python<'py>, ptr: *mut ffi::PyObject) -> Self { Self(py, ManuallyDrop::new(Py::from_owned_ptr(py, ptr))) } @@ -141,6 +142,7 @@ impl<'py> Bound<'py, PyAny> { /// /// - `ptr` must be a valid pointer to a Python object #[inline] + #[track_caller] pub unsafe fn from_borrowed_ptr(py: Python<'py>, ptr: *mut ffi::PyObject) -> Self { Self(py, ManuallyDrop::new(Py::from_borrowed_ptr(py, ptr))) } @@ -242,6 +244,7 @@ where /// Panics if the value is currently mutably borrowed. For a non-panicking variant, use /// [`try_borrow`](#method.try_borrow). #[inline] + #[track_caller] pub fn borrow(&self) -> PyRef<'py, T> { PyRef::borrow(self) } @@ -276,6 +279,7 @@ where /// Panics if the value is currently borrowed. For a non-panicking variant, use /// [`try_borrow_mut`](#method.try_borrow_mut). #[inline] + #[track_caller] pub fn borrow_mut(&self) -> PyRefMut<'py, T> where T: PyClass, @@ -573,6 +577,7 @@ impl<'a, 'py> Borrowed<'a, 'py, PyAny> { /// the caller and it is the caller's responsibility to ensure that the reference this is /// derived from is valid for the lifetime `'a`. #[inline] + #[track_caller] pub unsafe fn from_ptr(py: Python<'py>, ptr: *mut ffi::PyObject) -> Self { Self( NonNull::new(ptr).unwrap_or_else(|| crate::err::panic_after_error(py)), @@ -1138,6 +1143,7 @@ where /// Panics if the value is currently mutably borrowed. For a non-panicking variant, use /// [`try_borrow`](#method.try_borrow). #[inline] + #[track_caller] pub fn borrow<'py>(&'py self, py: Python<'py>) -> PyRef<'py, T> { self.bind(py).borrow() } @@ -1175,6 +1181,7 @@ where /// Panics if the value is currently borrowed. For a non-panicking variant, use /// [`try_borrow_mut`](#method.try_borrow_mut). #[inline] + #[track_caller] pub fn borrow_mut<'py>(&'py self, py: Python<'py>) -> PyRefMut<'py, T> where T: PyClass, @@ -1585,6 +1592,7 @@ impl Py { /// # Panics /// Panics if `ptr` is null. #[inline] + #[track_caller] pub unsafe fn from_owned_ptr(py: Python<'_>, ptr: *mut ffi::PyObject) -> Py { match NonNull::new(ptr) { Some(nonnull_ptr) => Py(nonnull_ptr, PhantomData), @@ -1628,6 +1636,7 @@ impl Py { /// # Panics /// Panics if `ptr` is null. #[inline] + #[track_caller] pub unsafe fn from_borrowed_ptr(py: Python<'_>, ptr: *mut ffi::PyObject) -> Py { match Self::from_borrowed_ptr_or_opt(py, ptr) { Some(slf) => slf, diff --git a/src/pycell.rs b/src/pycell.rs index a649ad02412..80ccff0a030 100644 --- a/src/pycell.rs +++ b/src/pycell.rs @@ -652,6 +652,7 @@ impl<'py, T: PyClass> PyRef<'py, T> { self.inner.clone().into_ptr() } + #[track_caller] pub(crate) fn borrow(obj: &Bound<'py, T>) -> Self { Self::try_borrow(obj).expect("Already mutably borrowed") } @@ -848,6 +849,7 @@ impl<'py, T: PyClass> PyRefMut<'py, T> { } #[inline] + #[track_caller] pub(crate) fn borrow(obj: &Bound<'py, T>) -> Self { Self::try_borrow(obj).expect("Already borrowed") } From 947b372dcc776f717e5a9862c87c85609a9e8d0d Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Fri, 19 Apr 2024 20:35:52 +0100 Subject: [PATCH 024/495] change `PyAnyMethods::dir` to be fallible (#4100) --- newsfragments/4100.changed.md | 1 + src/types/any.rs | 34 +++++++++++++++++++++++++++++----- 2 files changed, 30 insertions(+), 5 deletions(-) create mode 100644 newsfragments/4100.changed.md diff --git a/newsfragments/4100.changed.md b/newsfragments/4100.changed.md new file mode 100644 index 00000000000..13fd2e02aa8 --- /dev/null +++ b/newsfragments/4100.changed.md @@ -0,0 +1 @@ +Change `PyAnyMethods::dir` to be fallible and return `PyResult>` (and similar for `PyAny::dir`). diff --git a/src/types/any.rs b/src/types/any.rs index a5ea4c80d4c..6ba34c86bc6 100644 --- a/src/types/any.rs +++ b/src/types/any.rs @@ -845,8 +845,8 @@ impl PyAny { /// Returns the list of attributes of this object. /// /// This is equivalent to the Python expression `dir(self)`. - pub fn dir(&self) -> &PyList { - self.as_borrowed().dir().into_gil_ref() + pub fn dir(&self) -> PyResult<&PyList> { + self.as_borrowed().dir().map(Bound::into_gil_ref) } /// Checks whether this object is an instance of type `ty`. @@ -1674,7 +1674,7 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed { /// Returns the list of attributes of this object. /// /// This is equivalent to the Python expression `dir(self)`. - fn dir(&self) -> Bound<'py, PyList>; + fn dir(&self) -> PyResult>; /// Checks whether this object is an instance of type `ty`. /// @@ -2220,10 +2220,10 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { Ok(v as usize) } - fn dir(&self) -> Bound<'py, PyList> { + fn dir(&self) -> PyResult> { unsafe { ffi::PyObject_Dir(self.as_ptr()) - .assume_owned(self.py()) + .assume_owned_or_err(self.py()) .downcast_into_unchecked() } } @@ -2471,6 +2471,7 @@ class SimpleClass: .unwrap(); let a = obj .dir() + .unwrap() .into_iter() .map(|x| x.extract::().unwrap()); let b = dir.into_iter().map(|x| x.extract::().unwrap()); @@ -2745,4 +2746,27 @@ class SimpleClass: assert!(not_container.is_empty().is_err()); }); } + + #[cfg(feature = "macros")] + #[test] + #[allow(unknown_lints, non_local_definitions)] + fn test_fallible_dir() { + use crate::exceptions::PyValueError; + use crate::prelude::*; + + #[pyclass(crate = "crate")] + struct DirFail; + + #[pymethods(crate = "crate")] + impl DirFail { + fn __dir__(&self) -> PyResult { + Err(PyValueError::new_err("uh-oh!")) + } + } + + Python::with_gil(|py| { + let obj = Bound::new(py, DirFail).unwrap(); + assert!(obj.dir().unwrap_err().is_instance_of::(py)); + }) + } } From b0ad1e10aaf15a446635fd9bb8488079dc250624 Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Mon, 22 Apr 2024 09:19:01 +0200 Subject: [PATCH 025/495] feature gate deprecated APIs for `PyTuple` (#4107) --- src/types/tuple.rs | 85 +++++++++++++++++++++++++--------------------- 1 file changed, 47 insertions(+), 38 deletions(-) diff --git a/src/types/tuple.rs b/src/types/tuple.rs index 636a2f3e11f..563a81983fa 100644 --- a/src/types/tuple.rs +++ b/src/types/tuple.rs @@ -59,12 +59,10 @@ pyobject_native_type_core!(PyTuple, pyobject_native_static_type_object!(ffi::PyT impl PyTuple { /// Deprecated form of `PyTuple::new_bound`. #[track_caller] - #[cfg_attr( - not(feature = "gil-refs"), - deprecated( - since = "0.21.0", - note = "`PyTuple::new` will be replaced by `PyTuple::new_bound` in a future PyO3 version" - ) + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`PyTuple::new` will be replaced by `PyTuple::new_bound` in a future PyO3 version" )] pub fn new( py: Python<'_>, @@ -117,12 +115,10 @@ impl PyTuple { } /// Deprecated form of `PyTuple::empty_bound`. - #[cfg_attr( - not(feature = "gil-refs"), - deprecated( - since = "0.21.0", - note = "`PyTuple::empty` will be replaced by `PyTuple::empty_bound` in a future PyO3 version" - ) + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`PyTuple::empty` will be replaced by `PyTuple::empty_bound` in a future PyO3 version" )] pub fn empty(py: Python<'_>) -> &PyTuple { Self::empty_bound(py).into_gil_ref() @@ -832,24 +828,23 @@ tuple_conversion!( ); #[cfg(test)] -#[allow(deprecated)] // TODO: remove allow when GIL Pool is removed mod tests { - use crate::types::{any::PyAnyMethods, tuple::PyTupleMethods, PyAny, PyList, PyTuple}; + use crate::types::{any::PyAnyMethods, tuple::PyTupleMethods, PyList, PyTuple}; use crate::{Python, ToPyObject}; use std::collections::HashSet; #[test] fn test_new() { Python::with_gil(|py| { - let ob = PyTuple::new(py, [1, 2, 3]); + let ob = PyTuple::new_bound(py, [1, 2, 3]); assert_eq!(3, ob.len()); - let ob: &PyAny = ob.into(); + let ob = ob.as_any(); assert_eq!((1, 2, 3), ob.extract().unwrap()); let mut map = HashSet::new(); map.insert(1); map.insert(2); - PyTuple::new(py, map); + PyTuple::new_bound(py, map); }); } @@ -857,10 +852,10 @@ mod tests { fn test_len() { Python::with_gil(|py| { let ob = (1, 2, 3).to_object(py); - let tuple: &PyTuple = ob.downcast(py).unwrap(); + let tuple = ob.downcast_bound::(py).unwrap(); assert_eq!(3, tuple.len()); assert!(!tuple.is_empty()); - let ob: &PyAny = tuple.into(); + let ob = tuple.as_any(); assert_eq!((1, 2, 3), ob.extract().unwrap()); }); } @@ -868,7 +863,7 @@ mod tests { #[test] fn test_empty() { Python::with_gil(|py| { - let tuple = PyTuple::empty(py); + let tuple = PyTuple::empty_bound(py); assert!(tuple.is_empty()); assert_eq!(0, tuple.len()); }); @@ -877,7 +872,7 @@ mod tests { #[test] fn test_slice() { Python::with_gil(|py| { - let tup = PyTuple::new(py, [2, 3, 5, 7]); + let tup = PyTuple::new_bound(py, [2, 3, 5, 7]); let slice = tup.get_slice(1, 3); assert_eq!(2, slice.len()); let slice = tup.get_slice(1, 7); @@ -889,7 +884,7 @@ mod tests { fn test_iter() { Python::with_gil(|py| { let ob = (1, 2, 3).to_object(py); - let tuple: &PyTuple = ob.downcast(py).unwrap(); + let tuple = ob.downcast_bound::(py).unwrap(); assert_eq!(3, tuple.len()); let mut iter = tuple.iter(); @@ -913,7 +908,7 @@ mod tests { fn test_iter_rev() { Python::with_gil(|py| { let ob = (1, 2, 3).to_object(py); - let tuple: &PyTuple = ob.downcast(py).unwrap(); + let tuple = ob.downcast_bound::(py).unwrap(); assert_eq!(3, tuple.len()); let mut iter = tuple.iter().rev(); @@ -983,7 +978,7 @@ mod tests { fn test_into_iter() { Python::with_gil(|py| { let ob = (1, 2, 3).to_object(py); - let tuple: &PyTuple = ob.downcast(py).unwrap(); + let tuple = ob.downcast_bound::(py).unwrap(); assert_eq!(3, tuple.len()); for (i, item) in tuple.iter().enumerate() { @@ -1014,7 +1009,7 @@ mod tests { fn test_as_slice() { Python::with_gil(|py| { let ob = (1, 2, 3).to_object(py); - let tuple: &PyTuple = ob.downcast(py).unwrap(); + let tuple = ob.downcast_bound::(py).unwrap(); let slice = tuple.as_slice(); assert_eq!(3, slice.len()); @@ -1092,7 +1087,7 @@ mod tests { fn test_tuple_get_item_invalid_index() { Python::with_gil(|py| { let ob = (1, 2, 3).to_object(py); - let tuple: &PyTuple = ob.downcast(py).unwrap(); + let tuple = ob.downcast_bound::(py).unwrap(); let obj = tuple.get_item(5); assert!(obj.is_err()); assert_eq!( @@ -1106,7 +1101,7 @@ mod tests { fn test_tuple_get_item_sanity() { Python::with_gil(|py| { let ob = (1, 2, 3).to_object(py); - let tuple: &PyTuple = ob.downcast(py).unwrap(); + let tuple = ob.downcast_bound::(py).unwrap(); let obj = tuple.get_item(0); assert_eq!(obj.unwrap().extract::().unwrap(), 1); }); @@ -1117,13 +1112,15 @@ mod tests { fn test_tuple_get_item_unchecked_sanity() { Python::with_gil(|py| { let ob = (1, 2, 3).to_object(py); - let tuple: &PyTuple = ob.downcast(py).unwrap(); + let tuple = ob.downcast_bound::(py).unwrap(); let obj = unsafe { tuple.get_item_unchecked(0) }; assert_eq!(obj.extract::().unwrap(), 1); }); } #[test] + #[cfg(feature = "gil-refs")] + #[allow(deprecated)] fn test_tuple_index_trait() { Python::with_gil(|py| { let ob = (1, 2, 3).to_object(py); @@ -1136,6 +1133,8 @@ mod tests { #[test] #[should_panic] + #[cfg(feature = "gil-refs")] + #[allow(deprecated)] fn test_tuple_index_trait_panic() { Python::with_gil(|py| { let ob = (1, 2, 3).to_object(py); @@ -1145,6 +1144,8 @@ mod tests { } #[test] + #[cfg(feature = "gil-refs")] + #[allow(deprecated)] fn test_tuple_index_trait_ranges() { Python::with_gil(|py| { let ob = (1, 2, 3).to_object(py); @@ -1165,6 +1166,8 @@ mod tests { #[test] #[should_panic = "range start index 5 out of range for tuple of length 3"] + #[cfg(feature = "gil-refs")] + #[allow(deprecated)] fn test_tuple_index_trait_range_panic_start() { Python::with_gil(|py| { let ob = (1, 2, 3).to_object(py); @@ -1175,6 +1178,8 @@ mod tests { #[test] #[should_panic = "range end index 10 out of range for tuple of length 3"] + #[cfg(feature = "gil-refs")] + #[allow(deprecated)] fn test_tuple_index_trait_range_panic_end() { Python::with_gil(|py| { let ob = (1, 2, 3).to_object(py); @@ -1185,6 +1190,8 @@ mod tests { #[test] #[should_panic = "slice index starts at 2 but ends at 1"] + #[cfg(feature = "gil-refs")] + #[allow(deprecated)] fn test_tuple_index_trait_range_panic_wrong_order() { Python::with_gil(|py| { let ob = (1, 2, 3).to_object(py); @@ -1196,6 +1203,8 @@ mod tests { #[test] #[should_panic = "range start index 8 out of range for tuple of length 3"] + #[cfg(feature = "gil-refs")] + #[allow(deprecated)] fn test_tuple_index_trait_range_from_panic() { Python::with_gil(|py| { let ob = (1, 2, 3).to_object(py); @@ -1208,7 +1217,7 @@ mod tests { fn test_tuple_contains() { Python::with_gil(|py| { let ob = (1, 1, 2, 3, 5, 8).to_object(py); - let tuple: &PyTuple = ob.downcast(py).unwrap(); + let tuple = ob.downcast_bound::(py).unwrap(); assert_eq!(6, tuple.len()); let bad_needle = 7i32.to_object(py); @@ -1226,7 +1235,7 @@ mod tests { fn test_tuple_index() { Python::with_gil(|py| { let ob = (1, 1, 2, 3, 5, 8).to_object(py); - let tuple: &PyTuple = ob.downcast(py).unwrap(); + let tuple = ob.downcast_bound::(py).unwrap(); assert_eq!(0, tuple.index(1i32).unwrap()); assert_eq!(2, tuple.index(2i32).unwrap()); assert_eq!(3, tuple.index(3i32).unwrap()); @@ -1263,7 +1272,7 @@ mod tests { fn too_long_iterator() { Python::with_gil(|py| { let iter = FaultyIter(0..usize::MAX, 73); - let _tuple = PyTuple::new(py, iter); + let _tuple = PyTuple::new_bound(py, iter); }) } @@ -1274,7 +1283,7 @@ mod tests { fn too_short_iterator() { Python::with_gil(|py| { let iter = FaultyIter(0..35, 73); - let _tuple = PyTuple::new(py, iter); + let _tuple = PyTuple::new_bound(py, iter); }) } @@ -1286,14 +1295,14 @@ mod tests { Python::with_gil(|py| { let iter = FaultyIter(0..0, usize::MAX); - let _tuple = PyTuple::new(py, iter); + let _tuple = PyTuple::new_bound(py, iter); }) } #[cfg(feature = "macros")] #[test] fn bad_clone_mem_leaks() { - use crate::{IntoPy, Py}; + use crate::{IntoPy, Py, PyAny}; use std::sync::atomic::{AtomicUsize, Ordering::SeqCst}; static NEEDS_DESTRUCTING_COUNT: AtomicUsize = AtomicUsize::new(0); @@ -1346,7 +1355,7 @@ mod tests { Python::with_gil(|py| { std::panic::catch_unwind(|| { let iter = FaultyIter(0..50, 50); - let _tuple = PyTuple::new(py, iter); + let _tuple = PyTuple::new_bound(py, iter); }) .unwrap_err(); }); @@ -1361,7 +1370,7 @@ mod tests { #[cfg(feature = "macros")] #[test] fn bad_clone_mem_leaks_2() { - use crate::{IntoPy, Py}; + use crate::{IntoPy, Py, PyAny}; use std::sync::atomic::{AtomicUsize, Ordering::SeqCst}; static NEEDS_DESTRUCTING_COUNT: AtomicUsize = AtomicUsize::new(0); @@ -1412,9 +1421,9 @@ mod tests { #[test] fn test_tuple_to_list() { Python::with_gil(|py| { - let tuple = PyTuple::new(py, vec![1, 2, 3]); + let tuple = PyTuple::new_bound(py, vec![1, 2, 3]); let list = tuple.to_list(); - let list_expected = PyList::new(py, vec![1, 2, 3]); + let list_expected = PyList::new_bound(py, vec![1, 2, 3]); assert!(list.eq(list_expected).unwrap()); }) } From da2d1aa8b46636b62ea8fd07d7a3a412a3a28280 Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Mon, 22 Apr 2024 09:20:32 +0200 Subject: [PATCH 026/495] feature gate deprecated APIs for `PyString` (#4101) --- src/types/string.rs | 122 +++++++++++++++++++++++--------------------- 1 file changed, 63 insertions(+), 59 deletions(-) diff --git a/src/types/string.rs b/src/types/string.rs index 4bbc6fb86b0..4aa73341ae9 100644 --- a/src/types/string.rs +++ b/src/types/string.rs @@ -136,12 +136,10 @@ pyobject_native_type_core!(PyString, pyobject_native_static_type_object!(ffi::Py impl PyString { /// Deprecated form of [`PyString::new_bound`]. - #[cfg_attr( - not(feature = "gil-refs"), - deprecated( - since = "0.21.0", - note = "`PyString::new` will be replaced by `PyString::new_bound` in a future PyO3 version" - ) + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`PyString::new` will be replaced by `PyString::new_bound` in a future PyO3 version" )] pub fn new<'py>(py: Python<'py>, s: &str) -> &'py Self { Self::new_bound(py, s).into_gil_ref() @@ -161,12 +159,10 @@ impl PyString { } /// Deprecated form of [`PyString::intern_bound`]. - #[cfg_attr( - not(feature = "gil-refs"), - deprecated( - since = "0.21.0", - note = "`PyString::intern` will be replaced by `PyString::intern_bound` in a future PyO3 version" - ) + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`PyString::intern` will be replaced by `PyString::intern_bound` in a future PyO3 version" )] pub fn intern<'py>(py: Python<'py>, s: &str) -> &'py Self { Self::intern_bound(py, s).into_gil_ref() @@ -176,8 +172,8 @@ impl PyString { /// /// This will return a reference to the same Python string object if called repeatedly with the same string. /// - /// Note that while this is more memory efficient than [`PyString::new`], it unconditionally allocates a - /// temporary Python string object and is thereby slower than [`PyString::new`]. + /// Note that while this is more memory efficient than [`PyString::new_bound`], it unconditionally allocates a + /// temporary Python string object and is thereby slower than [`PyString::new_bound`]. /// /// Panics if out of memory. pub fn intern_bound<'py>(py: Python<'py>, s: &str) -> Bound<'py, PyString> { @@ -193,12 +189,10 @@ impl PyString { } /// Deprecated form of [`PyString::from_object_bound`]. - #[cfg_attr( - not(feature = "gil-refs"), - deprecated( - since = "0.21.0", - note = "`PyString::from_object` will be replaced by `PyString::from_object_bound` in a future PyO3 version" - ) + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`PyString::from_object` will be replaced by `PyString::from_object_bound` in a future PyO3 version" )] pub fn from_object<'py>(src: &'py PyAny, encoding: &str, errors: &str) -> PyResult<&'py Self> { Self::from_object_bound(&src.as_borrowed(), encoding, errors).map(Bound::into_gil_ref) @@ -502,37 +496,37 @@ impl IntoPy> for &'_ Py { } #[cfg(test)] -#[cfg_attr(not(feature = "gil-refs"), allow(deprecated))] mod tests { use super::*; use crate::{PyObject, ToPyObject}; #[test] - fn test_to_str_utf8() { + fn test_to_cow_utf8() { Python::with_gil(|py| { let s = "ascii 🐈"; - let obj: PyObject = PyString::new(py, s).into(); - let py_string: &PyString = obj.downcast(py).unwrap(); - assert_eq!(s, py_string.to_str().unwrap()); + let py_string = PyString::new_bound(py, s); + assert_eq!(s, py_string.to_cow().unwrap()); }) } #[test] - fn test_to_str_surrogate() { + fn test_to_cow_surrogate() { Python::with_gil(|py| { - let obj: PyObject = py.eval(r"'\ud800'", None, None).unwrap().into(); - let py_string: &PyString = obj.downcast(py).unwrap(); - assert!(py_string.to_str().is_err()); + let py_string = py + .eval_bound(r"'\ud800'", None, None) + .unwrap() + .downcast_into::() + .unwrap(); + assert!(py_string.to_cow().is_err()); }) } #[test] - fn test_to_str_unicode() { + fn test_to_cow_unicode() { Python::with_gil(|py| { let s = "哈哈🐈"; - let obj: PyObject = PyString::new(py, s).into(); - let py_string: &PyString = obj.downcast(py).unwrap(); - assert_eq!(s, py_string.to_str().unwrap()); + let py_string = PyString::new_bound(py, s); + assert_eq!(s, py_string.to_cow().unwrap()); }) } @@ -548,7 +542,7 @@ mod tests { #[test] fn test_encode_utf8_surrogate() { Python::with_gil(|py| { - let obj: PyObject = py.eval(r"'\ud800'", None, None).unwrap().into(); + let obj: PyObject = py.eval_bound(r"'\ud800'", None, None).unwrap().into(); assert!(obj .bind(py) .downcast::() @@ -561,11 +555,12 @@ mod tests { #[test] fn test_to_string_lossy() { Python::with_gil(|py| { - let obj: PyObject = py - .eval(r"'🐈 Hello \ud800World'", None, None) + let py_string = py + .eval_bound(r"'🐈 Hello \ud800World'", None, None) .unwrap() - .into(); - let py_string: &PyString = obj.downcast(py).unwrap(); + .downcast_into::() + .unwrap(); + assert_eq!(py_string.to_string_lossy(), "🐈 Hello ���World"); }) } @@ -574,7 +569,7 @@ mod tests { fn test_debug_string() { Python::with_gil(|py| { let v = "Hello\n".to_object(py); - let s: &PyString = v.downcast(py).unwrap(); + let s = v.downcast_bound::(py).unwrap(); assert_eq!(format!("{:?}", s), "'Hello\\n'"); }) } @@ -583,7 +578,7 @@ mod tests { fn test_display_string() { Python::with_gil(|py| { let v = "Hello\n".to_object(py); - let s: &PyString = v.downcast(py).unwrap(); + let s = v.downcast_bound::(py).unwrap(); assert_eq!(format!("{}", s), "Hello\n"); }) } @@ -592,7 +587,7 @@ mod tests { #[cfg(not(Py_LIMITED_API))] fn test_string_data_ucs1() { Python::with_gil(|py| { - let s = PyString::new(py, "hello, world"); + let s = PyString::new_bound(py, "hello, world"); let data = unsafe { s.data().unwrap() }; assert_eq!(data, PyStringData::Ucs1(b"hello, world")); @@ -615,11 +610,13 @@ mod tests { ) }; assert!(!ptr.is_null()); - let s: &PyString = unsafe { py.from_owned_ptr(ptr) }; + let s = unsafe { ptr.assume_owned(py).downcast_into_unchecked::() }; let data = unsafe { s.data().unwrap() }; assert_eq!(data, PyStringData::Ucs1(b"f\xfe")); let err = data.to_string(py).unwrap_err(); - assert!(err.get_type(py).is(py.get_type::())); + assert!(err + .get_type_bound(py) + .is(&py.get_type_bound::())); assert!(err .to_string() .contains("'utf-8' codec can't decode byte 0xfe in position 1")); @@ -631,7 +628,7 @@ mod tests { #[cfg(not(Py_LIMITED_API))] fn test_string_data_ucs2() { Python::with_gil(|py| { - let s = py.eval("'foo\\ud800'", None, None).unwrap(); + let s = py.eval_bound("'foo\\ud800'", None, None).unwrap(); let py_string = s.downcast::().unwrap(); let data = unsafe { py_string.data().unwrap() }; @@ -657,11 +654,13 @@ mod tests { ) }; assert!(!ptr.is_null()); - let s: &PyString = unsafe { py.from_owned_ptr(ptr) }; + let s = unsafe { ptr.assume_owned(py).downcast_into_unchecked::() }; let data = unsafe { s.data().unwrap() }; assert_eq!(data, PyStringData::Ucs2(&[0xff22, 0xd800])); let err = data.to_string(py).unwrap_err(); - assert!(err.get_type(py).is(py.get_type::())); + assert!(err + .get_type_bound(py) + .is(&py.get_type_bound::())); assert!(err .to_string() .contains("'utf-16' codec can't decode bytes in position 0-3")); @@ -674,7 +673,7 @@ mod tests { fn test_string_data_ucs4() { Python::with_gil(|py| { let s = "哈哈🐈"; - let py_string = PyString::new(py, s); + let py_string = PyString::new_bound(py, s); let data = unsafe { py_string.data().unwrap() }; assert_eq!(data, PyStringData::Ucs4(&[21704, 21704, 128008])); @@ -696,11 +695,13 @@ mod tests { ) }; assert!(!ptr.is_null()); - let s: &PyString = unsafe { py.from_owned_ptr(ptr) }; + let s = unsafe { ptr.assume_owned(py).downcast_into_unchecked::() }; let data = unsafe { s.data().unwrap() }; assert_eq!(data, PyStringData::Ucs4(&[0x20000, 0xd800])); let err = data.to_string(py).unwrap_err(); - assert!(err.get_type(py).is(py.get_type::())); + assert!(err + .get_type_bound(py) + .is(&py.get_type_bound::())); assert!(err .to_string() .contains("'utf-32' codec can't decode bytes in position 0-7")); @@ -711,16 +712,16 @@ mod tests { #[test] fn test_intern_string() { Python::with_gil(|py| { - let py_string1 = PyString::intern(py, "foo"); - assert_eq!(py_string1.to_str().unwrap(), "foo"); + let py_string1 = PyString::intern_bound(py, "foo"); + assert_eq!(py_string1.to_cow().unwrap(), "foo"); - let py_string2 = PyString::intern(py, "foo"); - assert_eq!(py_string2.to_str().unwrap(), "foo"); + let py_string2 = PyString::intern_bound(py, "foo"); + assert_eq!(py_string2.to_cow().unwrap(), "foo"); assert_eq!(py_string1.as_ptr(), py_string2.as_ptr()); - let py_string3 = PyString::intern(py, "bar"); - assert_eq!(py_string3.to_str().unwrap(), "bar"); + let py_string3 = PyString::intern_bound(py, "bar"); + assert_eq!(py_string3.to_cow().unwrap(), "bar"); assert_ne!(py_string1.as_ptr(), py_string3.as_ptr()); }); @@ -730,7 +731,7 @@ mod tests { fn test_py_to_str_utf8() { Python::with_gil(|py| { let s = "ascii 🐈"; - let py_string: Py = PyString::new(py, s).into_py(py); + let py_string: Py = PyString::new_bound(py, s).into_py(py); #[cfg(any(Py_3_10, not(Py_LIMITED_API)))] assert_eq!(s, py_string.to_str(py).unwrap()); @@ -742,8 +743,11 @@ mod tests { #[test] fn test_py_to_str_surrogate() { Python::with_gil(|py| { - let py_string: Py = - py.eval(r"'\ud800'", None, None).unwrap().extract().unwrap(); + let py_string: Py = py + .eval_bound(r"'\ud800'", None, None) + .unwrap() + .extract() + .unwrap(); #[cfg(any(Py_3_10, not(Py_LIMITED_API)))] assert!(py_string.to_str(py).is_err()); @@ -756,7 +760,7 @@ mod tests { fn test_py_to_string_lossy() { Python::with_gil(|py| { let py_string: Py = py - .eval(r"'🐈 Hello \ud800World'", None, None) + .eval_bound(r"'🐈 Hello \ud800World'", None, None) .unwrap() .extract() .unwrap(); From c2ac9a98e2316e42e59cf8b07f0e24ff244afbb7 Mon Sep 17 00:00:00 2001 From: Georg Brandl Date: Mon, 22 Apr 2024 10:56:39 +0200 Subject: [PATCH 027/495] fix vectorcall argument handling (#4104) Fixes #4093 - Make PY_VECTORCALL_ARGUMENTS_OFFSET a size_t like on CPython - Remove the assert in PyVectorcall_NARGS and use checked cast --- newsfragments/4104.fixed.md | 1 + pyo3-ffi/src/cpython/abstract_.rs | 16 +++++++--------- 2 files changed, 8 insertions(+), 9 deletions(-) create mode 100644 newsfragments/4104.fixed.md diff --git a/newsfragments/4104.fixed.md b/newsfragments/4104.fixed.md new file mode 100644 index 00000000000..3eff1654b4f --- /dev/null +++ b/newsfragments/4104.fixed.md @@ -0,0 +1 @@ +Changes definitions of `PY_VECTORCALL_ARGUMENTS_OFFSET` and `PyVectorcall_NARGS` to fix a false-positive assertion. diff --git a/pyo3-ffi/src/cpython/abstract_.rs b/pyo3-ffi/src/cpython/abstract_.rs index cf95f6711d4..ad28216cbff 100644 --- a/pyo3-ffi/src/cpython/abstract_.rs +++ b/pyo3-ffi/src/cpython/abstract_.rs @@ -4,8 +4,6 @@ use std::os::raw::{c_char, c_int}; #[cfg(not(Py_3_11))] use crate::Py_buffer; -#[cfg(Py_3_8)] -use crate::pyport::PY_SSIZE_T_MAX; #[cfg(all(Py_3_8, not(any(PyPy, GraalPy))))] use crate::{ vectorcallfunc, PyCallable_Check, PyThreadState, PyThreadState_GET, PyTuple_Check, @@ -42,14 +40,14 @@ extern "C" { } #[cfg(Py_3_8)] -const PY_VECTORCALL_ARGUMENTS_OFFSET: Py_ssize_t = - 1 << (8 * std::mem::size_of::() as Py_ssize_t - 1); +const PY_VECTORCALL_ARGUMENTS_OFFSET: size_t = + 1 << (8 * std::mem::size_of::() as size_t - 1); #[cfg(Py_3_8)] #[inline(always)] pub unsafe fn PyVectorcall_NARGS(n: size_t) -> Py_ssize_t { - assert!(n <= (PY_SSIZE_T_MAX as size_t)); - (n as Py_ssize_t) & !PY_VECTORCALL_ARGUMENTS_OFFSET + let n = n & !PY_VECTORCALL_ARGUMENTS_OFFSET; + n.try_into().expect("cannot fail due to mask") } #[cfg(all(Py_3_8, not(any(PyPy, GraalPy))))] @@ -184,7 +182,7 @@ pub unsafe fn PyObject_CallOneArg(func: *mut PyObject, arg: *mut PyObject) -> *m let args = args_array.as_ptr().offset(1); // For PY_VECTORCALL_ARGUMENTS_OFFSET let tstate = PyThreadState_GET(); let nargsf = 1 | PY_VECTORCALL_ARGUMENTS_OFFSET; - _PyObject_VectorcallTstate(tstate, func, args, nargsf as size_t, std::ptr::null_mut()) + _PyObject_VectorcallTstate(tstate, func, args, nargsf, std::ptr::null_mut()) } extern "C" { @@ -206,7 +204,7 @@ pub unsafe fn PyObject_CallMethodNoArgs( PyObject_VectorcallMethod( name, &self_, - 1 | PY_VECTORCALL_ARGUMENTS_OFFSET as size_t, + 1 | PY_VECTORCALL_ARGUMENTS_OFFSET, std::ptr::null_mut(), ) } @@ -223,7 +221,7 @@ pub unsafe fn PyObject_CallMethodOneArg( PyObject_VectorcallMethod( name, args.as_ptr(), - 2 | PY_VECTORCALL_ARGUMENTS_OFFSET as size_t, + 2 | PY_VECTORCALL_ARGUMENTS_OFFSET, std::ptr::null_mut(), ) } From 5d2f5b5702319150d41258de77f589119134ee74 Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Tue, 23 Apr 2024 07:48:27 +0200 Subject: [PATCH 028/495] feature gate deprecated APIs for `PyDict` (#4108) --- src/conversion.rs | 1 + src/instance.rs | 6 +-- src/types/dict.rs | 86 ++++++++++++++++++++----------------------- src/types/iterator.rs | 9 +++-- src/types/mapping.rs | 1 + 5 files changed, 49 insertions(+), 54 deletions(-) diff --git a/src/conversion.rs b/src/conversion.rs index dfa53eac83e..ced209abade 100644 --- a/src/conversion.rs +++ b/src/conversion.rs @@ -674,6 +674,7 @@ mod test_no_clone {} #[cfg(test)] mod tests { + #[cfg(feature = "gil-refs")] #[allow(deprecated)] mod deprecated { use super::super::PyTryFrom; diff --git a/src/instance.rs b/src/instance.rs index b2715abe2b9..02b3fc76241 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -2034,7 +2034,7 @@ mod tests { #[test] fn test_call_for_non_existing_method() { Python::with_gil(|py| { - let obj: PyObject = PyDict::new(py).into(); + let obj: PyObject = PyDict::new_bound(py).into(); assert!(obj.call_method0(py, "asdf").is_err()); assert!(obj .call_method(py, "nonexistent_method", (1,), None) @@ -2047,7 +2047,7 @@ mod tests { #[test] fn py_from_dict() { let dict: Py = Python::with_gil(|py| { - let native = PyDict::new(py); + let native = PyDict::new_bound(py); Py::from(native) }); @@ -2057,7 +2057,7 @@ mod tests { #[test] fn pyobject_from_py() { Python::with_gil(|py| { - let dict: Py = PyDict::new(py).into(); + let dict: Py = PyDict::new_bound(py).unbind(); let cnt = dict.get_refcnt(py); let p: PyObject = dict.into(); assert_eq!(p.get_refcnt(py), cnt); diff --git a/src/types/dict.rs b/src/types/dict.rs index a4ba72a59c0..64b3adcad44 100644 --- a/src/types/dict.rs +++ b/src/types/dict.rs @@ -57,12 +57,10 @@ pyobject_native_type_core!( impl PyDict { /// Deprecated form of [`new_bound`][PyDict::new_bound]. - #[cfg_attr( - not(feature = "gil-refs"), - deprecated( - since = "0.21.0", - note = "`PyDict::new` will be replaced by `PyDict::new_bound` in a future PyO3 version" - ) + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`PyDict::new` will be replaced by `PyDict::new_bound` in a future PyO3 version" )] #[inline] pub fn new(py: Python<'_>) -> &PyDict { @@ -75,12 +73,10 @@ impl PyDict { } /// Deprecated form of [`from_sequence_bound`][PyDict::from_sequence_bound]. - #[cfg_attr( - all(not(any(PyPy, GraalPy)), not(feature = "gil-refs")), - deprecated( - since = "0.21.0", - note = "`PyDict::from_sequence` will be replaced by `PyDict::from_sequence_bound` in a future PyO3 version" - ) + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`PyDict::from_sequence` will be replaced by `PyDict::from_sequence_bound` in a future PyO3 version" )] #[inline] #[cfg(not(any(PyPy, GraalPy)))] @@ -751,12 +747,10 @@ pub(crate) use borrowed_iter::BorrowedDictIter; pub trait IntoPyDict: Sized { /// Converts self into a `PyDict` object pointer. Whether pointer owned or borrowed /// depends on implementation. - #[cfg_attr( - not(feature = "gil-refs"), - deprecated( - since = "0.21.0", - note = "`IntoPyDict::into_py_dict` will be replaced by `IntoPyDict::into_py_dict_bound` in a future PyO3 version" - ) + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`IntoPyDict::into_py_dict` will be replaced by `IntoPyDict::into_py_dict_bound` in a future PyO3 version" )] fn into_py_dict(self, py: Python<'_>) -> &PyDict { Self::into_py_dict_bound(self, py).into_gil_ref() @@ -821,7 +815,6 @@ where } #[cfg(test)] -#[cfg_attr(not(feature = "gil-refs"), allow(deprecated))] mod tests { use super::*; #[cfg(not(any(PyPy, GraalPy)))] @@ -853,8 +846,8 @@ mod tests { #[cfg(not(any(PyPy, GraalPy)))] fn test_from_sequence() { Python::with_gil(|py| { - let items = PyList::new(py, &vec![("a", 1), ("b", 2)]); - let dict = PyDict::from_sequence(items).unwrap(); + let items = PyList::new_bound(py, &vec![("a", 1), ("b", 2)]); + let dict = PyDict::from_sequence_bound(&items).unwrap(); assert_eq!( 1, dict.get_item("a") @@ -884,8 +877,8 @@ mod tests { #[cfg(not(any(PyPy, GraalPy)))] fn test_from_sequence_err() { Python::with_gil(|py| { - let items = PyList::new(py, &vec!["a", "b"]); - assert!(PyDict::from_sequence(items).is_err()); + let items = PyList::new_bound(py, &vec!["a", "b"]); + assert!(PyDict::from_sequence_bound(&items).is_err()); }); } @@ -913,11 +906,11 @@ mod tests { Python::with_gil(|py| { let mut v = HashMap::new(); let ob = v.to_object(py); - let dict: &PyDict = ob.downcast(py).unwrap(); + let dict = ob.downcast_bound::(py).unwrap(); assert_eq!(0, dict.len()); v.insert(7, 32); let ob = v.to_object(py); - let dict2: &PyDict = ob.downcast(py).unwrap(); + let dict2 = ob.downcast_bound::(py).unwrap(); assert_eq!(1, dict2.len()); }); } @@ -928,7 +921,7 @@ mod tests { let mut v = HashMap::new(); v.insert(7, 32); let ob = v.to_object(py); - let dict: &PyDict = ob.downcast(py).unwrap(); + let dict = ob.downcast_bound::(py).unwrap(); assert!(dict.contains(7i32).unwrap()); assert!(!dict.contains(8i32).unwrap()); }); @@ -940,7 +933,7 @@ mod tests { let mut v = HashMap::new(); v.insert(7, 32); let ob = v.to_object(py); - let dict: &PyDict = ob.downcast(py).unwrap(); + let dict = ob.downcast_bound::(py).unwrap(); assert_eq!( 32, dict.get_item(7i32) @@ -961,7 +954,7 @@ mod tests { let mut v = HashMap::new(); v.insert(7, 32); let ob = v.to_object(py); - let dict: &PyDict = ob.downcast(py).unwrap(); + let dict = ob.downcast::(py).unwrap(); assert_eq!( 32, dict.get_item_with_error(7i32) @@ -984,7 +977,7 @@ mod tests { let mut v = HashMap::new(); v.insert(7, 32); let ob = v.to_object(py); - let dict: &PyDict = ob.downcast(py).unwrap(); + let dict = ob.downcast_bound::(py).unwrap(); assert!(dict.set_item(7i32, 42i32).is_ok()); // change assert!(dict.set_item(8i32, 123i32).is_ok()); // insert assert_eq!( @@ -1010,11 +1003,10 @@ mod tests { fn test_set_item_refcnt() { Python::with_gil(|py| { let cnt; - let obj = py.eval("object()", None, None).unwrap(); + let obj = py.eval_bound("object()", None, None).unwrap(); { - let _pool = unsafe { crate::GILPool::new() }; cnt = obj.get_refcnt(); - let _dict = [(10, obj)].into_py_dict_bound(py); + let _dict = [(10, &obj)].into_py_dict_bound(py); } { assert_eq!(cnt, obj.get_refcnt()); @@ -1028,7 +1020,7 @@ mod tests { let mut v = HashMap::new(); v.insert(7, 32); let ob = v.to_object(py); - let dict: &PyDict = ob.downcast(py).unwrap(); + let dict = ob.downcast_bound::(py).unwrap(); assert!(dict.set_item(7i32, 42i32).is_ok()); // change assert!(dict.set_item(8i32, 123i32).is_ok()); // insert assert_eq!(32i32, v[&7i32]); // not updated! @@ -1042,7 +1034,7 @@ mod tests { let mut v = HashMap::new(); v.insert(7, 32); let ob = v.to_object(py); - let dict: &PyDict = ob.downcast(py).unwrap(); + let dict = ob.downcast_bound::(py).unwrap(); assert!(dict.del_item(7i32).is_ok()); assert_eq!(0, dict.len()); assert!(dict.get_item(7i32).unwrap().is_none()); @@ -1055,7 +1047,7 @@ mod tests { let mut v = HashMap::new(); v.insert(7, 32); let ob = v.to_object(py); - let dict: &PyDict = ob.downcast(py).unwrap(); + let dict = ob.downcast_bound::(py).unwrap(); assert!(dict.del_item(7i32).is_ok()); // change assert_eq!(32i32, *v.get(&7i32).unwrap()); // not updated! }); @@ -1069,7 +1061,7 @@ mod tests { v.insert(8, 42); v.insert(9, 123); let ob = v.to_object(py); - let dict: &PyDict = ob.downcast(py).unwrap(); + let dict = ob.downcast_bound::(py).unwrap(); // Can't just compare against a vector of tuples since we don't have a guaranteed ordering. let mut key_sum = 0; let mut value_sum = 0; @@ -1091,7 +1083,7 @@ mod tests { v.insert(8, 42); v.insert(9, 123); let ob = v.to_object(py); - let dict: &PyDict = ob.downcast(py).unwrap(); + let dict = ob.downcast_bound::(py).unwrap(); // Can't just compare against a vector of tuples since we don't have a guaranteed ordering. let mut key_sum = 0; for el in dict.keys() { @@ -1109,7 +1101,7 @@ mod tests { v.insert(8, 42); v.insert(9, 123); let ob = v.to_object(py); - let dict: &PyDict = ob.downcast(py).unwrap(); + let dict = ob.downcast_bound::(py).unwrap(); // Can't just compare against a vector of tuples since we don't have a guaranteed ordering. let mut values_sum = 0; for el in dict.values() { @@ -1127,7 +1119,7 @@ mod tests { v.insert(8, 42); v.insert(9, 123); let ob = v.to_object(py); - let dict: &PyDict = ob.downcast(py).unwrap(); + let dict = ob.downcast_bound::(py).unwrap(); let mut key_sum = 0; let mut value_sum = 0; for (key, value) in dict { @@ -1168,7 +1160,7 @@ mod tests { v.insert(9, 123); let ob = v.to_object(py); - let dict: &PyDict = ob.downcast(py).unwrap(); + let dict = ob.downcast_bound::(py).unwrap(); for (key, value) in dict { dict.set_item(key, value.extract::().unwrap() + 7) @@ -1186,7 +1178,7 @@ mod tests { v.insert(i * 2, i * 2); } let ob = v.to_object(py); - let dict: &PyDict = ob.downcast(py).unwrap(); + let dict = ob.downcast_bound::(py).unwrap(); for (i, (key, value)) in dict.iter().enumerate() { let key = key.extract::().unwrap(); @@ -1211,7 +1203,7 @@ mod tests { v.insert(i * 2, i * 2); } let ob = v.to_object(py); - let dict: &PyDict = ob.downcast(py).unwrap(); + let dict = ob.downcast_bound::(py).unwrap(); for (i, (key, value)) in dict.iter().enumerate() { let key = key.extract::().unwrap(); @@ -1235,7 +1227,7 @@ mod tests { v.insert(8, 42); v.insert(9, 123); let ob = v.to_object(py); - let dict: &PyDict = ob.downcast(py).unwrap(); + let dict = ob.downcast_bound::(py).unwrap(); let mut iter = dict.iter(); assert_eq!(iter.size_hint(), (v.len(), Some(v.len()))); @@ -1261,7 +1253,7 @@ mod tests { v.insert(8, 42); v.insert(9, 123); let ob = v.to_object(py); - let dict: &PyDict = ob.downcast(py).unwrap(); + let dict = ob.downcast_bound::(py).unwrap(); let mut key_sum = 0; let mut value_sum = 0; for (key, value) in dict { @@ -1404,7 +1396,7 @@ mod tests { let dict = abc_dict(py); let keys = dict.call_method0("keys").unwrap(); assert!(keys - .is_instance(&py.get_type::().as_borrowed()) + .is_instance(&py.get_type_bound::().as_borrowed()) .unwrap()); }) } @@ -1416,7 +1408,7 @@ mod tests { let dict = abc_dict(py); let values = dict.call_method0("values").unwrap(); assert!(values - .is_instance(&py.get_type::().as_borrowed()) + .is_instance(&py.get_type_bound::().as_borrowed()) .unwrap()); }) } @@ -1428,7 +1420,7 @@ mod tests { let dict = abc_dict(py); let items = dict.call_method0("items").unwrap(); assert!(items - .is_instance(&py.get_type::().as_borrowed()) + .is_instance(&py.get_type_bound::().as_borrowed()) .unwrap()); }) } diff --git a/src/types/iterator.rs b/src/types/iterator.rs index bd01fe81a78..60f36f22de2 100644 --- a/src/types/iterator.rs +++ b/src/types/iterator.rs @@ -158,7 +158,7 @@ mod tests { use super::PyIterator; use crate::exceptions::PyTypeError; use crate::gil::GILPool; - use crate::types::{PyDict, PyList}; + use crate::types::{PyAnyMethods, PyDict, PyList}; use crate::{Py, PyAny, Python, ToPyObject}; #[test] @@ -244,10 +244,11 @@ def fibonacci(target): "#; Python::with_gil(|py| { - let context = PyDict::new(py); - py.run(fibonacci_generator, None, Some(context)).unwrap(); + let context = PyDict::new_bound(py); + py.run_bound(fibonacci_generator, None, Some(&context)) + .unwrap(); - let generator = py.eval("fibonacci(5)", None, Some(context)).unwrap(); + let generator = py.eval_bound("fibonacci(5)", None, Some(&context)).unwrap(); for (actual, expected) in generator.iter().unwrap().zip(&[1, 1, 2, 3, 5]) { let actual = actual.unwrap().extract::().unwrap(); assert_eq!(actual, *expected) diff --git a/src/types/mapping.rs b/src/types/mapping.rs index a5a93163bbd..a91dad3679f 100644 --- a/src/types/mapping.rs +++ b/src/types/mapping.rs @@ -435,6 +435,7 @@ mod tests { } #[test] + #[cfg(feature = "gil-refs")] #[allow(deprecated)] fn test_mapping_try_from() { use crate::PyTryFrom; From f5fee94afcaf11edceceed45192aabd71aeb9415 Mon Sep 17 00:00:00 2001 From: Bruno Kolenbrander <59372212+mejrs@users.noreply.github.com> Date: Tue, 23 Apr 2024 20:01:41 +0200 Subject: [PATCH 029/495] Scope macro imports more tightly (#4088) --- pyo3-macros-backend/src/module.rs | 11 ++++++----- pytests/src/enums.rs | 4 +++- tests/test_no_imports.rs | 5 ++++- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/pyo3-macros-backend/src/module.rs b/pyo3-macros-backend/src/module.rs index 3153279a2b8..626cde121e6 100644 --- a/pyo3-macros-backend/src/module.rs +++ b/pyo3-macros-backend/src/module.rs @@ -382,10 +382,7 @@ fn module_initialization(options: PyModuleOptions, ident: &syn::Ident) -> TokenS fn process_functions_in_module(options: &PyModuleOptions, func: &mut syn::ItemFn) -> Result<()> { let ctx = &Ctx::new(&options.krate); let Ctx { pyo3_path } = ctx; - let mut stmts: Vec = vec![syn::parse_quote!( - #[allow(unknown_lints, unused_imports, redundant_imports)] - use #pyo3_path::{PyNativeType, types::PyModuleMethods}; - )]; + let mut stmts: Vec = Vec::new(); for mut stmt in func.block.stmts.drain(..) { if let syn::Stmt::Item(Item::Fn(func)) = &mut stmt { @@ -395,7 +392,11 @@ fn process_functions_in_module(options: &PyModuleOptions, func: &mut syn::ItemFn let name = &func.sig.ident; let statements: Vec = syn::parse_quote! { #wrapped_function - #module_name.as_borrowed().add_function(#pyo3_path::wrap_pyfunction!(#name, #module_name.as_borrowed())?)?; + { + #[allow(unknown_lints, unused_imports, redundant_imports)] + use #pyo3_path::{PyNativeType, types::PyModuleMethods}; + #module_name.as_borrowed().add_function(#pyo3_path::wrap_pyfunction!(#name, #module_name.as_borrowed())?)?; + } }; stmts.extend(statements); } diff --git a/pytests/src/enums.rs b/pytests/src/enums.rs index 4bb269fbbd2..0a1bc49bb63 100644 --- a/pytests/src/enums.rs +++ b/pytests/src/enums.rs @@ -1,5 +1,7 @@ use pyo3::{ - pyclass, pyfunction, pymodule, types::PyModule, wrap_pyfunction_bound, Bound, PyResult, + pyclass, pyfunction, pymodule, + types::{PyModule, PyModuleMethods}, + wrap_pyfunction_bound, Bound, PyResult, }; #[pymodule] diff --git a/tests/test_no_imports.rs b/tests/test_no_imports.rs index 35c978b0da5..022d61e084d 100644 --- a/tests/test_no_imports.rs +++ b/tests/test_no_imports.rs @@ -30,7 +30,10 @@ fn basic_module_bound(m: &pyo3::Bound<'_, pyo3::types::PyModule>) -> pyo3::PyRes 42 } - m.add_function(pyo3::wrap_pyfunction_bound!(basic_function, m)?)?; + pyo3::types::PyModuleMethods::add_function( + m, + pyo3::wrap_pyfunction_bound!(basic_function, m)?, + )?; Ok(()) } From 013a4476ead64247257c1292a61201e8ee18adfc Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Wed, 24 Apr 2024 19:33:53 +0200 Subject: [PATCH 030/495] feature gate deprecated APIs for `datetime` types (#4111) --- src/types/datetime.rs | 145 ++++++++++++++++++------------------------ src/types/mod.rs | 2 +- 2 files changed, 64 insertions(+), 83 deletions(-) diff --git a/src/types/datetime.rs b/src/types/datetime.rs index e1620a6f647..12db67ab961 100644 --- a/src/types/datetime.rs +++ b/src/types/datetime.rs @@ -173,12 +173,10 @@ pub trait PyTimeAccess { /// Trait for accessing the components of a struct containing a tzinfo. pub trait PyTzInfoAccess<'py> { /// Deprecated form of `get_tzinfo_bound`. - #[cfg_attr( - not(feature = "gil-refs"), - deprecated( - since = "0.21.0", - note = "`get_tzinfo` will be replaced by `get_tzinfo_bound` in a future PyO3 version" - ) + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`get_tzinfo` will be replaced by `get_tzinfo_bound` in a future PyO3 version" )] fn get_tzinfo(&self) -> Option<&'py PyTzInfo> { self.get_tzinfo_bound().map(Bound::into_gil_ref) @@ -205,12 +203,10 @@ pyobject_native_type!( impl PyDate { /// Deprecated form of [`PyDate::new_bound`]. - #[cfg_attr( - not(feature = "gil-refs"), - deprecated( - since = "0.21.0", - note = "`PyDate::new` will be replaced by `PyDate::new_bound` in a future PyO3 version" - ) + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`PyDate::new` will be replaced by `PyDate::new_bound` in a future PyO3 version" )] pub fn new(py: Python<'_>, year: i32, month: u8, day: u8) -> PyResult<&PyDate> { Self::new_bound(py, year, month, day).map(Bound::into_gil_ref) @@ -227,12 +223,10 @@ impl PyDate { } /// Deprecated form of [`PyDate::from_timestamp_bound`]. - #[cfg_attr( - not(feature = "gil-refs"), - deprecated( - since = "0.21.0", - note = "`PyDate::from_timestamp` will be replaced by `PyDate::from_timestamp_bound` in a future PyO3 version" - ) + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`PyDate::from_timestamp` will be replaced by `PyDate::from_timestamp_bound` in a future PyO3 version" )] pub fn from_timestamp(py: Python<'_>, timestamp: i64) -> PyResult<&PyDate> { Self::from_timestamp_bound(py, timestamp).map(Bound::into_gil_ref) @@ -296,12 +290,10 @@ pyobject_native_type!( impl PyDateTime { /// Deprecated form of [`PyDateTime::new_bound`]. - #[cfg_attr( - not(feature = "gil-refs"), - deprecated( - since = "0.21.0", - note = "`PyDateTime::new` will be replaced by `PyDateTime::new_bound` in a future PyO3 version" - ) + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`PyDateTime::new` will be replaced by `PyDateTime::new_bound` in a future PyO3 version" )] #[allow(clippy::too_many_arguments)] pub fn new<'py>( @@ -361,12 +353,10 @@ impl PyDateTime { } /// Deprecated form of [`PyDateTime::new_bound_with_fold`]. - #[cfg_attr( - not(feature = "gil-refs"), - deprecated( - since = "0.21.0", - note = "`PyDateTime::new_with_fold` will be replaced by `PyDateTime::new_bound_with_fold` in a future PyO3 version" - ) + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`PyDateTime::new_with_fold` will be replaced by `PyDateTime::new_bound_with_fold` in a future PyO3 version" )] #[allow(clippy::too_many_arguments)] pub fn new_with_fold<'py>( @@ -436,12 +426,10 @@ impl PyDateTime { } /// Deprecated form of [`PyDateTime::from_timestamp_bound`]. - #[cfg_attr( - not(feature = "gil-refs"), - deprecated( - since = "0.21.0", - note = "`PyDateTime::from_timestamp` will be replaced by `PyDateTime::from_timestamp_bound` in a future PyO3 version" - ) + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`PyDateTime::from_timestamp` will be replaced by `PyDateTime::from_timestamp_bound` in a future PyO3 version" )] pub fn from_timestamp<'py>( py: Python<'py>, @@ -598,12 +586,10 @@ pyobject_native_type!( impl PyTime { /// Deprecated form of [`PyTime::new_bound`]. - #[cfg_attr( - not(feature = "gil-refs"), - deprecated( - since = "0.21.0", - note = "`PyTime::new` will be replaced by `PyTime::new_bound` in a future PyO3 version" - ) + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`PyTime::new` will be replaced by `PyTime::new_bound` in a future PyO3 version" )] pub fn new<'py>( py: Python<'py>, @@ -649,12 +635,10 @@ impl PyTime { } /// Deprecated form of [`PyTime::new_bound_with_fold`]. - #[cfg_attr( - not(feature = "gil-refs"), - deprecated( - since = "0.21.0", - note = "`PyTime::new_with_fold` will be replaced by `PyTime::new_bound_with_fold` in a future PyO3 version" - ) + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`PyTime::new_with_fold` will be replaced by `PyTime::new_bound_with_fold` in a future PyO3 version" )] pub fn new_with_fold<'py>( py: Python<'py>, @@ -677,7 +661,7 @@ impl PyTime { .map(Bound::into_gil_ref) } - /// Alternate constructor that takes a `fold` argument. See [`PyDateTime::new_with_fold`]. + /// Alternate constructor that takes a `fold` argument. See [`PyDateTime::new_bound_with_fold`]. pub fn new_bound_with_fold<'py>( py: Python<'py>, hour: u8, @@ -791,7 +775,7 @@ impl<'py> PyTzInfoAccess<'py> for Bound<'py, PyTime> { /// Bindings for `datetime.tzinfo`. /// /// This is an abstract base class and cannot be constructed directly. -/// For concrete time zone implementations, see [`timezone_utc`] and +/// For concrete time zone implementations, see [`timezone_utc_bound`] and /// the [`zoneinfo` module](https://docs.python.org/3/library/zoneinfo.html). #[repr(transparent)] pub struct PyTzInfo(PyAny); @@ -804,12 +788,10 @@ pyobject_native_type!( ); /// Deprecated form of [`timezone_utc_bound`]. -#[cfg_attr( - not(feature = "gil-refs"), - deprecated( - since = "0.21.0", - note = "`timezone_utc` will be replaced by `timezone_utc_bound` in a future PyO3 version" - ) +#[cfg(feature = "gil-refs")] +#[deprecated( + since = "0.21.0", + note = "`timezone_utc` will be replaced by `timezone_utc_bound` in a future PyO3 version" )] pub fn timezone_utc(py: Python<'_>) -> &PyTzInfo { timezone_utc_bound(py).into_gil_ref() @@ -858,12 +840,10 @@ pyobject_native_type!( impl PyDelta { /// Deprecated form of [`PyDelta::new_bound`]. - #[cfg_attr( - not(feature = "gil-refs"), - deprecated( - since = "0.21.0", - note = "`PyDelta::new` will be replaced by `PyDelta::new_bound` in a future PyO3 version" - ) + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`PyDelta::new` will be replaced by `PyDelta::new_bound` in a future PyO3 version" )] pub fn new( py: Python<'_>, @@ -936,7 +916,6 @@ fn opt_to_pyobj(opt: Option<&Bound<'_, PyTzInfo>>) -> *mut ffi::PyObject { } #[cfg(test)] -#[cfg_attr(not(feature = "gil-refs"), allow(deprecated))] mod tests { use super::*; #[cfg(feature = "macros")] @@ -947,14 +926,15 @@ mod tests { #[cfg_attr(target_arch = "wasm32", ignore)] // DateTime import fails on wasm for mysterious reasons fn test_datetime_fromtimestamp() { Python::with_gil(|py| { - let dt = PyDateTime::from_timestamp(py, 100.0, None).unwrap(); + let dt = PyDateTime::from_timestamp_bound(py, 100.0, None).unwrap(); py_run!( py, dt, "import datetime; assert dt == datetime.datetime.fromtimestamp(100)" ); - let dt = PyDateTime::from_timestamp(py, 100.0, Some(timezone_utc(py))).unwrap(); + let dt = + PyDateTime::from_timestamp_bound(py, 100.0, Some(&timezone_utc_bound(py))).unwrap(); py_run!( py, dt, @@ -968,7 +948,7 @@ mod tests { #[cfg_attr(target_arch = "wasm32", ignore)] // DateTime import fails on wasm for mysterious reasons fn test_date_fromtimestamp() { Python::with_gil(|py| { - let dt = PyDate::from_timestamp(py, 100).unwrap(); + let dt = PyDate::from_timestamp_bound(py, 100).unwrap(); py_run!( py, dt, @@ -981,8 +961,10 @@ mod tests { #[cfg_attr(target_arch = "wasm32", ignore)] // DateTime import fails on wasm for mysterious reasons fn test_new_with_fold() { Python::with_gil(|py| { - let a = PyDateTime::new_with_fold(py, 2021, 1, 23, 20, 32, 40, 341516, None, false); - let b = PyDateTime::new_with_fold(py, 2021, 1, 23, 20, 32, 40, 341516, None, true); + let a = + PyDateTime::new_bound_with_fold(py, 2021, 1, 23, 20, 32, 40, 341516, None, false); + let b = + PyDateTime::new_bound_with_fold(py, 2021, 1, 23, 20, 32, 40, 341516, None, true); assert!(!a.unwrap().get_fold()); assert!(b.unwrap().get_fold()); @@ -991,26 +973,25 @@ mod tests { #[test] #[cfg_attr(target_arch = "wasm32", ignore)] // DateTime import fails on wasm for mysterious reasons - #[cfg_attr(not(feature = "gil-refs"), allow(deprecated))] fn test_get_tzinfo() { crate::Python::with_gil(|py| { - let utc = timezone_utc(py); + let utc = timezone_utc_bound(py); - let dt = PyDateTime::new(py, 2018, 1, 1, 0, 0, 0, 0, Some(utc)).unwrap(); + let dt = PyDateTime::new_bound(py, 2018, 1, 1, 0, 0, 0, 0, Some(&utc)).unwrap(); - assert!(dt.get_tzinfo().unwrap().eq(utc).unwrap()); + assert!(dt.get_tzinfo_bound().unwrap().eq(&utc).unwrap()); - let dt = PyDateTime::new(py, 2018, 1, 1, 0, 0, 0, 0, None).unwrap(); + let dt = PyDateTime::new_bound(py, 2018, 1, 1, 0, 0, 0, 0, None).unwrap(); - assert!(dt.get_tzinfo().is_none()); + assert!(dt.get_tzinfo_bound().is_none()); - let t = PyTime::new(py, 0, 0, 0, 0, Some(utc)).unwrap(); + let t = PyTime::new_bound(py, 0, 0, 0, 0, Some(&utc)).unwrap(); - assert!(t.get_tzinfo().unwrap().eq(utc).unwrap()); + assert!(t.get_tzinfo_bound().unwrap().eq(utc).unwrap()); - let t = PyTime::new(py, 0, 0, 0, 0, None).unwrap(); + let t = PyTime::new_bound(py, 0, 0, 0, 0, None).unwrap(); - assert!(t.get_tzinfo().is_none()); + assert!(t.get_tzinfo_bound().is_none()); }); } @@ -1024,9 +1005,9 @@ mod tests { .unwrap() .call_method1("utcoffset", ((),)) .unwrap() - .extract::<&PyDelta>() + .downcast_into::() .unwrap() - .eq(PyDelta::new(py, 0, -3600, 0, true).unwrap()) + .eq(PyDelta::new_bound(py, 0, -3600, 0, true).unwrap()) .unwrap() ); @@ -1035,9 +1016,9 @@ mod tests { .unwrap() .call_method1("utcoffset", ((),)) .unwrap() - .extract::<&PyDelta>() + .downcast_into::() .unwrap() - .eq(PyDelta::new(py, 0, 3600, 0, true).unwrap()) + .eq(PyDelta::new_bound(py, 0, 3600, 0, true).unwrap()) .unwrap() ); diff --git a/src/types/mod.rs b/src/types/mod.rs index e7aead7fbe5..d127cbbca20 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -9,7 +9,7 @@ pub use self::capsule::{PyCapsule, PyCapsuleMethods}; pub use self::code::PyCode; pub use self::complex::{PyComplex, PyComplexMethods}; #[allow(deprecated)] -#[cfg(not(Py_LIMITED_API))] +#[cfg(all(not(Py_LIMITED_API), feature = "gil-refs"))] pub use self::datetime::timezone_utc; #[cfg(not(Py_LIMITED_API))] pub use self::datetime::{ From c951bf86de4c32c1e4d026687d6b54f975aea802 Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Wed, 24 Apr 2024 19:34:19 +0200 Subject: [PATCH 031/495] feature gate deprecated APIs for `PyCapsule` (#4112) --- src/types/capsule.rs | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/src/types/capsule.rs b/src/types/capsule.rs index fe5a47e1796..c2b73a0d0f7 100644 --- a/src/types/capsule.rs +++ b/src/types/capsule.rs @@ -47,12 +47,10 @@ pyobject_native_type_core!(PyCapsule, pyobject_native_static_type_object!(ffi::P impl PyCapsule { /// Deprecated form of [`PyCapsule::new_bound`]. - #[cfg_attr( - not(feature = "gil-refs"), - deprecated( - since = "0.21.0", - note = "`PyCapsule::new` will be replaced by `PyCapsule::new_bound` in a future PyO3 version" - ) + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`PyCapsule::new` will be replaced by `PyCapsule::new_bound` in a future PyO3 version" )] pub fn new( py: Python<'_>, @@ -102,12 +100,10 @@ impl PyCapsule { } /// Deprecated form of [`PyCapsule::new_bound_with_destructor`]. - #[cfg_attr( - not(feature = "gil-refs"), - deprecated( - since = "0.21.0", - note = "`PyCapsule::new_with_destructor` will be replaced by `PyCapsule::new_bound_with_destructor` in a future PyO3 version" - ) + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`PyCapsule::new_with_destructor` will be replaced by `PyCapsule::new_bound_with_destructor` in a future PyO3 version" )] pub fn new_with_destructor< T: 'static + Send + AssertNotZeroSized, @@ -441,7 +437,6 @@ fn name_ptr_ignore_error(slf: &Bound<'_, PyCapsule>) -> *const c_char { } #[cfg(test)] -#[cfg_attr(not(feature = "gil-refs"), allow(deprecated))] mod tests { use libc::c_void; From 6c2e6f8bcceb2d954ca8c645777bce3d6e77de88 Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Wed, 24 Apr 2024 23:13:27 +0200 Subject: [PATCH 032/495] feature gate deprecated APIs for `PyFrozenSet` (#4118) --- src/types/frozenset.rs | 43 ++++++++++++++++++------------------------ 1 file changed, 18 insertions(+), 25 deletions(-) diff --git a/src/types/frozenset.rs b/src/types/frozenset.rs index 36d5578f701..1fbbba44615 100644 --- a/src/types/frozenset.rs +++ b/src/types/frozenset.rs @@ -39,12 +39,10 @@ impl<'py> PyFrozenSetBuilder<'py> { } /// Deprecated form of [`PyFrozenSetBuilder::finalize_bound`] - #[cfg_attr( - not(feature = "gil-refs"), - deprecated( - since = "0.21.0", - note = "`PyFrozenSetBuilder::finalize` will be replaced by `PyFrozenSetBuilder::finalize_bound` in a future PyO3 version" - ) + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`PyFrozenSetBuilder::finalize` will be replaced by `PyFrozenSetBuilder::finalize_bound` in a future PyO3 version" )] pub fn finalize(self) -> &'py PyFrozenSet { self.finalize_bound().into_gil_ref() @@ -78,12 +76,10 @@ pyobject_native_type_core!( impl PyFrozenSet { /// Deprecated form of [`PyFrozenSet::new_bound`]. #[inline] - #[cfg_attr( - not(feature = "gil-refs"), - deprecated( - since = "0.21.0", - note = "`PyFrozenSet::new` will be replaced by `PyFrozenSet::new_bound` in a future PyO3 version" - ) + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`PyFrozenSet::new` will be replaced by `PyFrozenSet::new_bound` in a future PyO3 version" )] pub fn new<'a, 'p, T: ToPyObject + 'a>( py: Python<'p>, @@ -104,12 +100,10 @@ impl PyFrozenSet { } /// Deprecated form of [`PyFrozenSet::empty_bound`]. - #[cfg_attr( - not(feature = "gil-refs"), - deprecated( - since = "0.21.0", - note = "`PyFrozenSet::empty` will be replaced by `PyFrozenSet::empty_bound` in a future PyO3 version" - ) + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`PyFrozenSet::empty` will be replaced by `PyFrozenSet::empty_bound` in a future PyO3 version" )] pub fn empty(py: Python<'_>) -> PyResult<&'_ PyFrozenSet> { Self::empty_bound(py).map(Bound::into_gil_ref) @@ -324,25 +318,24 @@ pub(crate) fn new_from_iter( } #[cfg(test)] -#[cfg_attr(not(feature = "gil-refs"), allow(deprecated))] mod tests { use super::*; #[test] fn test_frozenset_new_and_len() { Python::with_gil(|py| { - let set = PyFrozenSet::new(py, &[1]).unwrap(); + let set = PyFrozenSet::new_bound(py, &[1]).unwrap(); assert_eq!(1, set.len()); let v = vec![1]; - assert!(PyFrozenSet::new(py, &[v]).is_err()); + assert!(PyFrozenSet::new_bound(py, &[v]).is_err()); }); } #[test] fn test_frozenset_empty() { Python::with_gil(|py| { - let set = PyFrozenSet::empty(py).unwrap(); + let set = PyFrozenSet::empty_bound(py).unwrap(); assert_eq!(0, set.len()); assert!(set.is_empty()); }); @@ -351,7 +344,7 @@ mod tests { #[test] fn test_frozenset_contains() { Python::with_gil(|py| { - let set = PyFrozenSet::new(py, &[1]).unwrap(); + let set = PyFrozenSet::new_bound(py, &[1]).unwrap(); assert!(set.contains(1).unwrap()); }); } @@ -359,7 +352,7 @@ mod tests { #[test] fn test_frozenset_iter() { Python::with_gil(|py| { - let set = PyFrozenSet::new(py, &[1]).unwrap(); + let set = PyFrozenSet::new_bound(py, &[1]).unwrap(); for el in set { assert_eq!(1i32, el.extract::().unwrap()); @@ -381,7 +374,7 @@ mod tests { #[test] fn test_frozenset_iter_size_hint() { Python::with_gil(|py| { - let set = PyFrozenSet::new(py, &[1]).unwrap(); + let set = PyFrozenSet::new_bound(py, &[1]).unwrap(); let mut iter = set.iter(); // Exact size From 3cb286e0d2749a67f1861d0d62295ffb32ff2fac Mon Sep 17 00:00:00 2001 From: Alexander Clausen Date: Thu, 25 Apr 2024 17:36:29 +0200 Subject: [PATCH 033/495] docs: fix typo in trait-bounds.md (#4124) --- guide/src/trait-bounds.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/guide/src/trait-bounds.md b/guide/src/trait-bounds.md index 0644e679190..eb67bc42413 100644 --- a/guide/src/trait-bounds.md +++ b/guide/src/trait-bounds.md @@ -1,6 +1,6 @@ # Using in Python a Rust function with trait bounds -PyO3 allows for easy conversion from Rust to Python for certain functions and classes (see the [conversion table](conversions/tables.md). +PyO3 allows for easy conversion from Rust to Python for certain functions and classes (see the [conversion table](conversions/tables.md)). However, it is not always straightforward to convert Rust code that requires a given trait implementation as an argument. This tutorial explains how to convert a Rust function that takes a trait as argument for use in Python with classes implementing the same methods as the trait. From 8734b76f60058475ddc3b80c86b8106796870fac Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Thu, 25 Apr 2024 19:44:42 +0200 Subject: [PATCH 034/495] feature gate deprecated APIs for `PyIterator` (#4119) --- src/types/iterator.rs | 59 ++++++++++++++++++++----------------------- 1 file changed, 28 insertions(+), 31 deletions(-) diff --git a/src/types/iterator.rs b/src/types/iterator.rs index 60f36f22de2..53330705869 100644 --- a/src/types/iterator.rs +++ b/src/types/iterator.rs @@ -1,9 +1,9 @@ use crate::ffi_ptr_ext::FfiPtrExt; use crate::instance::Borrowed; use crate::py_result_ext::PyResultExt; -use crate::{ - ffi, AsPyPointer, Bound, PyAny, PyDowncastError, PyErr, PyNativeType, PyResult, PyTypeCheck, -}; +#[cfg(feature = "gil-refs")] +use crate::PyDowncastError; +use crate::{ffi, AsPyPointer, Bound, PyAny, PyErr, PyNativeType, PyResult, PyTypeCheck}; /// A Python iterator object. /// @@ -32,12 +32,10 @@ pyobject_native_type_extract!(PyIterator); impl PyIterator { /// Deprecated form of `PyIterator::from_bound_object`. - #[cfg_attr( - not(feature = "gil-refs"), - deprecated( - since = "0.21.0", - note = "`PyIterator::from_object` will be replaced by `PyIterator::from_bound_object` in a future PyO3 version" - ) + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`PyIterator::from_object` will be replaced by `PyIterator::from_bound_object` in a future PyO3 version" )] pub fn from_object(obj: &PyAny) -> PyResult<&PyIterator> { Self::from_bound_object(&obj.as_borrowed()).map(Bound::into_gil_ref) @@ -128,6 +126,7 @@ impl PyTypeCheck for PyIterator { } } +#[cfg(feature = "gil-refs")] #[allow(deprecated)] impl<'v> crate::PyTryFrom<'v> for PyIterator { fn try_from>(value: V) -> Result<&'v PyIterator, PyDowncastError<'v>> { @@ -153,19 +152,17 @@ impl<'v> crate::PyTryFrom<'v> for PyIterator { } #[cfg(test)] -#[cfg_attr(not(feature = "gil-refs"), allow(deprecated))] mod tests { use super::PyIterator; use crate::exceptions::PyTypeError; - use crate::gil::GILPool; - use crate::types::{PyAnyMethods, PyDict, PyList}; - use crate::{Py, PyAny, Python, ToPyObject}; + use crate::types::{PyAnyMethods, PyDict, PyList, PyListMethods}; + use crate::{Python, ToPyObject}; #[test] fn vec_iter() { Python::with_gil(|py| { let obj = vec![10, 20].to_object(py); - let inst = obj.as_ref(py); + let inst = obj.bind(py); let mut it = inst.iter().unwrap(); assert_eq!( 10_i32, @@ -188,7 +185,7 @@ mod tests { }); Python::with_gil(|py| { - let inst = obj.as_ref(py); + let inst = obj.bind(py); let mut it = inst.iter().unwrap(); assert_eq!( @@ -206,26 +203,24 @@ mod tests { fn iter_item_refcnt() { Python::with_gil(|py| { let count; - let obj = py.eval("object()", None, None).unwrap(); + let obj = py.eval_bound("object()", None, None).unwrap(); let list = { - let _pool = unsafe { GILPool::new() }; - let list = PyList::empty(py); + let list = PyList::empty_bound(py); list.append(10).unwrap(); - list.append(obj).unwrap(); + list.append(&obj).unwrap(); count = obj.get_refcnt(); list.to_object(py) }; { - let _pool = unsafe { GILPool::new() }; - let inst = list.as_ref(py); + let inst = list.bind(py); let mut it = inst.iter().unwrap(); assert_eq!( 10_i32, it.next().unwrap().unwrap().extract::<'_, i32>().unwrap() ); - assert!(it.next().unwrap().unwrap().is(obj)); + assert!(it.next().unwrap().unwrap().is(&obj)); assert!(it.next().is_none()); } assert_eq!(count, obj.get_refcnt()); @@ -293,18 +288,20 @@ def fibonacci(target): fn int_not_iterable() { Python::with_gil(|py| { let x = 5.to_object(py); - let err = PyIterator::from_object(x.as_ref(py)).unwrap_err(); + let err = PyIterator::from_bound_object(x.bind(py)).unwrap_err(); assert!(err.is_instance_of::(py)); }); } #[test] - + #[cfg(feature = "gil-refs")] + #[allow(deprecated)] fn iterator_try_from() { Python::with_gil(|py| { - let obj: Py = vec![10, 20].to_object(py).as_ref(py).iter().unwrap().into(); - let iter: &PyIterator = obj.downcast(py).unwrap(); + let obj: crate::Py = + vec![10, 20].to_object(py).as_ref(py).iter().unwrap().into(); + let iter = ::try_from(obj.as_ref(py)).unwrap(); assert!(obj.is(iter)); }); } @@ -321,14 +318,14 @@ def fibonacci(target): #[crate::pymethods(crate = "crate")] impl Downcaster { - fn downcast_iterator(&mut self, obj: &PyAny) { + fn downcast_iterator(&mut self, obj: &crate::Bound<'_, crate::PyAny>) { self.failed = Some(obj.downcast::().unwrap_err().into()); } } // Regression test for 2913 Python::with_gil(|py| { - let downcaster = Py::new(py, Downcaster { failed: None }).unwrap(); + let downcaster = crate::Py::new(py, Downcaster { failed: None }).unwrap(); crate::py_run!( py, downcaster, @@ -360,13 +357,13 @@ def fibonacci(target): #[cfg(feature = "macros")] fn python_class_iterator() { #[crate::pyfunction(crate = "crate")] - fn assert_iterator(obj: &PyAny) { + fn assert_iterator(obj: &crate::Bound<'_, crate::PyAny>) { assert!(obj.downcast::().is_ok()) } // Regression test for 2913 Python::with_gil(|py| { - let assert_iterator = crate::wrap_pyfunction!(assert_iterator, py).unwrap(); + let assert_iterator = crate::wrap_pyfunction_bound!(assert_iterator, py).unwrap(); crate::py_run!( py, assert_iterator, @@ -385,7 +382,7 @@ def fibonacci(target): #[cfg(not(Py_LIMITED_API))] fn length_hint_becomes_size_hint_lower_bound() { Python::with_gil(|py| { - let list = py.eval("[1, 2, 3]", None, None).unwrap(); + let list = py.eval_bound("[1, 2, 3]", None, None).unwrap(); let iter = list.iter().unwrap(); let hint = iter.size_hint(); assert_eq!(hint, (3, None)); From c66ed292ec1849e031592b961c47ec21b37407ca Mon Sep 17 00:00:00 2001 From: David Matos Date: Fri, 26 Apr 2024 08:00:13 +0200 Subject: [PATCH 035/495] Disable PyUnicode_DATA on PyPy (#4116) * Disable PYUNICODE_DATA on PyPy * Add newsfragment * Adjust import on PyString --- newsfragments/4116.fixed.md | 1 + pyo3-ffi/src/cpython/unicodeobject.rs | 10 +++++----- src/ffi/tests.rs | 15 +++++++++------ src/types/string.rs | 20 ++++++++++---------- 4 files changed, 25 insertions(+), 21 deletions(-) create mode 100644 newsfragments/4116.fixed.md diff --git a/newsfragments/4116.fixed.md b/newsfragments/4116.fixed.md new file mode 100644 index 00000000000..63531aceb39 --- /dev/null +++ b/newsfragments/4116.fixed.md @@ -0,0 +1 @@ +Disable `PyUnicode_DATA` on PyPy: Not exposed by PyPy. diff --git a/pyo3-ffi/src/cpython/unicodeobject.rs b/pyo3-ffi/src/cpython/unicodeobject.rs index 9ab523a2d7f..feb78cf0c82 100644 --- a/pyo3-ffi/src/cpython/unicodeobject.rs +++ b/pyo3-ffi/src/cpython/unicodeobject.rs @@ -449,19 +449,19 @@ pub const PyUnicode_1BYTE_KIND: c_uint = 1; pub const PyUnicode_2BYTE_KIND: c_uint = 2; pub const PyUnicode_4BYTE_KIND: c_uint = 4; -#[cfg(not(GraalPy))] +#[cfg(not(any(GraalPy, PyPy)))] #[inline] pub unsafe fn PyUnicode_1BYTE_DATA(op: *mut PyObject) -> *mut Py_UCS1 { PyUnicode_DATA(op) as *mut Py_UCS1 } -#[cfg(not(GraalPy))] +#[cfg(not(any(GraalPy, PyPy)))] #[inline] pub unsafe fn PyUnicode_2BYTE_DATA(op: *mut PyObject) -> *mut Py_UCS2 { PyUnicode_DATA(op) as *mut Py_UCS2 } -#[cfg(not(GraalPy))] +#[cfg(not(any(GraalPy, PyPy)))] #[inline] pub unsafe fn PyUnicode_4BYTE_DATA(op: *mut PyObject) -> *mut Py_UCS4 { PyUnicode_DATA(op) as *mut Py_UCS4 @@ -487,7 +487,7 @@ pub unsafe fn _PyUnicode_COMPACT_DATA(op: *mut PyObject) -> *mut c_void { } } -#[cfg(not(GraalPy))] +#[cfg(not(any(GraalPy, PyPy)))] #[inline] pub unsafe fn _PyUnicode_NONCOMPACT_DATA(op: *mut PyObject) -> *mut c_void { debug_assert!(!(*(op as *mut PyUnicodeObject)).data.any.is_null()); @@ -495,7 +495,7 @@ pub unsafe fn _PyUnicode_NONCOMPACT_DATA(op: *mut PyObject) -> *mut c_void { (*(op as *mut PyUnicodeObject)).data.any } -#[cfg(not(GraalPy))] +#[cfg(not(any(GraalPy, PyPy)))] #[inline] pub unsafe fn PyUnicode_DATA(op: *mut PyObject) -> *mut c_void { debug_assert!(crate::PyUnicode_Check(op) != 0); diff --git a/src/ffi/tests.rs b/src/ffi/tests.rs index 3532172c933..5aee1618472 100644 --- a/src/ffi/tests.rs +++ b/src/ffi/tests.rs @@ -2,11 +2,14 @@ use crate::ffi::*; use crate::types::any::PyAnyMethods; use crate::Python; +#[cfg(all(PyPy, feature = "macros"))] +use crate::types::PyString; + +#[cfg(not(any(Py_LIMITED_API, PyPy)))] +use crate::types::PyString; + #[cfg(not(Py_LIMITED_API))] -use crate::{ - types::{PyDict, PyString}, - Bound, IntoPy, Py, PyAny, -}; +use crate::{types::PyDict, Bound, IntoPy, Py, PyAny}; #[cfg(not(any(Py_3_12, Py_LIMITED_API)))] use libc::wchar_t; @@ -160,7 +163,7 @@ fn ascii_object_bitfield() { } #[test] -#[cfg(not(Py_LIMITED_API))] +#[cfg(not(any(Py_LIMITED_API, PyPy)))] #[cfg_attr(Py_3_10, allow(deprecated))] fn ascii() { Python::with_gil(|py| { @@ -202,7 +205,7 @@ fn ascii() { } #[test] -#[cfg(not(Py_LIMITED_API))] +#[cfg(not(any(Py_LIMITED_API, PyPy)))] #[cfg_attr(Py_3_10, allow(deprecated))] fn ucs4() { Python::with_gil(|py| { diff --git a/src/types/string.rs b/src/types/string.rs index 4aa73341ae9..09c5903547c 100644 --- a/src/types/string.rs +++ b/src/types/string.rs @@ -264,7 +264,7 @@ impl PyString { /// /// By using this API, you accept responsibility for testing that PyStringData behaves as /// expected on the targets where you plan to distribute your software. - #[cfg(not(any(Py_LIMITED_API, GraalPy)))] + #[cfg(not(any(Py_LIMITED_API, GraalPy, PyPy)))] pub unsafe fn data(&self) -> PyResult> { self.as_borrowed().data() } @@ -313,7 +313,7 @@ pub trait PyStringMethods<'py>: crate::sealed::Sealed { /// /// By using this API, you accept responsibility for testing that PyStringData behaves as /// expected on the targets where you plan to distribute your software. - #[cfg(not(any(Py_LIMITED_API, GraalPy)))] + #[cfg(not(any(Py_LIMITED_API, GraalPy, PyPy)))] unsafe fn data(&self) -> PyResult>; } @@ -339,7 +339,7 @@ impl<'py> PyStringMethods<'py> for Bound<'py, PyString> { } } - #[cfg(not(any(Py_LIMITED_API, GraalPy)))] + #[cfg(not(any(Py_LIMITED_API, GraalPy, PyPy)))] unsafe fn data(&self) -> PyResult> { self.as_borrowed().data() } @@ -402,7 +402,7 @@ impl<'a> Borrowed<'a, '_, PyString> { Cow::Owned(String::from_utf8_lossy(bytes.as_bytes()).into_owned()) } - #[cfg(not(any(Py_LIMITED_API, GraalPy)))] + #[cfg(not(any(Py_LIMITED_API, GraalPy, PyPy)))] unsafe fn data(self) -> PyResult> { let ptr = self.as_ptr(); @@ -584,7 +584,7 @@ mod tests { } #[test] - #[cfg(not(Py_LIMITED_API))] + #[cfg(not(any(Py_LIMITED_API, PyPy)))] fn test_string_data_ucs1() { Python::with_gil(|py| { let s = PyString::new_bound(py, "hello, world"); @@ -597,7 +597,7 @@ mod tests { } #[test] - #[cfg(not(Py_LIMITED_API))] + #[cfg(not(any(Py_LIMITED_API, PyPy)))] fn test_string_data_ucs1_invalid() { Python::with_gil(|py| { // 0xfe is not allowed in UTF-8. @@ -625,7 +625,7 @@ mod tests { } #[test] - #[cfg(not(Py_LIMITED_API))] + #[cfg(not(any(Py_LIMITED_API, PyPy)))] fn test_string_data_ucs2() { Python::with_gil(|py| { let s = py.eval_bound("'foo\\ud800'", None, None).unwrap(); @@ -641,7 +641,7 @@ mod tests { } #[test] - #[cfg(all(not(Py_LIMITED_API), target_endian = "little"))] + #[cfg(all(not(any(Py_LIMITED_API, PyPy)), target_endian = "little"))] fn test_string_data_ucs2_invalid() { Python::with_gil(|py| { // U+FF22 (valid) & U+d800 (never valid) @@ -669,7 +669,7 @@ mod tests { } #[test] - #[cfg(not(Py_LIMITED_API))] + #[cfg(not(any(Py_LIMITED_API, PyPy)))] fn test_string_data_ucs4() { Python::with_gil(|py| { let s = "哈哈🐈"; @@ -682,7 +682,7 @@ mod tests { } #[test] - #[cfg(all(not(Py_LIMITED_API), target_endian = "little"))] + #[cfg(all(not(any(Py_LIMITED_API, PyPy)), target_endian = "little"))] fn test_string_data_ucs4_invalid() { Python::with_gil(|py| { // U+20000 (valid) & U+d800 (never valid) From 8ff5e5b0ab6002922d8d9b523b1b50fb5cf80b9a Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Sat, 27 Apr 2024 01:12:11 -0400 Subject: [PATCH 036/495] Fix lychee for guide (#4130) * Fix lychee for guide * Update nightly in netlify --- .netlify/build.sh | 1 + guide/src/parallelism.md | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.netlify/build.sh b/.netlify/build.sh index e1d86788ca1..a61180be59a 100755 --- a/.netlify/build.sh +++ b/.netlify/build.sh @@ -2,6 +2,7 @@ set -uex +rustup update nightly rustup default nightly PYO3_VERSION=$(cargo search pyo3 --limit 1 | head -1 | tr -s ' ' | cut -d ' ' -f 3 | tr -d '"') diff --git a/guide/src/parallelism.md b/guide/src/parallelism.md index 792e0ed8de4..a288b14be19 100644 --- a/guide/src/parallelism.md +++ b/guide/src/parallelism.md @@ -1,6 +1,6 @@ # Parallelism -CPython has the infamous [Global Interpreter Lock](https://docs.python.org/3/glossary.html#term-global-interpreter-lock), which prevents several threads from executing Python bytecode in parallel. This makes threading in Python a bad fit for [CPU-bound](https://stackoverflow.com/questions/868568/) tasks and often forces developers to accept the overhead of multiprocessing. +CPython has the infamous [Global Interpreter Lock](https://docs.python.org/3/glossary.html#term-global-interpreter-lock), which prevents several threads from executing Python bytecode in parallel. This makes threading in Python a bad fit for [CPU-bound](https://en.wikipedia.org/wiki/CPU-bound) tasks and often forces developers to accept the overhead of multiprocessing. In PyO3 parallelism can be easily achieved in Rust-only code. Let's take a look at our [word-count](https://github.com/PyO3/pyo3/blob/main/examples/word-count/src/lib.rs) example, where we have a `search` function that utilizes the [rayon](https://github.com/rayon-rs/rayon) crate to count words in parallel. ```rust,no_run From 059c8b386201a2aaa7a76b06392308ddcdfaa6e3 Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Sat, 27 Apr 2024 13:34:27 +0200 Subject: [PATCH 037/495] feature gate deprecated APIs for `PyBytes` and `PyPyByteArray` (#4131) --- src/types/bytearray.rs | 30 ++++++++++++------------------ src/types/bytes.rs | 30 ++++++++++++------------------ 2 files changed, 24 insertions(+), 36 deletions(-) diff --git a/src/types/bytearray.rs b/src/types/bytearray.rs index 7227519975b..8bc4bf55d5b 100644 --- a/src/types/bytearray.rs +++ b/src/types/bytearray.rs @@ -15,12 +15,10 @@ pyobject_native_type_core!(PyByteArray, pyobject_native_static_type_object!(ffi: impl PyByteArray { /// Deprecated form of [`PyByteArray::new_bound`] - #[cfg_attr( - not(feature = "gil-refs"), - deprecated( - since = "0.21.0", - note = "`PyByteArray::new` will be replaced by `PyByteArray::new_bound` in a future PyO3 version" - ) + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`PyByteArray::new` will be replaced by `PyByteArray::new_bound` in a future PyO3 version" )] pub fn new<'py>(py: Python<'py>, src: &[u8]) -> &'py PyByteArray { Self::new_bound(py, src).into_gil_ref() @@ -40,12 +38,10 @@ impl PyByteArray { } /// Deprecated form of [`PyByteArray::new_bound_with`] - #[cfg_attr( - not(feature = "gil-refs"), - deprecated( - since = "0.21.0", - note = "`PyByteArray::new_with` will be replaced by `PyByteArray::new_bound_with` in a future PyO3 version" - ) + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`PyByteArray::new_with` will be replaced by `PyByteArray::new_bound_with` in a future PyO3 version" )] pub fn new_with(py: Python<'_>, len: usize, init: F) -> PyResult<&PyByteArray> where @@ -104,12 +100,10 @@ impl PyByteArray { } /// Deprecated form of [`PyByteArray::from_bound`] - #[cfg_attr( - not(feature = "gil-refs"), - deprecated( - since = "0.21.0", - note = "`PyByteArray::from` will be replaced by `PyByteArray::from_bound` in a future PyO3 version" - ) + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`PyByteArray::from` will be replaced by `PyByteArray::from_bound` in a future PyO3 version" )] pub fn from(src: &PyAny) -> PyResult<&PyByteArray> { PyByteArray::from_bound(&src.as_borrowed()).map(Bound::into_gil_ref) diff --git a/src/types/bytes.rs b/src/types/bytes.rs index 44c87560514..f3da65c7c97 100644 --- a/src/types/bytes.rs +++ b/src/types/bytes.rs @@ -17,12 +17,10 @@ pyobject_native_type_core!(PyBytes, pyobject_native_static_type_object!(ffi::PyB impl PyBytes { /// Deprecated form of [`PyBytes::new_bound`]. - #[cfg_attr( - not(feature = "gil-refs"), - deprecated( - since = "0.21.0", - note = "`PyBytes::new` will be replaced by `PyBytes::new_bound` in a future PyO3 version" - ) + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`PyBytes::new` will be replaced by `PyBytes::new_bound` in a future PyO3 version" )] pub fn new<'p>(py: Python<'p>, s: &[u8]) -> &'p PyBytes { Self::new_bound(py, s).into_gil_ref() @@ -43,12 +41,10 @@ impl PyBytes { } /// Deprecated form of [`PyBytes::new_bound_with`]. - #[cfg_attr( - not(feature = "gil-refs"), - deprecated( - since = "0.21.0", - note = "`PyBytes::new_with` will be replaced by `PyBytes::new_bound_with` in a future PyO3 version" - ) + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`PyBytes::new_with` will be replaced by `PyBytes::new_bound_with` in a future PyO3 version" )] pub fn new_with(py: Python<'_>, len: usize, init: F) -> PyResult<&PyBytes> where @@ -103,12 +99,10 @@ impl PyBytes { /// /// # Safety /// See [`PyBytes::bound_from_ptr`]. - #[cfg_attr( - not(feature = "gil-refs"), - deprecated( - since = "0.21.0", - note = "`PyBytes::from_ptr` will be replaced by `PyBytes::bound_from_ptr` in a future PyO3 version" - ) + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`PyBytes::from_ptr` will be replaced by `PyBytes::bound_from_ptr` in a future PyO3 version" )] pub unsafe fn from_ptr(py: Python<'_>, ptr: *const u8, len: usize) -> &PyBytes { Self::bound_from_ptr(py, ptr, len).into_gil_ref() From 6fb972b2324ba115517ba886943e98f1b1dc84b2 Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Sat, 27 Apr 2024 17:34:13 +0200 Subject: [PATCH 038/495] feature gate deprecated APIs for `PyEllipsis`, `PyNone` and `PyNotImplemented` (#4132) --- src/types/ellipsis.rs | 10 ++++------ src/types/none.rs | 10 ++++------ src/types/notimplemented.rs | 10 ++++------ 3 files changed, 12 insertions(+), 18 deletions(-) diff --git a/src/types/ellipsis.rs b/src/types/ellipsis.rs index 4d091c6f9d0..cbeaf489c17 100644 --- a/src/types/ellipsis.rs +++ b/src/types/ellipsis.rs @@ -12,12 +12,10 @@ pyobject_native_type_extract!(PyEllipsis); impl PyEllipsis { /// Returns the `Ellipsis` object. - #[cfg_attr( - not(feature = "gil-refs"), - deprecated( - since = "0.21.0", - note = "`PyEllipsis::get` will be replaced by `PyEllipsis::get_bound` in a future PyO3 version" - ) + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`PyEllipsis::get` will be replaced by `PyEllipsis::get_bound` in a future PyO3 version" )] #[inline] pub fn get(py: Python<'_>) -> &PyEllipsis { diff --git a/src/types/none.rs b/src/types/none.rs index a14af044808..0ab1570b92d 100644 --- a/src/types/none.rs +++ b/src/types/none.rs @@ -14,12 +14,10 @@ pyobject_native_type_extract!(PyNone); impl PyNone { /// Returns the `None` object. /// Deprecated form of [`PyNone::get_bound`] - #[cfg_attr( - not(feature = "gil-refs"), - deprecated( - since = "0.21.0", - note = "`PyNone::get` will be replaced by `PyNone::get_bound` in a future PyO3 version" - ) + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`PyNone::get` will be replaced by `PyNone::get_bound` in a future PyO3 version" )] #[inline] pub fn get(py: Python<'_>) -> &PyNone { diff --git a/src/types/notimplemented.rs b/src/types/notimplemented.rs index 9d31e670e2b..7fad1220b26 100644 --- a/src/types/notimplemented.rs +++ b/src/types/notimplemented.rs @@ -12,12 +12,10 @@ pyobject_native_type_extract!(PyNotImplemented); impl PyNotImplemented { /// Returns the `NotImplemented` object. - #[cfg_attr( - not(feature = "gil-refs"), - deprecated( - since = "0.21.0", - note = "`PyNotImplemented::get` will be replaced by `PyNotImplemented::get_bound` in a future PyO3 version" - ) + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`PyNotImplemented::get` will be replaced by `PyNotImplemented::get_bound` in a future PyO3 version" )] #[inline] pub fn get(py: Python<'_>) -> &PyNotImplemented { From 9e1960ea348d6beab648c5c05fb8a0bad306d9e1 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Sun, 28 Apr 2024 12:11:28 -0400 Subject: [PATCH 039/495] Update MSRV to 1.63 (#4129) * Bump MSRV to 1.63 * Drop parking_lot in favor of std::sync * Make portable-atomic dep conditional * Remove no longer required cfg --- .github/workflows/build.yml | 2 +- .github/workflows/ci.yml | 4 +- Cargo.toml | 7 +-- README.md | 4 +- build.rs | 2 +- guide/src/building-and-distribution.md | 2 +- guide/src/getting-started.md | 2 +- newsfragments/4129.changed.md | 1 + noxfile.py | 21 ++------ pyo3-build-config/src/lib.rs | 5 -- pyo3-ffi/README.md | 2 +- pyo3-ffi/build.rs | 2 +- pyo3-ffi/src/lib.rs | 2 +- src/coroutine/cancel.rs | 9 ++-- src/gil.rs | 67 +++++++++++--------------- src/impl_/pymodule.rs | 10 +++- src/lib.rs | 2 +- tests/test_coroutine.rs | 3 ++ 18 files changed, 67 insertions(+), 80 deletions(-) create mode 100644 newsfragments/4129.changed.md diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d2f74401dd6..40d4a0012fd 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -54,7 +54,7 @@ jobs: name: Prepare LD_LIBRARY_PATH (Ubuntu only) run: echo LD_LIBRARY_PATH=${pythonLocation}/lib >> $GITHUB_ENV - - if: inputs.rust == '1.56.0' + - if: inputs.rust == '1.63.0' name: Prepare minimal package versions (MSRV only) run: nox -s set-minimal-package-versions diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 12db5867f90..0c245f33483 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,7 +47,7 @@ jobs: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@master with: - toolchain: 1.56.0 + toolchain: 1.63.0 targets: x86_64-unknown-linux-gnu components: rust-src - uses: actions/setup-python@v5 @@ -255,7 +255,7 @@ jobs: ] include: # Test minimal supported Rust version - - rust: 1.56.0 + - rust: 1.63.0 python-version: "3.12" platform: { diff --git a/Cargo.toml b/Cargo.toml index 90bcc03c80c..9202c69ef92 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,14 +12,12 @@ categories = ["api-bindings", "development-tools::ffi"] license = "MIT OR Apache-2.0" exclude = ["/.gitignore", ".cargo/config", "/codecov.yml", "/Makefile", "/pyproject.toml", "/noxfile.py", "/.github", "/tests/test_compile_error.rs", "/tests/ui"] edition = "2021" -rust-version = "1.56" +rust-version = "1.63" [dependencies] cfg-if = "1.0" libc = "0.2.62" -parking_lot = ">= 0.11, < 0.13" memoffset = "0.9" -portable-atomic = "1.0" # ffi bindings to the python interpreter, split into a separate crate so they can be used independently pyo3-ffi = { path = "pyo3-ffi", version = "=0.21.2" } @@ -46,6 +44,9 @@ rust_decimal = { version = "1.0.0", default-features = false, optional = true } serde = { version = "1.0", optional = true } smallvec = { version = "1.0", optional = true } +[target.'cfg(not(target_has_atomic = "64"))'.dependencies] +portable-atomic = "1.0" + [dev-dependencies] assert_approx_eq = "1.1.0" chrono = "0.4.25" diff --git a/README.md b/README.md index 4da2447e8c4..72653e8fc77 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![benchmark](https://img.shields.io/badge/benchmark-✓-Green?logo=github)](https://pyo3.rs/dev/bench/) [![codecov](https://img.shields.io/codecov/c/gh/PyO3/pyo3?logo=codecov)](https://codecov.io/gh/PyO3/pyo3) [![crates.io](https://img.shields.io/crates/v/pyo3?logo=rust)](https://crates.io/crates/pyo3) -[![minimum rustc 1.56](https://img.shields.io/badge/rustc-1.56+-blue?logo=rust)](https://rust-lang.github.io/rfcs/2495-min-rust-version.html) +[![minimum rustc 1.63](https://img.shields.io/badge/rustc-1.63+-blue?logo=rust)](https://rust-lang.github.io/rfcs/2495-min-rust-version.html) [![discord server](https://img.shields.io/discord/1209263839632424990?logo=discord)](https://discord.gg/33kcChzH7f) [![contributing notes](https://img.shields.io/badge/contribute-on%20github-Green?logo=github)](https://github.com/PyO3/pyo3/blob/main/Contributing.md) @@ -18,7 +18,7 @@ PyO3 supports the following software versions: - Python 3.7 and up (CPython, PyPy, and GraalPy) - - Rust 1.56 and up + - Rust 1.63 and up You can use PyO3 to write a native Python module in Rust, or to embed Python in a Rust binary. The following sections explain each of these in turn. diff --git a/build.rs b/build.rs index 7f0ae6e31c8..28592004df2 100644 --- a/build.rs +++ b/build.rs @@ -39,7 +39,7 @@ fn configure_pyo3() -> Result<()> { println!("{}", cfg) } - // Emit cfgs like `thread_local_const_init` + // Emit cfgs like `invalid_from_utf8_lint` print_feature_cfgs(); Ok(()) diff --git a/guide/src/building-and-distribution.md b/guide/src/building-and-distribution.md index 780f135e211..b7aa2d2abc2 100644 --- a/guide/src/building-and-distribution.md +++ b/guide/src/building-and-distribution.md @@ -151,7 +151,7 @@ rustflags = [ ] ``` -Alternatively, on rust >= 1.56, one can include in `build.rs`: +Alternatively, one can include in `build.rs`: ```rust fn main() { diff --git a/guide/src/getting-started.md b/guide/src/getting-started.md index 59cf5ba6ded..94ab95cb3d6 100644 --- a/guide/src/getting-started.md +++ b/guide/src/getting-started.md @@ -6,7 +6,7 @@ To get started using PyO3 you will need three things: a Rust toolchain, a Python ## Rust -First, make sure you have Rust installed on your system. If you haven't already done so, try following the instructions [here](https://www.rust-lang.org/tools/install). PyO3 runs on both the `stable` and `nightly` versions so you can choose whichever one fits you best. The minimum required Rust version is 1.56. +First, make sure you have Rust installed on your system. If you haven't already done so, try following the instructions [here](https://www.rust-lang.org/tools/install). PyO3 runs on both the `stable` and `nightly` versions so you can choose whichever one fits you best. The minimum required Rust version is 1.63. If you can run `rustc --version` and the version is new enough you're good to go! diff --git a/newsfragments/4129.changed.md b/newsfragments/4129.changed.md new file mode 100644 index 00000000000..6818181ad0c --- /dev/null +++ b/newsfragments/4129.changed.md @@ -0,0 +1 @@ +Raised the MSRV to 1.63 diff --git a/noxfile.py b/noxfile.py index 3d82c8d3746..c8d2ead63d3 100644 --- a/noxfile.py +++ b/noxfile.py @@ -557,22 +557,11 @@ def set_minimal_package_versions(session: nox.Session): "examples/word-count", ) min_pkg_versions = { - "rust_decimal": "1.26.1", - "csv": "1.1.6", - "indexmap": "1.6.2", - "hashbrown": "0.9.1", - "log": "0.4.17", - "once_cell": "1.17.2", - "rayon": "1.6.1", - "rayon-core": "1.10.2", - "regex": "1.7.3", - "proptest": "1.0.0", - "chrono": "0.4.25", - "byteorder": "1.4.3", - "crossbeam-channel": "0.5.8", - "crossbeam-deque": "0.8.3", - "crossbeam-epoch": "0.9.15", - "crossbeam-utils": "0.8.16", + "regex": "1.9.6", + "proptest": "1.2.0", + "trybuild": "1.0.89", + "eyre": "0.6.8", + "allocator-api2": "0.2.10", } # run cargo update first to ensure that everything is at highest diff --git a/pyo3-build-config/src/lib.rs b/pyo3-build-config/src/lib.rs index eab7376d0ea..5b1cfdaf322 100644 --- a/pyo3-build-config/src/lib.rs +++ b/pyo3-build-config/src/lib.rs @@ -145,11 +145,6 @@ pub fn print_feature_cfgs() { let rustc_minor_version = rustc_minor_version().unwrap_or(0); - // Enable use of const initializer for thread_local! on Rust 1.59 and greater - if rustc_minor_version >= 59 { - println!("cargo:rustc-cfg=thread_local_const_init"); - } - // invalid_from_utf8 lint was added in Rust 1.74 if rustc_minor_version >= 74 { println!("cargo:rustc-cfg=invalid_from_utf8_lint"); diff --git a/pyo3-ffi/README.md b/pyo3-ffi/README.md index 4e85e83c88f..283a7072357 100644 --- a/pyo3-ffi/README.md +++ b/pyo3-ffi/README.md @@ -14,7 +14,7 @@ Manual][capi] for up-to-date documentation. PyO3 supports the following software versions: - Python 3.7 and up (CPython and PyPy) - - Rust 1.56 and up + - Rust 1.63 and up # Example: Building Python Native modules diff --git a/pyo3-ffi/build.rs b/pyo3-ffi/build.rs index a5ab352b6a7..405da889708 100644 --- a/pyo3-ffi/build.rs +++ b/pyo3-ffi/build.rs @@ -189,7 +189,7 @@ fn configure_pyo3() -> Result<()> { println!("{}", line); } - // Emit cfgs like `thread_local_const_init` + // Emit cfgs like `invalid_from_utf8_lint` print_feature_cfgs(); Ok(()) diff --git a/pyo3-ffi/src/lib.rs b/pyo3-ffi/src/lib.rs index 7a203362ed5..877d42dce33 100644 --- a/pyo3-ffi/src/lib.rs +++ b/pyo3-ffi/src/lib.rs @@ -51,7 +51,7 @@ //! //! PyO3 supports the following software versions: //! - Python 3.7 and up (CPython and PyPy) -//! - Rust 1.56 and up +//! - Rust 1.63 and up //! //! # Example: Building Python Native modules //! diff --git a/src/coroutine/cancel.rs b/src/coroutine/cancel.rs index 2b10fb9a438..47f5d69430a 100644 --- a/src/coroutine/cancel.rs +++ b/src/coroutine/cancel.rs @@ -1,8 +1,7 @@ use crate::{Py, PyAny, PyObject}; -use parking_lot::Mutex; use std::future::Future; use std::pin::Pin; -use std::sync::Arc; +use std::sync::{Arc, Mutex}; use std::task::{Context, Poll, Waker}; #[derive(Debug, Default)] @@ -25,12 +24,12 @@ impl CancelHandle { /// Returns whether the associated coroutine has been cancelled. pub fn is_cancelled(&self) -> bool { - self.0.lock().exception.is_some() + self.0.lock().unwrap().exception.is_some() } /// Poll to retrieve the exception thrown in the associated coroutine. pub fn poll_cancelled(&mut self, cx: &mut Context<'_>) -> Poll { - let mut inner = self.0.lock(); + let mut inner = self.0.lock().unwrap(); if let Some(exc) = inner.exception.take() { return Poll::Ready(exc); } @@ -69,7 +68,7 @@ pub struct ThrowCallback(Arc>); impl ThrowCallback { pub(super) fn throw(&self, exc: Py) { - let mut inner = self.0.lock(); + let mut inner = self.0.lock().unwrap(); inner.exception = Some(exc); if let Some(waker) = inner.waker.take() { waker.wake(); diff --git a/src/gil.rs b/src/gil.rs index e2f36037755..0bcb8c086c0 100644 --- a/src/gil.rs +++ b/src/gil.rs @@ -2,29 +2,16 @@ use crate::impl_::not_send::{NotSend, NOT_SEND}; use crate::{ffi, Python}; -use parking_lot::{const_mutex, Mutex, Once}; use std::cell::Cell; #[cfg(debug_assertions)] use std::cell::RefCell; #[cfg(not(debug_assertions))] use std::cell::UnsafeCell; -use std::{mem, ptr::NonNull}; +use std::{mem, ptr::NonNull, sync}; -static START: Once = Once::new(); +static START: sync::Once = sync::Once::new(); -cfg_if::cfg_if! { - if #[cfg(thread_local_const_init)] { - use std::thread_local as thread_local_const_init; - } else { - macro_rules! thread_local_const_init { - ($($(#[$attr:meta])* static $name:ident: $ty:ty = const { $init:expr };)*) => ( - thread_local! { $($(#[$attr])* static $name: $ty = $init;)* } - ) - } - } -} - -thread_local_const_init! { +std::thread_local! { /// This is an internal counter in pyo3 monitoring whether this thread has the GIL. /// /// It will be incremented whenever a GILGuard or GILPool is created, and decremented whenever @@ -249,26 +236,26 @@ type PyObjVec = Vec>; /// Thread-safe storage for objects which were inc_ref / dec_ref while the GIL was not held. struct ReferencePool { // .0 is INCREFs, .1 is DECREFs - pointer_ops: Mutex<(PyObjVec, PyObjVec)>, + pointer_ops: sync::Mutex<(PyObjVec, PyObjVec)>, } impl ReferencePool { const fn new() -> Self { Self { - pointer_ops: const_mutex((Vec::new(), Vec::new())), + pointer_ops: sync::Mutex::new((Vec::new(), Vec::new())), } } fn register_incref(&self, obj: NonNull) { - self.pointer_ops.lock().0.push(obj); + self.pointer_ops.lock().unwrap().0.push(obj); } fn register_decref(&self, obj: NonNull) { - self.pointer_ops.lock().1.push(obj); + self.pointer_ops.lock().unwrap().1.push(obj); } fn update_counts(&self, _py: Python<'_>) { - let mut ops = self.pointer_ops.lock(); + let mut ops = self.pointer_ops.lock().unwrap(); if ops.0.is_empty() && ops.1.is_empty() { return; } @@ -523,9 +510,9 @@ mod tests { use super::{gil_is_acquired, GIL_COUNT, OWNED_OBJECTS, POOL}; use crate::types::any::PyAnyMethods; use crate::{ffi, gil, PyObject, Python}; - #[cfg(not(target_arch = "wasm32"))] - use parking_lot::{const_mutex, Condvar, Mutex}; use std::ptr::NonNull; + #[cfg(not(target_arch = "wasm32"))] + use std::sync; fn get_object(py: Python<'_>) -> PyObject { py.eval_bound("object()", None, None).unwrap().unbind() @@ -543,6 +530,7 @@ mod tests { !POOL .pointer_ops .lock() + .unwrap() .0 .contains(&unsafe { NonNull::new_unchecked(obj.as_ptr()) }) } @@ -551,6 +539,7 @@ mod tests { !POOL .pointer_ops .lock() + .unwrap() .1 .contains(&unsafe { NonNull::new_unchecked(obj.as_ptr()) }) } @@ -559,6 +548,7 @@ mod tests { fn pool_dec_refs_contains(obj: &PyObject) -> bool { POOL.pointer_ops .lock() + .unwrap() .1 .contains(&unsafe { NonNull::new_unchecked(obj.as_ptr()) }) } @@ -671,8 +661,8 @@ mod tests { Python::with_gil(|py| { assert_eq!(obj.get_refcnt(py), 1); let non_null = unsafe { NonNull::new_unchecked(obj.as_ptr()) }; - assert!(!POOL.pointer_ops.lock().0.contains(&non_null)); - assert!(!POOL.pointer_ops.lock().1.contains(&non_null)); + assert!(!POOL.pointer_ops.lock().unwrap().0.contains(&non_null)); + assert!(!POOL.pointer_ops.lock().unwrap().1.contains(&non_null)); }); } @@ -770,29 +760,30 @@ mod tests { #[cfg(not(target_arch = "wasm32"))] struct Event { - set: Mutex, - wait: Condvar, + set: sync::Mutex, + wait: sync::Condvar, } #[cfg(not(target_arch = "wasm32"))] impl Event { const fn new() -> Self { Self { - set: const_mutex(false), - wait: Condvar::new(), + set: sync::Mutex::new(false), + wait: sync::Condvar::new(), } } fn set(&self) { - *self.set.lock() = true; + *self.set.lock().unwrap() = true; self.wait.notify_all(); } fn wait(&self) { - let mut set = self.set.lock(); - while !*set { - self.wait.wait(&mut set); - } + drop( + self.wait + .wait_while(self.set.lock().unwrap(), |s| !*s) + .unwrap(), + ); } } @@ -891,16 +882,16 @@ mod tests { // The pointer should appear once in the incref pool, and once in the // decref pool (for the clone being created and also dropped) - assert!(POOL.pointer_ops.lock().0.contains(&ptr)); - assert!(POOL.pointer_ops.lock().1.contains(&ptr)); + assert!(POOL.pointer_ops.lock().unwrap().0.contains(&ptr)); + assert!(POOL.pointer_ops.lock().unwrap().1.contains(&ptr)); (obj, count, ptr) }); Python::with_gil(|py| { // Acquiring the gil clears the pool - assert!(!POOL.pointer_ops.lock().0.contains(&ptr)); - assert!(!POOL.pointer_ops.lock().1.contains(&ptr)); + assert!(!POOL.pointer_ops.lock().unwrap().0.contains(&ptr)); + assert!(!POOL.pointer_ops.lock().unwrap().1.contains(&ptr)); // Overall count is still unchanged assert_eq!(count, obj.get_refcnt(py)); diff --git a/src/impl_/pymodule.rs b/src/impl_/pymodule.rs index 20560aeb8d5..5f04d888a50 100644 --- a/src/impl_/pymodule.rs +++ b/src/impl_/pymodule.rs @@ -5,9 +5,17 @@ use std::{cell::UnsafeCell, marker::PhantomData}; #[cfg(all( not(any(PyPy, GraalPy)), Py_3_9, - not(all(windows, Py_LIMITED_API, not(Py_3_10))) + not(all(windows, Py_LIMITED_API, not(Py_3_10))), + not(target_has_atomic = "64"), ))] use portable_atomic::{AtomicI64, Ordering}; +#[cfg(all( + not(any(PyPy, GraalPy)), + Py_3_9, + not(all(windows, Py_LIMITED_API, not(Py_3_10))), + target_has_atomic = "64", +))] +use std::sync::atomic::{AtomicI64, Ordering}; #[cfg(not(any(PyPy, GraalPy)))] use crate::exceptions::PyImportError; diff --git a/src/lib.rs b/src/lib.rs index e444912a63d..b400f143f5a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -132,7 +132,7 @@ //! //! PyO3 supports the following software versions: //! - Python 3.7 and up (CPython and PyPy) -//! - Rust 1.56 and up +//! - Rust 1.63 and up //! //! # Example: Building a native Python module //! diff --git a/tests/test_coroutine.rs b/tests/test_coroutine.rs index 4abba9f36b4..75b524edf78 100644 --- a/tests/test_coroutine.rs +++ b/tests/test_coroutine.rs @@ -3,6 +3,7 @@ use std::{task::Poll, thread, time::Duration}; use futures::{channel::oneshot, future::poll_fn, FutureExt}; +#[cfg(not(target_has_atomic = "64"))] use portable_atomic::{AtomicBool, Ordering}; use pyo3::{ coroutine::CancelHandle, @@ -10,6 +11,8 @@ use pyo3::{ py_run, types::{IntoPyDict, PyType}, }; +#[cfg(target_has_atomic = "64")] +use std::sync::atomic::{AtomicBool, Ordering}; #[path = "../src/tests/common.rs"] mod common; From d5452bcd8d1b71eaed3437d0f39f687effb8fdbe Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Sun, 28 Apr 2024 23:03:51 +0200 Subject: [PATCH 040/495] feature gate deprecated APIs for `PyType`, `PyTypeInfo` and `PySuper` (#4134) --- guide/src/migration.md | 2 +- src/conversion.rs | 3 ++- src/instance.rs | 5 ++--- src/type_object.rs | 30 ++++++++++++------------------ src/types/any.rs | 4 ++-- src/types/pysuper.rs | 13 ++++++------- src/types/typeobject.rs | 20 ++++++++------------ 7 files changed, 33 insertions(+), 44 deletions(-) diff --git a/guide/src/migration.md b/guide/src/migration.md index d855f69d396..9c7bc15002e 100644 --- a/guide/src/migration.md +++ b/guide/src/migration.md @@ -60,7 +60,7 @@ To migrate, switch all type casts to use `obj.downcast()` instead of `try_from(o Before: -```rust +```rust,ignore # #![allow(deprecated)] # use pyo3::prelude::*; # use pyo3::types::{PyInt, PyList}; diff --git a/src/conversion.rs b/src/conversion.rs index ced209abade..8644db84289 100644 --- a/src/conversion.rs +++ b/src/conversion.rs @@ -3,7 +3,6 @@ use crate::err::{self, PyDowncastError, PyResult}; #[cfg(feature = "experimental-inspect")] use crate::inspect::types::TypeInfo; use crate::pyclass::boolean_struct::False; -use crate::type_object::PyTypeInfo; use crate::types::any::PyAnyMethods; use crate::types::PyTuple; use crate::{ @@ -435,9 +434,11 @@ pub trait PyTryInto: Sized { fn try_into_exact(&self) -> Result<&T, PyDowncastError<'_>>; } +#[cfg(feature = "gil-refs")] #[allow(deprecated)] mod implementations { use super::*; + use crate::type_object::PyTypeInfo; // TryFrom implies TryInto impl PyTryInto for PyAny diff --git a/src/instance.rs b/src/instance.rs index 02b3fc76241..cc892a2dd44 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -2310,8 +2310,6 @@ a = A() #[cfg(feature = "macros")] mod using_macros { - use crate::PyCell; - use super::*; #[crate::pyclass(crate = "crate")] @@ -2371,9 +2369,10 @@ a = A() } #[test] + #[cfg(feature = "gil-refs")] #[allow(deprecated)] fn cell_tryfrom() { - use crate::PyTryInto; + use crate::{PyCell, PyTryInto}; // More detailed tests of the underlying semantics in pycell.rs Python::with_gil(|py| { let instance: &PyAny = Py::new(py, SomeClass(0)).unwrap().into_ref(py); diff --git a/src/type_object.rs b/src/type_object.rs index 84888bee458..7f35f7d967a 100644 --- a/src/type_object.rs +++ b/src/type_object.rs @@ -66,12 +66,10 @@ pub unsafe trait PyTypeInfo: Sized + HasPyGilRef { /// Returns the safe abstraction over the type object. #[inline] - #[cfg_attr( - not(feature = "gil-refs"), - deprecated( - since = "0.21.0", - note = "`PyTypeInfo::type_object` will be replaced by `PyTypeInfo::type_object_bound` in a future PyO3 version" - ) + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`PyTypeInfo::type_object` will be replaced by `PyTypeInfo::type_object_bound` in a future PyO3 version" )] fn type_object(py: Python<'_>) -> &PyType { // This isn't implemented in terms of `type_object_bound` because this just borrowed the @@ -101,12 +99,10 @@ pub unsafe trait PyTypeInfo: Sized + HasPyGilRef { /// Checks if `object` is an instance of this type or a subclass of this type. #[inline] - #[cfg_attr( - not(feature = "gil-refs"), - deprecated( - since = "0.21.0", - note = "`PyTypeInfo::is_type_of` will be replaced by `PyTypeInfo::is_type_of_bound` in a future PyO3 version" - ) + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`PyTypeInfo::is_type_of` will be replaced by `PyTypeInfo::is_type_of_bound` in a future PyO3 version" )] fn is_type_of(object: &PyAny) -> bool { Self::is_type_of_bound(&object.as_borrowed()) @@ -120,12 +116,10 @@ pub unsafe trait PyTypeInfo: Sized + HasPyGilRef { /// Checks if `object` is an instance of this type. #[inline] - #[cfg_attr( - not(feature = "gil-refs"), - deprecated( - since = "0.21.0", - note = "`PyTypeInfo::is_exact_type_of` will be replaced by `PyTypeInfo::is_exact_type_of_bound` in a future PyO3 version" - ) + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`PyTypeInfo::is_exact_type_of` will be replaced by `PyTypeInfo::is_exact_type_of_bound` in a future PyO3 version" )] fn is_exact_type_of(object: &PyAny) -> bool { Self::is_exact_type_of_bound(&object.as_borrowed()) diff --git a/src/types/any.rs b/src/types/any.rs index 6ba34c86bc6..777a8dcb4c3 100644 --- a/src/types/any.rs +++ b/src/types/any.rs @@ -2726,9 +2726,9 @@ class SimpleClass: #[test] fn test_is_callable() { Python::with_gil(|py| { - assert!(PyList::type_object(py).is_callable()); + assert!(PyList::type_object_bound(py).is_callable()); - let not_callable = 5.to_object(py).into_ref(py); + let not_callable = 5.to_object(py).into_bound(py); assert!(!not_callable.is_callable()); }); } diff --git a/src/types/pysuper.rs b/src/types/pysuper.rs index 0f1a47444d6..7c4d781525a 100644 --- a/src/types/pysuper.rs +++ b/src/types/pysuper.rs @@ -1,7 +1,7 @@ use crate::instance::Bound; use crate::types::any::PyAnyMethods; use crate::types::PyType; -use crate::{ffi, PyNativeType, PyTypeInfo}; +use crate::{ffi, PyTypeInfo}; use crate::{PyAny, PyResult}; /// Represents a Python `super` object. @@ -17,14 +17,13 @@ pyobject_native_type_core!( impl PySuper { /// Deprecated form of `PySuper::new_bound`. - #[cfg_attr( - not(feature = "gil-refs"), - deprecated( - since = "0.21.0", - note = "`PySuper::new` will be replaced by `PySuper::new_bound` in a future PyO3 version" - ) + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`PySuper::new` will be replaced by `PySuper::new_bound` in a future PyO3 version" )] pub fn new<'py>(ty: &'py PyType, obj: &'py PyAny) -> PyResult<&'py PySuper> { + use crate::PyNativeType; Self::new_bound(&ty.as_borrowed(), &obj.as_borrowed()).map(Bound::into_gil_ref) } diff --git a/src/types/typeobject.rs b/src/types/typeobject.rs index d75e39d022d..2261834ef2a 100644 --- a/src/types/typeobject.rs +++ b/src/types/typeobject.rs @@ -15,12 +15,10 @@ pyobject_native_type_core!(PyType, pyobject_native_static_type_object!(ffi::PyTy impl PyType { /// Deprecated form of [`PyType::new_bound`]. #[inline] - #[cfg_attr( - not(feature = "gil-refs"), - deprecated( - since = "0.21.0", - note = "`PyType::new` will be replaced by `PyType::new_bound` in a future PyO3 version" - ) + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`PyType::new` will be replaced by `PyType::new_bound` in a future PyO3 version" )] pub fn new(py: Python<'_>) -> &PyType { T::type_object_bound(py).into_gil_ref() @@ -44,12 +42,10 @@ impl PyType { /// /// - The pointer must a valid non-null reference to a `PyTypeObject`. #[inline] - #[cfg_attr( - not(feature = "gil-refs"), - deprecated( - since = "0.21.0", - note = "Use `PyType::from_borrowed_type_ptr` instead" - ) + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "Use `PyType::from_borrowed_type_ptr` instead" )] pub unsafe fn from_type_ptr(py: Python<'_>, p: *mut ffi::PyTypeObject) -> &PyType { Self::from_borrowed_type_ptr(py, p).into_gil_ref() From 22c5cff0394f0095b204e9aa6e0f4b31808506a1 Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Tue, 30 Apr 2024 18:58:53 +0200 Subject: [PATCH 041/495] feature gate deprecated APIs for `PyErr` and exceptions (#4136) --- src/err/mod.rs | 108 ++++++++++++++++----------------------- src/exceptions.rs | 40 ++++++++------- tests/test_exceptions.rs | 7 ++- 3 files changed, 69 insertions(+), 86 deletions(-) diff --git a/src/err/mod.rs b/src/err/mod.rs index a61c8c62d31..4f46f360094 100644 --- a/src/err/mod.rs +++ b/src/err/mod.rs @@ -24,9 +24,9 @@ use err_state::{PyErrState, PyErrStateLazyFnOutput, PyErrStateNormalized}; /// compatibility with `?` and other Rust errors) this type supports creating exceptions instances /// in a lazy fashion, where the full Python object for the exception is created only when needed. /// -/// Accessing the contained exception in any way, such as with [`value`](PyErr::value), -/// [`get_type`](PyErr::get_type), or [`is_instance`](PyErr::is_instance) will create the full -/// exception object if it was not already created. +/// Accessing the contained exception in any way, such as with [`value_bound`](PyErr::value_bound), +/// [`get_type_bound`](PyErr::get_type_bound), or [`is_instance_bound`](PyErr::is_instance_bound) +/// will create the full exception object if it was not already created. pub struct PyErr { // Safety: can only hand out references when in the "normalized" state. Will never change // after normalization. @@ -136,7 +136,7 @@ impl PyErr { /// /// This exception instance will be initialized lazily. This avoids the need for the Python GIL /// to be held, but requires `args` to be `Send` and `Sync`. If `args` is not `Send` or `Sync`, - /// consider using [`PyErr::from_value`] instead. + /// consider using [`PyErr::from_value_bound`] instead. /// /// If `T` does not inherit from `BaseException`, then a `TypeError` will be returned. /// @@ -192,12 +192,10 @@ impl PyErr { } /// Deprecated form of [`PyErr::from_type_bound`] - #[cfg_attr( - not(feature = "gil-refs"), - deprecated( - since = "0.21.0", - note = "`PyErr::from_type` will be replaced by `PyErr::from_type_bound` in a future PyO3 version" - ) + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`PyErr::from_type` will be replaced by `PyErr::from_type_bound` in a future PyO3 version" )] pub fn from_type(ty: &PyType, args: A) -> PyErr where @@ -224,12 +222,10 @@ impl PyErr { } /// Deprecated form of [`PyErr::from_value_bound`]. - #[cfg_attr( - not(feature = "gil-refs"), - deprecated( - since = "0.21.0", - note = "`PyErr::from_value` will be replaced by `PyErr::from_value_bound` in a future PyO3 version" - ) + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`PyErr::from_value` will be replaced by `PyErr::from_value_bound` in a future PyO3 version" )] pub fn from_value(obj: &PyAny) -> PyErr { PyErr::from_value_bound(obj.as_borrowed().to_owned()) @@ -284,12 +280,10 @@ impl PyErr { } /// Deprecated form of [`PyErr::get_type_bound`]. - #[cfg_attr( - not(feature = "gil-refs"), - deprecated( - since = "0.21.0", - note = "`PyErr::get_type` will be replaced by `PyErr::get_type_bound` in a future PyO3 version" - ) + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`PyErr::get_type` will be replaced by `PyErr::get_type_bound` in a future PyO3 version" )] pub fn get_type<'py>(&'py self, py: Python<'py>) -> &'py PyType { self.get_type_bound(py).into_gil_ref() @@ -311,12 +305,10 @@ impl PyErr { } /// Deprecated form of [`PyErr::value_bound`]. - #[cfg_attr( - not(feature = "gil-refs"), - deprecated( - since = "0.21.0", - note = "`PyErr::value` will be replaced by `PyErr::value_bound` in a future PyO3 version" - ) + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`PyErr::value` will be replaced by `PyErr::value_bound` in a future PyO3 version" )] pub fn value<'py>(&'py self, py: Python<'py>) -> &'py PyBaseException { self.value_bound(py).as_gil_ref() @@ -355,12 +347,10 @@ impl PyErr { } /// Deprecated form of [`PyErr::traceback_bound`]. - #[cfg_attr( - not(feature = "gil-refs"), - deprecated( - since = "0.21.0", - note = "`PyErr::traceback` will be replaced by `PyErr::traceback_bound` in a future PyO3 version" - ) + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`PyErr::traceback` will be replaced by `PyErr::traceback_bound` in a future PyO3 version" )] pub fn traceback<'py>(&'py self, py: Python<'py>) -> Option<&'py PyTraceback> { self.normalized(py).ptraceback(py).map(|b| b.into_gil_ref()) @@ -508,12 +498,10 @@ impl PyErr { } /// Deprecated form of [`PyErr::new_type_bound`] - #[cfg_attr( - not(feature = "gil-refs"), - deprecated( - since = "0.21.0", - note = "`PyErr::new_type` will be replaced by `PyErr::new_type_bound` in a future PyO3 version" - ) + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`PyErr::new_type` will be replaced by `PyErr::new_type_bound` in a future PyO3 version" )] pub fn new_type( py: Python<'_>, @@ -636,12 +624,10 @@ impl PyErr { } /// Deprecated form of `PyErr::is_instance_bound`. - #[cfg_attr( - not(feature = "gil-refs"), - deprecated( - since = "0.21.0", - note = "`PyErr::is_instance` will be replaced by `PyErr::is_instance_bound` in a future PyO3 version" - ) + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`PyErr::is_instance` will be replaced by `PyErr::is_instance_bound` in a future PyO3 version" )] #[inline] pub fn is_instance(&self, py: Python<'_>, ty: &PyAny) -> bool { @@ -675,12 +661,10 @@ impl PyErr { } /// Deprecated form of `PyErr::write_unraisable_bound`. - #[cfg_attr( - not(feature = "gil-refs"), - deprecated( - since = "0.21.0", - note = "`PyErr::write_unraisable` will be replaced by `PyErr::write_unraisable_bound` in a future PyO3 version" - ) + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`PyErr::write_unraisable` will be replaced by `PyErr::write_unraisable_bound` in a future PyO3 version" )] #[inline] pub fn write_unraisable(self, py: Python<'_>, obj: Option<&PyAny>) { @@ -722,12 +706,10 @@ impl PyErr { } /// Deprecated form of [`PyErr::warn_bound`]. - #[cfg_attr( - not(feature = "gil-refs"), - deprecated( - since = "0.21.0", - note = "`PyErr::warn` will be replaced by `PyErr::warn_bound` in a future PyO3 version" - ) + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`PyErr::warn` will be replaced by `PyErr::warn_bound` in a future PyO3 version" )] pub fn warn(py: Python<'_>, category: &PyAny, message: &str, stacklevel: i32) -> PyResult<()> { Self::warn_bound(py, &category.as_borrowed(), message, stacklevel) @@ -771,12 +753,10 @@ impl PyErr { } /// Deprecated form of [`PyErr::warn_explicit_bound`]. - #[cfg_attr( - not(feature = "gil-refs"), - deprecated( - since = "0.21.0", - note = "`PyErr::warn_explicit` will be replaced by `PyErr::warn_explicit_bound` in a future PyO3 version" - ) + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`PyErr::warn_explicit` will be replaced by `PyErr::warn_explicit_bound` in a future PyO3 version" )] pub fn warn_explicit( py: Python<'_>, diff --git a/src/exceptions.rs b/src/exceptions.rs index c650d7af079..367022927c5 100644 --- a/src/exceptions.rs +++ b/src/exceptions.rs @@ -21,6 +21,7 @@ macro_rules! impl_exception_boilerplate { ($name: ident) => { // FIXME https://github.com/PyO3/pyo3/issues/3903 #[allow(unknown_lints, non_local_definitions)] + #[cfg(feature = "gil-refs")] impl ::std::convert::From<&$name> for $crate::PyErr { #[inline] fn from(err: &$name) -> $crate::PyErr { @@ -650,12 +651,10 @@ impl_windows_native_exception!( impl PyUnicodeDecodeError { /// Deprecated form of [`PyUnicodeDecodeError::new_bound`]. - #[cfg_attr( - not(feature = "gil-refs"), - deprecated( - since = "0.21.0", - note = "`PyUnicodeDecodeError::new` will be replaced by `PyUnicodeDecodeError::new_bound` in a future PyO3 version" - ) + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`PyUnicodeDecodeError::new` will be replaced by `PyUnicodeDecodeError::new_bound` in a future PyO3 version" )] pub fn new<'p>( py: Python<'p>, @@ -692,12 +691,10 @@ impl PyUnicodeDecodeError { } /// Deprecated form of [`PyUnicodeDecodeError::new_utf8_bound`]. - #[cfg_attr( - not(feature = "gil-refs"), - deprecated( - since = "0.21.0", - note = "`PyUnicodeDecodeError::new_utf8` will be replaced by `PyUnicodeDecodeError::new_utf8_bound` in a future PyO3 version" - ) + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`PyUnicodeDecodeError::new_utf8` will be replaced by `PyUnicodeDecodeError::new_utf8_bound` in a future PyO3 version" )] pub fn new_utf8<'p>( py: Python<'p>, @@ -808,7 +805,7 @@ macro_rules! test_exception { use super::$exc_ty; $crate::Python::with_gil(|py| { - use std::error::Error; + use $crate::types::PyAnyMethods; let err: $crate::PyErr = { None $( @@ -819,13 +816,19 @@ macro_rules! test_exception { assert!(err.is_instance_of::<$exc_ty>(py)); - let value: &$exc_ty = err.value_bound(py).clone().into_gil_ref().downcast().unwrap(); - assert!(value.source().is_none()); + let value = err.value_bound(py).as_any().downcast::<$exc_ty>().unwrap(); - err.set_cause(py, Some($crate::exceptions::PyValueError::new_err("a cause"))); - assert!(value.source().is_some()); + #[cfg(feature = "gil-refs")] + { + use std::error::Error; + let value = value.as_gil_ref(); + assert!(value.source().is_none()); + + err.set_cause(py, Some($crate::exceptions::PyValueError::new_err("a cause"))); + assert!(value.source().is_some()); + } - assert!($crate::PyErr::from(value).is_instance_of::<$exc_ty>(py)); + assert!($crate::PyErr::from(value.clone()).is_instance_of::<$exc_ty>(py)); }) } }; @@ -1068,6 +1071,7 @@ mod tests { } #[test] + #[cfg(feature = "gil-refs")] fn native_exception_chain() { use std::error::Error; diff --git a/tests/test_exceptions.rs b/tests/test_exceptions.rs index ec2fe156b29..e85355fd40e 100644 --- a/tests/test_exceptions.rs +++ b/tests/test_exceptions.rs @@ -100,10 +100,9 @@ fn test_exception_nosegfault() { #[test] #[cfg(Py_3_8)] -#[cfg_attr(not(feature = "gil-refs"), allow(deprecated))] fn test_write_unraisable() { use common::UnraisableCapture; - use pyo3::{exceptions::PyRuntimeError, ffi}; + use pyo3::{exceptions::PyRuntimeError, ffi, types::PyNotImplemented}; Python::with_gil(|py| { let capture = UnraisableCapture::install(py); @@ -111,14 +110,14 @@ fn test_write_unraisable() { assert!(capture.borrow(py).capture.is_none()); let err = PyRuntimeError::new_err("foo"); - err.write_unraisable(py, None); + err.write_unraisable_bound(py, None); let (err, object) = capture.borrow_mut(py).capture.take().unwrap(); assert_eq!(err.to_string(), "RuntimeError: foo"); assert!(object.is_none(py)); let err = PyRuntimeError::new_err("bar"); - err.write_unraisable(py, Some(py.NotImplemented().as_ref(py))); + err.write_unraisable_bound(py, Some(&PyNotImplemented::get_bound(py))); let (err, object) = capture.borrow_mut(py).capture.take().unwrap(); assert_eq!(err.to_string(), "RuntimeError: bar"); From 4616838ee1f89a461ae4560f16e4c1476feac822 Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Tue, 30 Apr 2024 20:53:40 +0200 Subject: [PATCH 042/495] port `PySequence` tests to `Bound` API (#4139) --- src/types/sequence.rs | 116 +++++++++++++++++++++++++----------------- 1 file changed, 69 insertions(+), 47 deletions(-) diff --git a/src/types/sequence.rs b/src/types/sequence.rs index 2b37b6d14f0..afe4a595964 100644 --- a/src/types/sequence.rs +++ b/src/types/sequence.rs @@ -566,15 +566,14 @@ impl<'v> crate::PyTryFrom<'v> for PySequence { } #[cfg(test)] -#[cfg_attr(not(feature = "gil-refs"), allow(deprecated))] mod tests { - use crate::types::{PyList, PySequence, PyTuple}; + use crate::types::{PyAnyMethods, PyList, PySequence, PySequenceMethods, PyTuple}; use crate::{PyObject, Python, ToPyObject}; fn get_object() -> PyObject { // Convenience function for getting a single unique object Python::with_gil(|py| { - let obj = py.eval("object()", None, None).unwrap(); + let obj = py.eval_bound("object()", None, None).unwrap(); obj.to_object(py) }) @@ -584,7 +583,7 @@ mod tests { fn test_numbers_are_not_sequences() { Python::with_gil(|py| { let v = 42i32; - assert!(v.to_object(py).downcast::(py).is_err()); + assert!(v.to_object(py).downcast_bound::(py).is_err()); }); } @@ -592,7 +591,7 @@ mod tests { fn test_strings_are_sequences() { Python::with_gil(|py| { let v = "London Calling"; - assert!(v.to_object(py).downcast::(py).is_ok()); + assert!(v.to_object(py).downcast_bound::(py).is_ok()); }); } @@ -612,7 +611,7 @@ mod tests { Python::with_gil(|py| { let v: Vec = vec![]; let ob = v.to_object(py); - let seq = ob.downcast::(py).unwrap(); + let seq = ob.downcast_bound::(py).unwrap(); assert_eq!(0, seq.len().unwrap()); let needle = 7i32.to_object(py); @@ -624,11 +623,11 @@ mod tests { fn test_seq_is_empty() { Python::with_gil(|py| { let list = vec![1].to_object(py); - let seq = list.downcast::(py).unwrap(); + let seq = list.downcast_bound::(py).unwrap(); assert!(!seq.is_empty().unwrap()); let vec: Vec = Vec::new(); let empty_list = vec.to_object(py); - let empty_seq = empty_list.downcast::(py).unwrap(); + let empty_seq = empty_list.downcast_bound::(py).unwrap(); assert!(empty_seq.is_empty().unwrap()); }); } @@ -638,7 +637,7 @@ mod tests { Python::with_gil(|py| { let v: Vec = vec![1, 1, 2, 3, 5, 8]; let ob = v.to_object(py); - let seq = ob.downcast::(py).unwrap(); + let seq = ob.downcast_bound::(py).unwrap(); assert_eq!(6, seq.len().unwrap()); let bad_needle = 7i32.to_object(py); @@ -657,7 +656,7 @@ mod tests { Python::with_gil(|py| { let v: Vec = vec![1, 1, 2, 3, 5, 8]; let ob = v.to_object(py); - let seq = ob.downcast::(py).unwrap(); + let seq = ob.downcast_bound::(py).unwrap(); assert_eq!(1, seq.get_item(0).unwrap().extract::().unwrap()); assert_eq!(1, seq.get_item(1).unwrap().extract::().unwrap()); assert_eq!(2, seq.get_item(2).unwrap().extract::().unwrap()); @@ -669,6 +668,8 @@ mod tests { } #[test] + #[cfg(feature = "gil-refs")] + #[allow(deprecated)] fn test_seq_index_trait() { Python::with_gil(|py| { let v: Vec = vec![1, 1, 2]; @@ -682,6 +683,8 @@ mod tests { #[test] #[should_panic = "index 7 out of range for sequence"] + #[cfg(feature = "gil-refs")] + #[allow(deprecated)] fn test_seq_index_trait_panic() { Python::with_gil(|py| { let v: Vec = vec![1, 1, 2]; @@ -692,6 +695,8 @@ mod tests { } #[test] + #[cfg(feature = "gil-refs")] + #[allow(deprecated)] fn test_seq_index_trait_ranges() { Python::with_gil(|py| { let v: Vec = vec![1, 1, 2]; @@ -710,6 +715,8 @@ mod tests { #[test] #[should_panic = "range start index 5 out of range for sequence of length 3"] + #[cfg(feature = "gil-refs")] + #[allow(deprecated)] fn test_seq_index_trait_range_panic_start() { Python::with_gil(|py| { let v: Vec = vec![1, 1, 2]; @@ -721,6 +728,8 @@ mod tests { #[test] #[should_panic = "range end index 10 out of range for sequence of length 3"] + #[cfg(feature = "gil-refs")] + #[allow(deprecated)] fn test_seq_index_trait_range_panic_end() { Python::with_gil(|py| { let v: Vec = vec![1, 1, 2]; @@ -732,6 +741,8 @@ mod tests { #[test] #[should_panic = "slice index starts at 2 but ends at 1"] + #[cfg(feature = "gil-refs")] + #[allow(deprecated)] fn test_seq_index_trait_range_panic_wrong_order() { Python::with_gil(|py| { let v: Vec = vec![1, 1, 2]; @@ -744,6 +755,8 @@ mod tests { #[test] #[should_panic = "range start index 8 out of range for sequence of length 3"] + #[cfg(feature = "gil-refs")] + #[allow(deprecated)] fn test_seq_index_trait_range_from_panic() { Python::with_gil(|py| { let v: Vec = vec![1, 1, 2]; @@ -758,19 +771,19 @@ mod tests { Python::with_gil(|py| { let v: Vec = vec![1, 1, 2, 3, 5, 8]; let ob = v.to_object(py); - let seq = ob.downcast::(py).unwrap(); + let seq = ob.downcast_bound::(py).unwrap(); assert!(seq.del_item(10).is_err()); - assert_eq!(1, seq[0].extract::().unwrap()); + assert_eq!(1, seq.get_item(0).unwrap().extract::().unwrap()); assert!(seq.del_item(0).is_ok()); - assert_eq!(1, seq[0].extract::().unwrap()); + assert_eq!(1, seq.get_item(0).unwrap().extract::().unwrap()); assert!(seq.del_item(0).is_ok()); - assert_eq!(2, seq[0].extract::().unwrap()); + assert_eq!(2, seq.get_item(0).unwrap().extract::().unwrap()); assert!(seq.del_item(0).is_ok()); - assert_eq!(3, seq[0].extract::().unwrap()); + assert_eq!(3, seq.get_item(0).unwrap().extract::().unwrap()); assert!(seq.del_item(0).is_ok()); - assert_eq!(5, seq[0].extract::().unwrap()); + assert_eq!(5, seq.get_item(0).unwrap().extract::().unwrap()); assert!(seq.del_item(0).is_ok()); - assert_eq!(8, seq[0].extract::().unwrap()); + assert_eq!(8, seq.get_item(0).unwrap().extract::().unwrap()); assert!(seq.del_item(0).is_ok()); assert_eq!(0, seq.len().unwrap()); assert!(seq.del_item(0).is_err()); @@ -782,10 +795,10 @@ mod tests { Python::with_gil(|py| { let v: Vec = vec![1, 2]; let ob = v.to_object(py); - let seq = ob.downcast::(py).unwrap(); - assert_eq!(2, seq[1].extract::().unwrap()); + let seq = ob.downcast_bound::(py).unwrap(); + assert_eq!(2, seq.get_item(1).unwrap().extract::().unwrap()); assert!(seq.set_item(1, 10).is_ok()); - assert_eq!(10, seq[1].extract::().unwrap()); + assert_eq!(10, seq.get_item(1).unwrap().extract::().unwrap()); }); } @@ -796,9 +809,9 @@ mod tests { Python::with_gil(|py| { let v: Vec = vec![1, 2]; let ob = v.to_object(py); - let seq = ob.downcast::(py).unwrap(); + let seq = ob.downcast_bound::(py).unwrap(); assert!(seq.set_item(1, &obj).is_ok()); - assert!(seq[1].as_ptr() == obj.as_ptr()); + assert!(seq.get_item(1).unwrap().as_ptr() == obj.as_ptr()); }); Python::with_gil(|py| { @@ -811,7 +824,7 @@ mod tests { Python::with_gil(|py| { let v: Vec = vec![1, 1, 2, 3, 5, 8]; let ob = v.to_object(py); - let seq = ob.downcast::(py).unwrap(); + let seq = ob.downcast_bound::(py).unwrap(); assert_eq!( [1, 2, 3], seq.get_slice(1, 4).unwrap().extract::<[i32; 3]>().unwrap() @@ -832,11 +845,11 @@ mod tests { let v: Vec = vec![1, 1, 2, 3, 5, 8]; let w: Vec = vec![7, 4]; let ob = v.to_object(py); - let seq = ob.downcast::(py).unwrap(); + let seq = ob.downcast_bound::(py).unwrap(); let ins = w.to_object(py); - seq.set_slice(1, 4, ins.as_ref(py)).unwrap(); + seq.set_slice(1, 4, ins.bind(py)).unwrap(); assert_eq!([1, 7, 4, 5, 8], seq.extract::<[i32; 5]>().unwrap()); - seq.set_slice(3, 100, PyList::empty(py)).unwrap(); + seq.set_slice(3, 100, &PyList::empty_bound(py)).unwrap(); assert_eq!([1, 7, 4], seq.extract::<[i32; 3]>().unwrap()); }); } @@ -846,7 +859,7 @@ mod tests { Python::with_gil(|py| { let v: Vec = vec![1, 1, 2, 3, 5, 8]; let ob = v.to_object(py); - let seq = ob.downcast::(py).unwrap(); + let seq = ob.downcast_bound::(py).unwrap(); seq.del_slice(1, 4).unwrap(); assert_eq!([1, 5, 8], seq.extract::<[i32; 3]>().unwrap()); seq.del_slice(1, 100).unwrap(); @@ -859,7 +872,7 @@ mod tests { Python::with_gil(|py| { let v: Vec = vec![1, 1, 2, 3, 5, 8]; let ob = v.to_object(py); - let seq = ob.downcast::(py).unwrap(); + let seq = ob.downcast_bound::(py).unwrap(); assert_eq!(0, seq.index(1i32).unwrap()); assert_eq!(2, seq.index(2i32).unwrap()); assert_eq!(3, seq.index(3i32).unwrap()); @@ -875,7 +888,7 @@ mod tests { Python::with_gil(|py| { let v: Vec = vec![1, 1, 2, 3, 5, 8]; let ob = v.to_object(py); - let seq = ob.downcast::(py).unwrap(); + let seq = ob.downcast_bound::(py).unwrap(); assert_eq!(2, seq.count(1i32).unwrap()); assert_eq!(1, seq.count(2i32).unwrap()); assert_eq!(1, seq.count(3i32).unwrap()); @@ -890,7 +903,7 @@ mod tests { Python::with_gil(|py| { let v: Vec = vec![1, 1, 2, 3, 5, 8]; let ob = v.to_object(py); - let seq = ob.downcast::(py).unwrap(); + let seq = ob.downcast_bound::(py).unwrap(); let mut idx = 0; for el in seq.iter().unwrap() { assert_eq!(v[idx], el.unwrap().extract::().unwrap()); @@ -905,7 +918,7 @@ mod tests { Python::with_gil(|py| { let v = vec!["It", "was", "the", "worst", "of", "times"]; let ob = v.to_object(py); - let seq = ob.downcast::(py).unwrap(); + let seq = ob.downcast_bound::(py).unwrap(); let bad_needle = "blurst".to_object(py); assert!(!seq.contains(bad_needle).unwrap()); @@ -920,7 +933,7 @@ mod tests { Python::with_gil(|py| { let v: Vec = vec![1, 2, 3]; let ob = v.to_object(py); - let seq = ob.downcast::(py).unwrap(); + let seq = ob.downcast_bound::(py).unwrap(); let concat_seq = seq.concat(seq).unwrap(); assert_eq!(6, concat_seq.len().unwrap()); let concat_v: Vec = vec![1, 2, 3, 1, 2, 3]; @@ -935,7 +948,7 @@ mod tests { Python::with_gil(|py| { let v = "string"; let ob = v.to_object(py); - let seq = ob.downcast::(py).unwrap(); + let seq = ob.downcast_bound::(py).unwrap(); let concat_seq = seq.concat(seq).unwrap(); assert_eq!(12, concat_seq.len().unwrap()); let concat_v = "stringstring".to_owned(); @@ -950,7 +963,7 @@ mod tests { Python::with_gil(|py| { let v = vec!["foo", "bar"]; let ob = v.to_object(py); - let seq = ob.downcast::(py).unwrap(); + let seq = ob.downcast_bound::(py).unwrap(); let repeat_seq = seq.repeat(3).unwrap(); assert_eq!(6, repeat_seq.len().unwrap()); let repeated = ["foo", "bar", "foo", "bar", "foo", "bar"]; @@ -965,14 +978,14 @@ mod tests { Python::with_gil(|py| { let v = vec!["foo", "bar"]; let ob = v.to_object(py); - let seq = ob.downcast::(py).unwrap(); + let seq = ob.downcast_bound::(py).unwrap(); let rep_seq = seq.in_place_repeat(3).unwrap(); assert_eq!(6, seq.len().unwrap()); - assert!(seq.is(rep_seq)); + assert!(seq.is(&rep_seq)); let conc_seq = seq.in_place_concat(seq).unwrap(); assert_eq!(12, seq.len().unwrap()); - assert!(seq.is(conc_seq)); + assert!(seq.is(&conc_seq)); }); } @@ -981,8 +994,12 @@ mod tests { Python::with_gil(|py| { let v = vec!["foo", "bar"]; let ob = v.to_object(py); - let seq = ob.downcast::(py).unwrap(); - assert!(seq.to_list().unwrap().eq(PyList::new(py, &v)).unwrap()); + let seq = ob.downcast_bound::(py).unwrap(); + assert!(seq + .to_list() + .unwrap() + .eq(PyList::new_bound(py, &v)) + .unwrap()); }); } @@ -991,11 +1008,11 @@ mod tests { Python::with_gil(|py| { let v = "foo"; let ob = v.to_object(py); - let seq: &PySequence = ob.downcast(py).unwrap(); + let seq = ob.downcast_bound::(py).unwrap(); assert!(seq .to_list() .unwrap() - .eq(PyList::new(py, ["f", "o", "o"])) + .eq(PyList::new_bound(py, ["f", "o", "o"])) .unwrap()); }); } @@ -1005,7 +1022,7 @@ mod tests { Python::with_gil(|py| { let v = ("foo", "bar"); let ob = v.to_object(py); - let seq = ob.downcast::(py).unwrap(); + let seq = ob.downcast_bound::(py).unwrap(); assert!(seq .to_tuple() .unwrap() @@ -1019,7 +1036,7 @@ mod tests { Python::with_gil(|py| { let v = vec!["foo", "bar"]; let ob = v.to_object(py); - let seq = ob.downcast::(py).unwrap(); + let seq = ob.downcast_bound::(py).unwrap(); assert!(seq .to_tuple() .unwrap() @@ -1031,7 +1048,11 @@ mod tests { #[test] fn test_extract_tuple_to_vec() { Python::with_gil(|py| { - let v: Vec = py.eval("(1, 2)", None, None).unwrap().extract().unwrap(); + let v: Vec = py + .eval_bound("(1, 2)", None, None) + .unwrap() + .extract() + .unwrap(); assert!(v == [1, 2]); }); } @@ -1040,7 +1061,7 @@ mod tests { fn test_extract_range_to_vec() { Python::with_gil(|py| { let v: Vec = py - .eval("range(1, 5)", None, None) + .eval_bound("range(1, 5)", None, None) .unwrap() .extract() .unwrap(); @@ -1052,7 +1073,7 @@ mod tests { fn test_extract_bytearray_to_vec() { Python::with_gil(|py| { let v: Vec = py - .eval("bytearray(b'abc')", None, None) + .eval_bound("bytearray(b'abc')", None, None) .unwrap() .extract() .unwrap(); @@ -1065,7 +1086,7 @@ mod tests { Python::with_gil(|py| { let v = vec!["foo", "bar"]; let ob = v.to_object(py); - let seq = ob.downcast::(py).unwrap(); + let seq = ob.downcast_bound::(py).unwrap(); let type_ptr = seq.as_ref(); let seq_from = unsafe { type_ptr.downcast_unchecked::() }; assert!(seq_from.to_list().is_ok()); @@ -1073,6 +1094,7 @@ mod tests { } #[test] + #[cfg(feature = "gil-refs")] #[allow(deprecated)] fn test_seq_try_from() { use crate::PyTryFrom; From 82c00a2fe4bcb732cce075b3416024447755c746 Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Tue, 30 Apr 2024 23:49:00 +0200 Subject: [PATCH 043/495] port `PyAny` tests to `Bound` API (#4140) --- src/types/any.rs | 75 +++++++++++++++++++++++------------------------- 1 file changed, 36 insertions(+), 39 deletions(-) diff --git a/src/types/any.rs b/src/types/any.rs index 777a8dcb4c3..8b0bac44ca6 100644 --- a/src/types/any.rs +++ b/src/types/any.rs @@ -2324,12 +2324,11 @@ impl<'py> Bound<'py, PyAny> { } #[cfg(test)] -#[cfg_attr(not(feature = "gil-refs"), allow(deprecated))] mod tests { use crate::{ basic::CompareOp, - types::{any::PyAnyMethods, IntoPyDict, PyAny, PyBool, PyList, PyLong, PyModule}, - Bound, PyNativeType, PyTypeInfo, Python, ToPyObject, + types::{IntoPyDict, PyAny, PyAnyMethods, PyBool, PyList, PyLong, PyModule, PyTypeMethods}, + Bound, PyTypeInfo, Python, ToPyObject, }; #[test] @@ -2407,7 +2406,7 @@ class NonHeapNonDescriptorInt: #[test] fn test_call_for_non_existing_method() { Python::with_gil(|py| { - let a = py.eval("42", None, None).unwrap(); + let a = py.eval_bound("42", None, None).unwrap(); a.call_method0("__str__").unwrap(); // ok assert!(a.call_method("nonexistent_method", (1,), None).is_err()); assert!(a.call_method0("nonexistent_method").is_err()); @@ -2455,7 +2454,7 @@ class SimpleClass: #[test] fn test_type() { Python::with_gil(|py| { - let obj = py.eval("42", None, None).unwrap(); + let obj = py.eval_bound("42", None, None).unwrap(); assert_eq!(obj.get_type().as_type_ptr(), obj.get_type_ptr()); }); } @@ -2463,11 +2462,11 @@ class SimpleClass: #[test] fn test_dir() { Python::with_gil(|py| { - let obj = py.eval("42", None, None).unwrap(); + let obj = py.eval_bound("42", None, None).unwrap(); let dir = py - .eval("dir(42)", None, None) + .eval_bound("dir(42)", None, None) .unwrap() - .downcast::() + .downcast_into::() .unwrap(); let a = obj .dir() @@ -2482,7 +2481,7 @@ class SimpleClass: #[test] fn test_hasattr() { Python::with_gil(|py| { - let x = 5.to_object(py).into_ref(py); + let x = 5.to_object(py).into_bound(py); assert!(x.is_instance_of::()); assert!(x.hasattr("to_bytes").unwrap()); @@ -2509,7 +2508,7 @@ class SimpleClass: Python::with_gil(|py| { let obj = Py::new(py, GetattrFail).unwrap(); - let obj = obj.as_ref(py).as_ref(); + let obj = obj.bind(py).as_ref(); assert!(obj .hasattr("foo") @@ -2521,18 +2520,18 @@ class SimpleClass: #[test] fn test_nan_eq() { Python::with_gil(|py| { - let nan = py.eval("float('nan')", None, None).unwrap(); - assert!(nan.compare(nan).is_err()); + let nan = py.eval_bound("float('nan')", None, None).unwrap(); + assert!(nan.compare(&nan).is_err()); }); } #[test] fn test_any_is_instance_of() { Python::with_gil(|py| { - let x = 5.to_object(py).into_ref(py); + let x = 5.to_object(py).into_bound(py); assert!(x.is_instance_of::()); - let l = vec![x, x].to_object(py).into_ref(py); + let l = vec![&x, &x].to_object(py).into_bound(py); assert!(l.is_instance_of::()); }); } @@ -2540,15 +2539,15 @@ class SimpleClass: #[test] fn test_any_is_instance() { Python::with_gil(|py| { - let l = vec![1u8, 2].to_object(py).into_ref(py); - assert!(l.is_instance(py.get_type::()).unwrap()); + let l = vec![1u8, 2].to_object(py).into_bound(py); + assert!(l.is_instance(&py.get_type_bound::()).unwrap()); }); } #[test] fn test_any_is_exact_instance_of() { Python::with_gil(|py| { - let x = 5.to_object(py).into_ref(py); + let x = 5.to_object(py).into_bound(py); assert!(x.is_exact_instance_of::()); let t = PyBool::new_bound(py, true); @@ -2556,7 +2555,7 @@ class SimpleClass: assert!(!t.is_exact_instance_of::()); assert!(t.is_exact_instance_of::()); - let l = vec![x, x].to_object(py).into_ref(py); + let l = vec![&x, &x].to_object(py).into_bound(py); assert!(l.is_exact_instance_of::()); }); } @@ -2565,11 +2564,9 @@ class SimpleClass: fn test_any_is_exact_instance() { Python::with_gil(|py| { let t = PyBool::new_bound(py, true); - assert!(t - .is_instance(&py.get_type::().as_borrowed()) - .unwrap()); - assert!(!t.is_exact_instance(&py.get_type::().as_borrowed())); - assert!(t.is_exact_instance(&py.get_type::().as_borrowed())); + assert!(t.is_instance(&py.get_type_bound::()).unwrap()); + assert!(!t.is_exact_instance(&py.get_type_bound::())); + assert!(t.is_exact_instance(&py.get_type_bound::())); }); } @@ -2577,7 +2574,7 @@ class SimpleClass: fn test_any_contains() { Python::with_gil(|py| { let v: Vec = vec![1, 1, 2, 3, 5, 8]; - let ob = v.to_object(py).into_ref(py); + let ob = v.to_object(py).into_bound(py); let bad_needle = 7i32.to_object(py); assert!(!ob.contains(&bad_needle).unwrap()); @@ -2589,7 +2586,7 @@ class SimpleClass: assert!(ob.contains(&type_coerced_needle).unwrap()); let n: u32 = 42; - let bad_haystack = n.to_object(py).into_ref(py); + let bad_haystack = n.to_object(py).into_bound(py); let irrelevant_needle = 0i32.to_object(py); assert!(bad_haystack.contains(&irrelevant_needle).is_err()); }); @@ -2603,12 +2600,12 @@ class SimpleClass: Python::with_gil(|py| { for a in list { for b in list { - let a_py = a.to_object(py).into_ref(py); - let b_py = b.to_object(py).into_ref(py); + let a_py = a.to_object(py).into_bound(py); + let b_py = b.to_object(py).into_bound(py); assert_eq!( a.lt(b), - a_py.lt(b_py).unwrap(), + a_py.lt(&b_py).unwrap(), "{} < {} should be {}.", a_py, b_py, @@ -2616,7 +2613,7 @@ class SimpleClass: ); assert_eq!( a.le(b), - a_py.le(b_py).unwrap(), + a_py.le(&b_py).unwrap(), "{} <= {} should be {}.", a_py, b_py, @@ -2624,7 +2621,7 @@ class SimpleClass: ); assert_eq!( a.eq(b), - a_py.eq(b_py).unwrap(), + a_py.eq(&b_py).unwrap(), "{} == {} should be {}.", a_py, b_py, @@ -2632,7 +2629,7 @@ class SimpleClass: ); assert_eq!( a.ne(b), - a_py.ne(b_py).unwrap(), + a_py.ne(&b_py).unwrap(), "{} != {} should be {}.", a_py, b_py, @@ -2640,7 +2637,7 @@ class SimpleClass: ); assert_eq!( a.gt(b), - a_py.gt(b_py).unwrap(), + a_py.gt(&b_py).unwrap(), "{} > {} should be {}.", a_py, b_py, @@ -2648,7 +2645,7 @@ class SimpleClass: ); assert_eq!( a.ge(b), - a_py.ge(b_py).unwrap(), + a_py.ge(&b_py).unwrap(), "{} >= {} should be {}.", a_py, b_py, @@ -2695,10 +2692,10 @@ class SimpleClass: #[test] fn test_rich_compare_type_error() { Python::with_gil(|py| { - let py_int = 1.to_object(py).into_ref(py); - let py_str = "1".to_object(py).into_ref(py); + let py_int = 1.to_object(py).into_bound(py); + let py_str = "1".to_object(py).into_bound(py); - assert!(py_int.rich_compare(py_str, CompareOp::Lt).is_err()); + assert!(py_int.rich_compare(&py_str, CompareOp::Lt).is_err()); assert!(!py_int .rich_compare(py_str, CompareOp::Eq) .unwrap() @@ -2736,13 +2733,13 @@ class SimpleClass: #[test] fn test_is_empty() { Python::with_gil(|py| { - let empty_list: &PyAny = PyList::empty(py); + let empty_list = PyList::empty_bound(py).into_any(); assert!(empty_list.is_empty().unwrap()); - let list: &PyAny = PyList::new(py, vec![1, 2, 3]); + let list = PyList::new_bound(py, vec![1, 2, 3]).into_any(); assert!(!list.is_empty().unwrap()); - let not_container = 5.to_object(py).into_ref(py); + let not_container = 5.to_object(py).into_bound(py); assert!(not_container.is_empty().is_err()); }); } From 2f3a33fda11eb06783378db39ef3515dbb618202 Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Wed, 1 May 2024 00:00:31 +0200 Subject: [PATCH 044/495] feature gate deprecated APIs for `PyList` (#4127) --- guide/src/migration.md | 4 +- guide/src/types.md | 4 + src/types/list.rs | 190 +++++++++++++++++++++-------------------- 3 files changed, 104 insertions(+), 94 deletions(-) diff --git a/guide/src/migration.md b/guide/src/migration.md index 9c7bc15002e..ad39adfa0e8 100644 --- a/guide/src/migration.md +++ b/guide/src/migration.md @@ -75,7 +75,7 @@ Python::with_gil(|py| { After: -```rust +```rust,ignore # use pyo3::prelude::*; # use pyo3::types::{PyInt, PyList}; # fn main() -> PyResult<()> { @@ -1089,7 +1089,7 @@ An additional advantage of using Rust's indexing conventions for these types is that these types can now also support Rust's indexing operators as part of a consistent API: -```rust +```rust,ignore #![allow(deprecated)] use pyo3::{Python, types::PyList}; diff --git a/guide/src/types.md b/guide/src/types.md index 4c63d175991..82040de2c43 100644 --- a/guide/src/types.md +++ b/guide/src/types.md @@ -330,8 +330,10 @@ For a `&PyAny` object reference `any` where the underlying object is a Python-na a list: ```rust +# #![allow(unused_imports)] # use pyo3::prelude::*; # use pyo3::types::PyList; +# #[cfg(feature = "gil-refs")] # Python::with_gil(|py| -> PyResult<()> { #[allow(deprecated)] // PyList::empty is part of the deprecated "GIL Refs" API. let obj: &PyAny = PyList::empty(py); @@ -390,8 +392,10 @@ To see all Python types exposed by `PyO3` consult the [`pyo3::types`][pyo3::type **Conversions:** ```rust +# #![allow(unused_imports)] # use pyo3::prelude::*; # use pyo3::types::PyList; +# #[cfg(feature = "gil-refs")] # Python::with_gil(|py| -> PyResult<()> { #[allow(deprecated)] // PyList::empty is part of the deprecated "GIL Refs" API. let list = PyList::empty(py); diff --git a/src/types/list.rs b/src/types/list.rs index 19dbe59510f..56f21feb133 100644 --- a/src/types/list.rs +++ b/src/types/list.rs @@ -58,12 +58,10 @@ impl PyList { /// Deprecated form of [`PyList::new_bound`]. #[inline] #[track_caller] - #[cfg_attr( - not(feature = "gil-refs"), - deprecated( - since = "0.21.0", - note = "`PyList::new` will be replaced by `PyList::new_bound` in a future PyO3 version" - ) + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`PyList::new` will be replaced by `PyList::new_bound` in a future PyO3 version" )] pub fn new(py: Python<'_>, elements: impl IntoIterator) -> &PyList where @@ -113,12 +111,10 @@ impl PyList { /// Deprecated form of [`PyList::empty_bound`]. #[inline] - #[cfg_attr( - not(feature = "gil-refs"), - deprecated( - since = "0.21.0", - note = "`PyList::empty` will be replaced by `PyList::empty_bound` in a future PyO3 version" - ) + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`PyList::empty` will be replaced by `PyList::empty_bound` in a future PyO3 version" )] pub fn empty(py: Python<'_>) -> &PyList { Self::empty_bound(py).into_gil_ref() @@ -721,7 +717,6 @@ impl<'py> IntoIterator for &Bound<'py, PyList> { } #[cfg(test)] -#[cfg_attr(not(feature = "gil-refs"), allow(deprecated))] mod tests { use crate::types::any::PyAnyMethods; use crate::types::list::PyListMethods; @@ -733,18 +728,18 @@ mod tests { #[test] fn test_new() { Python::with_gil(|py| { - let list = PyList::new(py, [2, 3, 5, 7]); - assert_eq!(2, list[0].extract::().unwrap()); - assert_eq!(3, list[1].extract::().unwrap()); - assert_eq!(5, list[2].extract::().unwrap()); - assert_eq!(7, list[3].extract::().unwrap()); + let list = PyList::new_bound(py, [2, 3, 5, 7]); + assert_eq!(2, list.get_item(0).unwrap().extract::().unwrap()); + assert_eq!(3, list.get_item(1).unwrap().extract::().unwrap()); + assert_eq!(5, list.get_item(2).unwrap().extract::().unwrap()); + assert_eq!(7, list.get_item(3).unwrap().extract::().unwrap()); }); } #[test] fn test_len() { Python::with_gil(|py| { - let list = PyList::new(py, [1, 2, 3, 4]); + let list = PyList::new_bound(py, [1, 2, 3, 4]); assert_eq!(4, list.len()); }); } @@ -752,7 +747,7 @@ mod tests { #[test] fn test_get_item() { Python::with_gil(|py| { - let list = PyList::new(py, [2, 3, 5, 7]); + let list = PyList::new_bound(py, [2, 3, 5, 7]); assert_eq!(2, list.get_item(0).unwrap().extract::().unwrap()); assert_eq!(3, list.get_item(1).unwrap().extract::().unwrap()); assert_eq!(5, list.get_item(2).unwrap().extract::().unwrap()); @@ -763,7 +758,7 @@ mod tests { #[test] fn test_get_slice() { Python::with_gil(|py| { - let list = PyList::new(py, [2, 3, 5, 7]); + let list = PyList::new_bound(py, [2, 3, 5, 7]); let slice = list.get_slice(1, 3); assert_eq!(2, slice.len()); let slice = list.get_slice(1, 7); @@ -774,12 +769,12 @@ mod tests { #[test] fn test_set_item() { Python::with_gil(|py| { - let list = PyList::new(py, [2, 3, 5, 7]); + let list = PyList::new_bound(py, [2, 3, 5, 7]); let val = 42i32.to_object(py); let val2 = 42i32.to_object(py); - assert_eq!(2, list[0].extract::().unwrap()); + assert_eq!(2, list.get_item(0).unwrap().extract::().unwrap()); list.set_item(0, val).unwrap(); - assert_eq!(42, list[0].extract::().unwrap()); + assert_eq!(42, list.get_item(0).unwrap().extract::().unwrap()); assert!(list.set_item(10, val2).is_err()); }); } @@ -787,15 +782,14 @@ mod tests { #[test] fn test_set_item_refcnt() { Python::with_gil(|py| { - let obj = py.eval("object()", None, None).unwrap(); + let obj = py.eval_bound("object()", None, None).unwrap(); let cnt; { - let _pool = unsafe { crate::GILPool::new() }; let v = vec![2]; let ob = v.to_object(py); - let list: &PyList = ob.downcast(py).unwrap(); + let list = ob.downcast_bound::(py).unwrap(); cnt = obj.get_refcnt(); - list.set_item(0, obj).unwrap(); + list.set_item(0, &obj).unwrap(); } assert_eq!(cnt, obj.get_refcnt()); @@ -805,17 +799,17 @@ mod tests { #[test] fn test_insert() { Python::with_gil(|py| { - let list = PyList::new(py, [2, 3, 5, 7]); + let list = PyList::new_bound(py, [2, 3, 5, 7]); let val = 42i32.to_object(py); let val2 = 43i32.to_object(py); assert_eq!(4, list.len()); - assert_eq!(2, list[0].extract::().unwrap()); + assert_eq!(2, list.get_item(0).unwrap().extract::().unwrap()); list.insert(0, val).unwrap(); list.insert(1000, val2).unwrap(); assert_eq!(6, list.len()); - assert_eq!(42, list[0].extract::().unwrap()); - assert_eq!(2, list[1].extract::().unwrap()); - assert_eq!(43, list[5].extract::().unwrap()); + assert_eq!(42, list.get_item(0).unwrap().extract::().unwrap()); + assert_eq!(2, list.get_item(1).unwrap().extract::().unwrap()); + assert_eq!(43, list.get_item(5).unwrap().extract::().unwrap()); }); } @@ -823,12 +817,11 @@ mod tests { fn test_insert_refcnt() { Python::with_gil(|py| { let cnt; - let obj = py.eval("object()", None, None).unwrap(); + let obj = py.eval_bound("object()", None, None).unwrap(); { - let _pool = unsafe { crate::GILPool::new() }; - let list = PyList::empty(py); + let list = PyList::empty_bound(py); cnt = obj.get_refcnt(); - list.insert(0, obj).unwrap(); + list.insert(0, &obj).unwrap(); } assert_eq!(cnt, obj.get_refcnt()); @@ -838,10 +831,10 @@ mod tests { #[test] fn test_append() { Python::with_gil(|py| { - let list = PyList::new(py, [2]); + let list = PyList::new_bound(py, [2]); list.append(3).unwrap(); - assert_eq!(2, list[0].extract::().unwrap()); - assert_eq!(3, list[1].extract::().unwrap()); + assert_eq!(2, list.get_item(0).unwrap().extract::().unwrap()); + assert_eq!(3, list.get_item(1).unwrap().extract::().unwrap()); }); } @@ -849,12 +842,11 @@ mod tests { fn test_append_refcnt() { Python::with_gil(|py| { let cnt; - let obj = py.eval("object()", None, None).unwrap(); + let obj = py.eval_bound("object()", None, None).unwrap(); { - let _pool = unsafe { crate::GILPool::new() }; - let list = PyList::empty(py); + let list = PyList::empty_bound(py); cnt = obj.get_refcnt(); - list.append(obj).unwrap(); + list.append(&obj).unwrap(); } assert_eq!(cnt, obj.get_refcnt()); }); @@ -864,7 +856,7 @@ mod tests { fn test_iter() { Python::with_gil(|py| { let v = vec![2, 3, 5, 7]; - let list = PyList::new(py, &v); + let list = PyList::new_bound(py, &v); let mut idx = 0; for el in list { assert_eq!(v[idx], el.extract::().unwrap()); @@ -879,7 +871,7 @@ mod tests { Python::with_gil(|py| { let v = vec![2, 3, 5, 7]; let ob = v.to_object(py); - let list: &PyList = ob.downcast(py).unwrap(); + let list = ob.downcast_bound::(py).unwrap(); let mut iter = list.iter(); assert_eq!(iter.size_hint(), (v.len(), Some(v.len()))); @@ -898,7 +890,7 @@ mod tests { Python::with_gil(|py| { let v = vec![2, 3, 5, 7]; let ob = v.to_object(py); - let list: &PyList = ob.downcast(py).unwrap(); + let list = ob.downcast_bound::(py).unwrap(); let mut iter = list.iter().rev(); @@ -924,7 +916,7 @@ mod tests { #[test] fn test_into_iter() { Python::with_gil(|py| { - let list = PyList::new(py, [1, 2, 3, 4]); + let list = PyList::new_bound(py, [1, 2, 3, 4]); for (i, item) in list.iter().enumerate() { assert_eq!((i + 1) as i32, item.extract::().unwrap()); } @@ -978,7 +970,7 @@ mod tests { fn test_extract() { Python::with_gil(|py| { let v = vec![2, 3, 5, 7]; - let list = PyList::new(py, &v); + let list = PyList::new_bound(py, &v); let v2 = list.as_ref().extract::>().unwrap(); assert_eq!(v, v2); }); @@ -988,16 +980,16 @@ mod tests { fn test_sort() { Python::with_gil(|py| { let v = vec![7, 3, 2, 5]; - let list = PyList::new(py, &v); - assert_eq!(7, list[0].extract::().unwrap()); - assert_eq!(3, list[1].extract::().unwrap()); - assert_eq!(2, list[2].extract::().unwrap()); - assert_eq!(5, list[3].extract::().unwrap()); + let list = PyList::new_bound(py, &v); + assert_eq!(7, list.get_item(0).unwrap().extract::().unwrap()); + assert_eq!(3, list.get_item(1).unwrap().extract::().unwrap()); + assert_eq!(2, list.get_item(2).unwrap().extract::().unwrap()); + assert_eq!(5, list.get_item(3).unwrap().extract::().unwrap()); list.sort().unwrap(); - assert_eq!(2, list[0].extract::().unwrap()); - assert_eq!(3, list[1].extract::().unwrap()); - assert_eq!(5, list[2].extract::().unwrap()); - assert_eq!(7, list[3].extract::().unwrap()); + assert_eq!(2, list.get_item(0).unwrap().extract::().unwrap()); + assert_eq!(3, list.get_item(1).unwrap().extract::().unwrap()); + assert_eq!(5, list.get_item(2).unwrap().extract::().unwrap()); + assert_eq!(7, list.get_item(3).unwrap().extract::().unwrap()); }); } @@ -1005,16 +997,16 @@ mod tests { fn test_reverse() { Python::with_gil(|py| { let v = vec![2, 3, 5, 7]; - let list = PyList::new(py, &v); - assert_eq!(2, list[0].extract::().unwrap()); - assert_eq!(3, list[1].extract::().unwrap()); - assert_eq!(5, list[2].extract::().unwrap()); - assert_eq!(7, list[3].extract::().unwrap()); + let list = PyList::new_bound(py, &v); + assert_eq!(2, list.get_item(0).unwrap().extract::().unwrap()); + assert_eq!(3, list.get_item(1).unwrap().extract::().unwrap()); + assert_eq!(5, list.get_item(2).unwrap().extract::().unwrap()); + assert_eq!(7, list.get_item(3).unwrap().extract::().unwrap()); list.reverse().unwrap(); - assert_eq!(7, list[0].extract::().unwrap()); - assert_eq!(5, list[1].extract::().unwrap()); - assert_eq!(3, list[2].extract::().unwrap()); - assert_eq!(2, list[3].extract::().unwrap()); + assert_eq!(7, list.get_item(0).unwrap().extract::().unwrap()); + assert_eq!(5, list.get_item(1).unwrap().extract::().unwrap()); + assert_eq!(3, list.get_item(2).unwrap().extract::().unwrap()); + assert_eq!(2, list.get_item(3).unwrap().extract::().unwrap()); }); } @@ -1022,16 +1014,16 @@ mod tests { fn test_array_into_py() { Python::with_gil(|py| { let array: PyObject = [1, 2].into_py(py); - let list: &PyList = array.downcast(py).unwrap(); - assert_eq!(1, list[0].extract::().unwrap()); - assert_eq!(2, list[1].extract::().unwrap()); + let list = array.downcast_bound::(py).unwrap(); + assert_eq!(1, list.get_item(0).unwrap().extract::().unwrap()); + assert_eq!(2, list.get_item(1).unwrap().extract::().unwrap()); }); } #[test] fn test_list_get_item_invalid_index() { Python::with_gil(|py| { - let list = PyList::new(py, [2, 3, 5, 7]); + let list = PyList::new_bound(py, [2, 3, 5, 7]); let obj = list.get_item(5); assert!(obj.is_err()); assert_eq!( @@ -1044,7 +1036,7 @@ mod tests { #[test] fn test_list_get_item_sanity() { Python::with_gil(|py| { - let list = PyList::new(py, [2, 3, 5, 7]); + let list = PyList::new_bound(py, [2, 3, 5, 7]); let obj = list.get_item(0); assert_eq!(obj.unwrap().extract::().unwrap(), 2); }); @@ -1054,13 +1046,15 @@ mod tests { #[test] fn test_list_get_item_unchecked_sanity() { Python::with_gil(|py| { - let list = PyList::new(py, [2, 3, 5, 7]); + let list = PyList::new_bound(py, [2, 3, 5, 7]); let obj = unsafe { list.get_item_unchecked(0) }; assert_eq!(obj.extract::().unwrap(), 2); }); } #[test] + #[cfg(feature = "gil-refs")] + #[allow(deprecated)] fn test_list_index_trait() { Python::with_gil(|py| { let list = PyList::new(py, [2, 3, 5]); @@ -1072,6 +1066,8 @@ mod tests { #[test] #[should_panic] + #[cfg(feature = "gil-refs")] + #[allow(deprecated)] fn test_list_index_trait_panic() { Python::with_gil(|py| { let list = PyList::new(py, [2, 3, 5]); @@ -1080,6 +1076,8 @@ mod tests { } #[test] + #[cfg(feature = "gil-refs")] + #[allow(deprecated)] fn test_list_index_trait_ranges() { Python::with_gil(|py| { let list = PyList::new(py, [2, 3, 5]); @@ -1096,6 +1094,8 @@ mod tests { #[test] #[should_panic = "range start index 5 out of range for list of length 3"] + #[cfg(feature = "gil-refs")] + #[allow(deprecated)] fn test_list_index_trait_range_panic_start() { Python::with_gil(|py| { let list = PyList::new(py, [2, 3, 5]); @@ -1105,6 +1105,8 @@ mod tests { #[test] #[should_panic = "range end index 10 out of range for list of length 3"] + #[cfg(feature = "gil-refs")] + #[allow(deprecated)] fn test_list_index_trait_range_panic_end() { Python::with_gil(|py| { let list = PyList::new(py, [2, 3, 5]); @@ -1114,6 +1116,8 @@ mod tests { #[test] #[should_panic = "slice index starts at 2 but ends at 1"] + #[cfg(feature = "gil-refs")] + #[allow(deprecated)] fn test_list_index_trait_range_panic_wrong_order() { Python::with_gil(|py| { let list = PyList::new(py, [2, 3, 5]); @@ -1124,6 +1128,8 @@ mod tests { #[test] #[should_panic = "range start index 8 out of range for list of length 3"] + #[cfg(feature = "gil-refs")] + #[allow(deprecated)] fn test_list_index_trait_range_from_panic() { Python::with_gil(|py| { let list = PyList::new(py, [2, 3, 5]); @@ -1134,19 +1140,19 @@ mod tests { #[test] fn test_list_del_item() { Python::with_gil(|py| { - let list = PyList::new(py, [1, 1, 2, 3, 5, 8]); + let list = PyList::new_bound(py, [1, 1, 2, 3, 5, 8]); assert!(list.del_item(10).is_err()); - assert_eq!(1, list[0].extract::().unwrap()); + assert_eq!(1, list.get_item(0).unwrap().extract::().unwrap()); assert!(list.del_item(0).is_ok()); - assert_eq!(1, list[0].extract::().unwrap()); + assert_eq!(1, list.get_item(0).unwrap().extract::().unwrap()); assert!(list.del_item(0).is_ok()); - assert_eq!(2, list[0].extract::().unwrap()); + assert_eq!(2, list.get_item(0).unwrap().extract::().unwrap()); assert!(list.del_item(0).is_ok()); - assert_eq!(3, list[0].extract::().unwrap()); + assert_eq!(3, list.get_item(0).unwrap().extract::().unwrap()); assert!(list.del_item(0).is_ok()); - assert_eq!(5, list[0].extract::().unwrap()); + assert_eq!(5, list.get_item(0).unwrap().extract::().unwrap()); assert!(list.del_item(0).is_ok()); - assert_eq!(8, list[0].extract::().unwrap()); + assert_eq!(8, list.get_item(0).unwrap().extract::().unwrap()); assert!(list.del_item(0).is_ok()); assert_eq!(0, list.len()); assert!(list.del_item(0).is_err()); @@ -1156,11 +1162,11 @@ mod tests { #[test] fn test_list_set_slice() { Python::with_gil(|py| { - let list = PyList::new(py, [1, 1, 2, 3, 5, 8]); - let ins = PyList::new(py, [7, 4]); - list.set_slice(1, 4, ins).unwrap(); + let list = PyList::new_bound(py, [1, 1, 2, 3, 5, 8]); + let ins = PyList::new_bound(py, [7, 4]); + list.set_slice(1, 4, &ins).unwrap(); assert_eq!([1, 7, 4, 5, 8], list.extract::<[i32; 5]>().unwrap()); - list.set_slice(3, 100, PyList::empty(py)).unwrap(); + list.set_slice(3, 100, &PyList::empty_bound(py)).unwrap(); assert_eq!([1, 7, 4], list.extract::<[i32; 3]>().unwrap()); }); } @@ -1168,7 +1174,7 @@ mod tests { #[test] fn test_list_del_slice() { Python::with_gil(|py| { - let list = PyList::new(py, [1, 1, 2, 3, 5, 8]); + let list = PyList::new_bound(py, [1, 1, 2, 3, 5, 8]); list.del_slice(1, 4).unwrap(); assert_eq!([1, 5, 8], list.extract::<[i32; 3]>().unwrap()); list.del_slice(1, 100).unwrap(); @@ -1179,7 +1185,7 @@ mod tests { #[test] fn test_list_contains() { Python::with_gil(|py| { - let list = PyList::new(py, [1, 1, 2, 3, 5, 8]); + let list = PyList::new_bound(py, [1, 1, 2, 3, 5, 8]); assert_eq!(6, list.len()); let bad_needle = 7i32.to_object(py); @@ -1196,7 +1202,7 @@ mod tests { #[test] fn test_list_index() { Python::with_gil(|py| { - let list = PyList::new(py, [1, 1, 2, 3, 5, 8]); + let list = PyList::new_bound(py, [1, 1, 2, 3, 5, 8]); assert_eq!(0, list.index(1i32).unwrap()); assert_eq!(2, list.index(2i32).unwrap()); assert_eq!(3, list.index(3i32).unwrap()); @@ -1233,7 +1239,7 @@ mod tests { fn too_long_iterator() { Python::with_gil(|py| { let iter = FaultyIter(0..usize::MAX, 73); - let _list = PyList::new(py, iter); + let _list = PyList::new_bound(py, iter); }) } @@ -1244,7 +1250,7 @@ mod tests { fn too_short_iterator() { Python::with_gil(|py| { let iter = FaultyIter(0..35, 73); - let _list = PyList::new(py, iter); + let _list = PyList::new_bound(py, iter); }) } @@ -1256,7 +1262,7 @@ mod tests { Python::with_gil(|py| { let iter = FaultyIter(0..0, usize::MAX); - let _list = PyList::new(py, iter); + let _list = PyList::new_bound(py, iter); }) } @@ -1315,7 +1321,7 @@ mod tests { Python::with_gil(|py| { std::panic::catch_unwind(|| { let iter = FaultyIter(0..50, 50); - let _list = PyList::new(py, iter); + let _list = PyList::new_bound(py, iter); }) .unwrap_err(); }); @@ -1330,7 +1336,7 @@ mod tests { #[test] fn test_list_to_tuple() { Python::with_gil(|py| { - let list = PyList::new(py, vec![1, 2, 3]); + let list = PyList::new_bound(py, vec![1, 2, 3]); let tuple = list.to_tuple(); let tuple_expected = PyTuple::new_bound(py, vec![1, 2, 3]); assert!(tuple.eq(tuple_expected).unwrap()); From 261d27d1973caf1b159b10cdc2583f121fbe08a5 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Tue, 30 Apr 2024 19:55:43 -0400 Subject: [PATCH 045/495] feature gate deprecated APIs for `PySlice` (#4141) --- src/types/slice.rs | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/src/types/slice.rs b/src/types/slice.rs index b6895d09e10..70285c9c251 100644 --- a/src/types/slice.rs +++ b/src/types/slice.rs @@ -48,12 +48,10 @@ impl PySliceIndices { impl PySlice { /// Deprecated form of `PySlice::new_bound`. - #[cfg_attr( - not(feature = "gil-refs"), - deprecated( - since = "0.21.0", - note = "`PySlice::new` will be replaced by `PySlice::new_bound` in a future PyO3 version" - ) + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`PySlice::new` will be replaced by `PySlice::new_bound` in a future PyO3 version" )] pub fn new(py: Python<'_>, start: isize, stop: isize, step: isize) -> &PySlice { Self::new_bound(py, start, stop, step).into_gil_ref() @@ -73,12 +71,10 @@ impl PySlice { } /// Deprecated form of `PySlice::full_bound`. - #[cfg_attr( - not(feature = "gil-refs"), - deprecated( - since = "0.21.0", - note = "`PySlice::full` will be replaced by `PySlice::full_bound` in a future PyO3 version" - ) + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`PySlice::full` will be replaced by `PySlice::full_bound` in a future PyO3 version" )] pub fn full(py: Python<'_>) -> &PySlice { PySlice::full_bound(py).into_gil_ref() From dc9a41521af0f1485a152e18fafe2b951f9d98e6 Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Wed, 1 May 2024 12:57:03 +0200 Subject: [PATCH 046/495] feature gate deprecated APIs for `Py` (#4142) --- guide/src/memory.md | 14 +++ guide/src/python-from-rust/function-calls.md | 2 +- guide/src/types.md | 2 + src/err/mod.rs | 1 + src/instance.rs | 115 ++++++++----------- src/marker.rs | 34 +++--- src/types/any.rs | 5 +- src/types/dict.rs | 6 +- 8 files changed, 88 insertions(+), 91 deletions(-) diff --git a/guide/src/memory.md b/guide/src/memory.md index cd4af4f8f13..a6640e65cf3 100644 --- a/guide/src/memory.md +++ b/guide/src/memory.md @@ -177,9 +177,11 @@ What happens to the memory when the last `Py` is dropped and its reference count reaches zero? It depends whether or not we are holding the GIL. ```rust +# #![allow(unused_imports)] # use pyo3::prelude::*; # use pyo3::types::PyString; # fn main() -> PyResult<()> { +# #[cfg(feature = "gil-refs")] Python::with_gil(|py| -> PyResult<()> { #[allow(deprecated)] // py.eval() is part of the GIL Refs API let hello: Py = py.eval("\"Hello World!\"", None, None)?.extract()?; @@ -203,9 +205,12 @@ This example wasn't very interesting. We could have just used a GIL-bound we are *not* holding the GIL? ```rust +# #![allow(unused_imports)] # use pyo3::prelude::*; # use pyo3::types::PyString; # fn main() -> PyResult<()> { +# #[cfg(feature = "gil-refs")] +# { let hello: Py = Python::with_gil(|py| { #[allow(deprecated)] // py.eval() is part of the GIL Refs API py.eval("\"Hello World!\"", None, None)?.extract() @@ -224,6 +229,7 @@ Python::with_gil(|py| // Memory for `hello` is released here. # () ); +# } # Ok(()) # } ``` @@ -237,9 +243,12 @@ We can avoid the delay in releasing memory if we are careful to drop the `Py` while the GIL is held. ```rust +# #![allow(unused_imports)] # use pyo3::prelude::*; # use pyo3::types::PyString; # fn main() -> PyResult<()> { +# #[cfg(feature = "gil-refs")] +# { #[allow(deprecated)] // py.eval() is part of the GIL Refs API let hello: Py = Python::with_gil(|py| py.eval("\"Hello World!\"", None, None)?.extract())?; @@ -252,6 +261,7 @@ Python::with_gil(|py| { } drop(hello); // Memory released here. }); +# } # Ok(()) # } ``` @@ -263,9 +273,12 @@ that rather than being released immediately, the memory will not be released until the GIL is dropped. ```rust +# #![allow(unused_imports)] # use pyo3::prelude::*; # use pyo3::types::PyString; # fn main() -> PyResult<()> { +# #[cfg(feature = "gil-refs")] +# { #[allow(deprecated)] // py.eval() is part of the GIL Refs API let hello: Py = Python::with_gil(|py| py.eval("\"Hello World!\"", None, None)?.extract())?; @@ -280,6 +293,7 @@ Python::with_gil(|py| { // Do more stuff... // Memory released here at end of `with_gil()` closure. }); +# } # Ok(()) # } ``` diff --git a/guide/src/python-from-rust/function-calls.md b/guide/src/python-from-rust/function-calls.md index f97de1f24ce..c19d6fafabc 100644 --- a/guide/src/python-from-rust/function-calls.md +++ b/guide/src/python-from-rust/function-calls.md @@ -107,7 +107,7 @@ fn main() -> PyResult<()> {
-During PyO3's [migration from "GIL Refs" to the `Bound` smart pointer](../migration.md#migrating-from-the-gil-refs-api-to-boundt), [`Py::call`]({{#PYO3_DOCS_URL}}/pyo3/struct.Py.html#method.call) is temporarily named `call_bound` (and `call_method` is temporarily `call_method_bound`). +During PyO3's [migration from "GIL Refs" to the `Bound` smart pointer](../migration.md#migrating-from-the-gil-refs-api-to-boundt), `Py::call` is temporarily named [`Py::call_bound`]({{#PYO3_DOCS_URL}}/pyo3/struct.Py.html#method.call_bound) (and `call_method` is temporarily `call_method_bound`). (This temporary naming is only the case for the `Py` smart pointer. The methods on the `&PyAny` GIL Ref such as `call` have not been given replacements, and the methods on the `Bound` smart pointer such as [`Bound::call`]({{#PYO3_DOCS_URL}}/pyo3/types/trait.PyAnyMethods.html#tymethod.call) already use follow the newest API conventions.) diff --git a/guide/src/types.md b/guide/src/types.md index 82040de2c43..d28fb7e15d6 100644 --- a/guide/src/types.md +++ b/guide/src/types.md @@ -353,8 +353,10 @@ let _: Py = obj.extract()?; For a `&PyAny` object reference `any` where the underlying object is a `#[pyclass]`: ```rust +# #![allow(unused_imports)] # use pyo3::prelude::*; # #[pyclass] #[derive(Clone)] struct MyClass { } +# #[cfg(feature = "gil-refs")] # Python::with_gil(|py| -> PyResult<()> { #[allow(deprecated)] // into_ref is part of the deprecated GIL Refs API let obj: &PyAny = Py::new(py, MyClass {})?.into_ref(py); diff --git a/src/err/mod.rs b/src/err/mod.rs index 4f46f360094..d923761af1d 100644 --- a/src/err/mod.rs +++ b/src/err/mod.rs @@ -64,6 +64,7 @@ impl<'a> PyDowncastError<'a> { /// Compatibility API to convert the Bound variant `DowncastError` into the /// gil-ref variant + #[cfg(feature = "gil-refs")] pub(crate) fn from_downcast_err(DowncastError { from, to }: DowncastError<'a, 'a>) -> Self { #[allow(deprecated)] let from = unsafe { from.py().from_borrowed_ptr(from.as_ptr()) }; diff --git a/src/instance.rs b/src/instance.rs index cc892a2dd44..11d45df6f78 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -1,4 +1,4 @@ -use crate::err::{self, PyDowncastError, PyErr, PyResult}; +use crate::err::{self, PyErr, PyResult}; use crate::impl_::pycell::PyClassObject; use crate::pycell::{PyBorrowError, PyBorrowMutError}; use crate::pyclass::boolean_struct::{False, True}; @@ -721,12 +721,12 @@ impl IntoPy for Borrowed<'_, '_, T> { /// /// This type does not auto-dereference to the inner object because you must prove you hold the GIL to access it. /// Instead, call one of its methods to access the inner object: -/// - [`Py::as_ref`], to borrow a GIL-bound reference to the contained object. +/// - [`Py::bind`] or [`Py::into_bound`], to borrow a GIL-bound reference to the contained object. /// - [`Py::borrow`], [`Py::try_borrow`], [`Py::borrow_mut`], or [`Py::try_borrow_mut`], /// to get a (mutable) reference to a contained pyclass, using a scheme similar to std's [`RefCell`]. -/// See the [`PyCell` guide entry](https://pyo3.rs/latest/class.html#pycell-and-interior-mutability) +/// See the [guide entry](https://pyo3.rs/latest/class.html#bound-and-interior-mutability) /// for more information. -/// - You can call methods directly on `Py` with [`Py::call`], [`Py::call_method`] and friends. +/// - You can call methods directly on `Py` with [`Py::call_bound`], [`Py::call_method_bound`] and friends. /// These require passing in the [`Python<'py>`](crate::Python) token but are otherwise similar to the corresponding /// methods on [`PyAny`]. /// @@ -991,12 +991,10 @@ where /// assert!(my_class_cell.try_borrow().is_ok()); /// }); /// ``` - #[cfg_attr( - not(feature = "gil-refs"), - deprecated( - since = "0.21.0", - note = "use `obj.bind(py)` instead of `obj.as_ref(py)`" - ) + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "use `obj.bind(py)` instead of `obj.as_ref(py)`" )] pub fn as_ref<'py>(&'py self, _py: Python<'py>) -> &'py T::AsRefTarget { let any = self.as_ptr() as *const PyAny; @@ -1046,12 +1044,10 @@ where /// obj.into_ref(py) /// } /// ``` - #[cfg_attr( - not(feature = "gil-refs"), - deprecated( - since = "0.21.0", - note = "use `obj.into_bound(py)` instead of `obj.into_ref(py)`" - ) + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "use `obj.into_bound(py)` instead of `obj.into_ref(py)`" )] pub fn into_ref(self, py: Python<'_>) -> &T::AsRefTarget { #[allow(deprecated)] @@ -1464,12 +1460,10 @@ impl Py { } /// Deprecated form of [`call_bound`][Py::call_bound]. - #[cfg_attr( - not(feature = "gil-refs"), - deprecated( - since = "0.21.0", - note = "`call` will be replaced by `call_bound` in a future PyO3 version" - ) + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`call` will be replaced by `call_bound` in a future PyO3 version" )] #[inline] pub fn call(&self, py: Python<'_>, args: A, kwargs: Option<&PyDict>) -> PyResult @@ -1506,12 +1500,10 @@ impl Py { } /// Deprecated form of [`call_method_bound`][Py::call_method_bound]. - #[cfg_attr( - not(feature = "gil-refs"), - deprecated( - since = "0.21.0", - note = "`call_method` will be replaced by `call_method_bound` in a future PyO3 version" - ) + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`call_method` will be replaced by `call_method_bound` in a future PyO3 version" )] #[inline] pub fn call_method( @@ -1779,6 +1771,7 @@ impl std::convert::From> for Py { } // `&PyCell` can be converted to `Py` +#[cfg(feature = "gil-refs")] #[allow(deprecated)] impl std::convert::From<&crate::PyCell> for Py where @@ -1844,10 +1837,7 @@ where { /// Extracts `Self` from the source `PyObject`. fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult { - // TODO update MSRV past 1.59 and use .cloned() to make - // clippy happy - #[allow(clippy::map_clone)] - ob.downcast().map(Clone::clone).map_err(Into::into) + ob.downcast().cloned().map_err(Into::into) } } @@ -1888,21 +1878,22 @@ pub type PyObject = Py; impl PyObject { /// Deprecated form of [`PyObject::downcast_bound`] - #[cfg_attr( - not(feature = "gil-refs"), - deprecated( - since = "0.21.0", - note = "`PyObject::downcast` will be replaced by `PyObject::downcast_bound` in a future PyO3 version" - ) + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`PyObject::downcast` will be replaced by `PyObject::downcast_bound` in a future PyO3 version" )] #[inline] - pub fn downcast<'py, T>(&'py self, py: Python<'py>) -> Result<&'py T, PyDowncastError<'py>> + pub fn downcast<'py, T>( + &'py self, + py: Python<'py>, + ) -> Result<&'py T, crate::err::PyDowncastError<'py>> where T: PyTypeCheck, { self.downcast_bound::(py) .map(Bound::as_gil_ref) - .map_err(PyDowncastError::from_downcast_err) + .map_err(crate::err::PyDowncastError::from_downcast_err) } /// Downcast this `PyObject` to a concrete Python type or pyclass. /// @@ -1970,12 +1961,10 @@ impl PyObject { /// # Safety /// /// Callers must ensure that the type is valid or risk type confusion. - #[cfg_attr( - not(feature = "gil-refs"), - deprecated( - since = "0.21.0", - note = "`PyObject::downcast_unchecked` will be replaced by `PyObject::downcast_bound_unchecked` in a future PyO3 version" - ) + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`PyObject::downcast_unchecked` will be replaced by `PyObject::downcast_bound_unchecked` in a future PyO3 version" )] #[inline] pub unsafe fn downcast_unchecked<'py, T>(&'py self, py: Python<'py>) -> &T @@ -1997,35 +1986,31 @@ impl PyObject { } #[cfg(test)] -#[cfg_attr(not(feature = "gil-refs"), allow(deprecated))] mod tests { use super::{Bound, Py, PyObject}; use crate::types::any::PyAnyMethods; use crate::types::{dict::IntoPyDict, PyDict, PyString}; use crate::types::{PyCapsule, PyStringMethods}; - use crate::{ffi, Borrowed, PyAny, PyNativeType, PyResult, Python, ToPyObject}; + use crate::{ffi, Borrowed, PyAny, PyResult, Python, ToPyObject}; #[test] fn test_call() { Python::with_gil(|py| { - let obj = py.get_type::().to_object(py); + let obj = py.get_type_bound::().to_object(py); - let assert_repr = |obj: &PyAny, expected: &str| { - assert_eq!(obj.repr().unwrap().to_str().unwrap(), expected); + let assert_repr = |obj: &Bound<'_, PyAny>, expected: &str| { + assert_eq!(obj.repr().unwrap().to_cow().unwrap(), expected); }; - assert_repr(obj.call0(py).unwrap().as_ref(py), "{}"); - assert_repr(obj.call1(py, ()).unwrap().as_ref(py), "{}"); - assert_repr(obj.call(py, (), None).unwrap().as_ref(py), "{}"); + assert_repr(obj.call0(py).unwrap().bind(py), "{}"); + assert_repr(obj.call1(py, ()).unwrap().bind(py), "{}"); + assert_repr(obj.call_bound(py, (), None).unwrap().bind(py), "{}"); - assert_repr( - obj.call1(py, ((('x', 1),),)).unwrap().as_ref(py), - "{'x': 1}", - ); + assert_repr(obj.call1(py, ((('x', 1),),)).unwrap().bind(py), "{'x': 1}"); assert_repr( obj.call_bound(py, (), Some(&[('x', 1)].into_py_dict_bound(py))) .unwrap() - .as_ref(py), + .bind(py), "{'x': 1}", ); }) @@ -2037,7 +2022,7 @@ mod tests { let obj: PyObject = PyDict::new_bound(py).into(); assert!(obj.call_method0(py, "asdf").is_err()); assert!(obj - .call_method(py, "nonexistent_method", (1,), None) + .call_method_bound(py, "nonexistent_method", (1,), None) .is_err()); assert!(obj.call_method0(py, "nonexistent_method").is_err()); assert!(obj.call_method1(py, "nonexistent_method", (1,)).is_err()); @@ -2083,7 +2068,7 @@ a = A() assert!(instance .getattr(py, "foo")? - .as_ref(py) + .bind(py) .eq(PyString::new_bound(py, "bar"))?); instance.getattr(py, "foo")?; @@ -2109,7 +2094,7 @@ a = A() instance.getattr(py, foo).unwrap_err(); instance.setattr(py, foo, bar)?; - assert!(instance.getattr(py, foo)?.as_ref(py).eq(bar)?); + assert!(instance.getattr(py, foo)?.bind(py).eq(bar)?); Ok(()) }) } @@ -2117,7 +2102,7 @@ a = A() #[test] fn invalid_attr() -> PyResult<()> { Python::with_gil(|py| { - let instance: Py = py.eval("object()", None, None)?.into(); + let instance: Py = py.eval_bound("object()", None, None)?.into(); instance.getattr(py, "foo").unwrap_err(); @@ -2130,7 +2115,7 @@ a = A() #[test] fn test_py2_from_py_object() { Python::with_gil(|py| { - let instance: &PyAny = py.eval("object()", None, None).unwrap(); + let instance = py.eval_bound("object()", None, None).unwrap(); let ptr = instance.as_ptr(); let instance: Bound<'_, PyAny> = instance.extract().unwrap(); assert_eq!(instance.as_ptr(), ptr); @@ -2141,7 +2126,7 @@ a = A() fn test_py2_into_py_object() { Python::with_gil(|py| { let instance = py - .eval("object()", None, None) + .eval_bound("object()", None, None) .unwrap() .as_borrowed() .to_owned(); diff --git a/src/marker.rs b/src/marker.rs index b1f2d399209..29aae69d4f2 100644 --- a/src/marker.rs +++ b/src/marker.rs @@ -116,18 +116,17 @@ //! [`SendWrapper`]: https://docs.rs/send_wrapper/latest/send_wrapper/struct.SendWrapper.html //! [`Rc`]: std::rc::Rc //! [`Py`]: crate::Py -use crate::err::{self, PyDowncastError, PyErr, PyResult}; +use crate::err::{self, PyErr, PyResult}; use crate::ffi_ptr_ext::FfiPtrExt; use crate::gil::{GILGuard, SuspendGIL}; use crate::impl_::not_send::NotSend; use crate::py_result_ext::PyResultExt; -use crate::type_object::HasPyGilRef; use crate::types::any::PyAnyMethods; use crate::types::{ PyAny, PyDict, PyEllipsis, PyModule, PyNone, PyNotImplemented, PyString, PyType, }; use crate::version::PythonVersionInfo; -use crate::{ffi, Bound, IntoPy, Py, PyNativeType, PyObject, PyTypeCheck, PyTypeInfo}; +use crate::{ffi, Bound, IntoPy, Py, PyNativeType, PyObject, PyTypeInfo}; #[allow(deprecated)] use crate::{gil::GILPool, FromPyPointer}; use std::ffi::{CStr, CString}; @@ -839,16 +838,17 @@ impl<'py> Python<'py> { } /// Registers the object in the release pool, and tries to downcast to specific type. - #[cfg_attr( - not(feature = "gil-refs"), - deprecated( - since = "0.21.0", - note = "use `obj.downcast_bound::(py)` instead of `py.checked_cast_as::(obj)`" - ) + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "use `obj.downcast_bound::(py)` instead of `py.checked_cast_as::(obj)`" )] - pub fn checked_cast_as(self, obj: PyObject) -> Result<&'py T, PyDowncastError<'py>> + pub fn checked_cast_as( + self, + obj: PyObject, + ) -> Result<&'py T, crate::err::PyDowncastError<'py>> where - T: PyTypeCheck, + T: crate::PyTypeCheck, { #[allow(deprecated)] obj.into_ref(self).downcast() @@ -860,16 +860,14 @@ impl<'py> Python<'py> { /// # Safety /// /// Callers must ensure that ensure that the cast is valid. - #[cfg_attr( - not(feature = "gil-refs"), - deprecated( - since = "0.21.0", - note = "use `obj.downcast_bound_unchecked::(py)` instead of `py.cast_as::(obj)`" - ) + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "use `obj.downcast_bound_unchecked::(py)` instead of `py.cast_as::(obj)`" )] pub unsafe fn cast_as(self, obj: PyObject) -> &'py T where - T: HasPyGilRef, + T: crate::type_object::HasPyGilRef, { #[allow(deprecated)] obj.into_ref(self).downcast_unchecked() diff --git a/src/types/any.rs b/src/types/any.rs index 8b0bac44ca6..1854308ae7f 100644 --- a/src/types/any.rs +++ b/src/types/any.rs @@ -2705,17 +2705,16 @@ class SimpleClass: } #[test] - #[allow(deprecated)] fn test_is_ellipsis() { Python::with_gil(|py| { let v = py - .eval("...", None, None) + .eval_bound("...", None, None) .map_err(|e| e.display(py)) .unwrap(); assert!(v.is_ellipsis()); - let not_ellipsis = 5.to_object(py).into_ref(py); + let not_ellipsis = 5.to_object(py).into_bound(py); assert!(!not_ellipsis.is_ellipsis()); }); } diff --git a/src/types/dict.rs b/src/types/dict.rs index 64b3adcad44..68cca1cd981 100644 --- a/src/types/dict.rs +++ b/src/types/dict.rs @@ -817,8 +817,6 @@ where #[cfg(test)] mod tests { use super::*; - #[cfg(not(any(PyPy, GraalPy)))] - use crate::exceptions; use crate::types::PyTuple; use std::collections::{BTreeMap, HashMap}; @@ -948,7 +946,7 @@ mod tests { #[test] #[allow(deprecated)] - #[cfg(not(any(PyPy, GraalPy)))] + #[cfg(all(not(any(PyPy, GraalPy)), feature = "gil-refs"))] fn test_get_item_with_error() { Python::with_gil(|py| { let mut v = HashMap::new(); @@ -967,7 +965,7 @@ mod tests { assert!(dict .get_item_with_error(dict) .unwrap_err() - .is_instance_of::(py)); + .is_instance_of::(py)); }); } From 5534a7bee8baac15885f012fdbc2f793e5890176 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Wed, 1 May 2024 08:18:12 -0400 Subject: [PATCH 047/495] feature gate deprecated APIs for `PyBuffer` (#4144) --- src/buffer.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/buffer.rs b/src/buffer.rs index 84b08289771..74ac7fe8e53 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -18,8 +18,10 @@ // DEALINGS IN THE SOFTWARE. //! `PyBuffer` implementation +use crate::Bound; +#[cfg(feature = "gil-refs")] +use crate::PyNativeType; use crate::{err, exceptions::PyBufferError, ffi, FromPyObject, PyAny, PyResult, Python}; -use crate::{Bound, PyNativeType}; use std::marker::PhantomData; use std::os::raw; use std::pin::Pin; @@ -190,12 +192,10 @@ impl<'py, T: Element> FromPyObject<'py> for PyBuffer { impl PyBuffer { /// Deprecated form of [`PyBuffer::get_bound`] - #[cfg_attr( - not(feature = "gil-refs"), - deprecated( - since = "0.21.0", - note = "`PyBuffer::get` will be replaced by `PyBuffer::get_bound` in a future PyO3 version" - ) + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`PyBuffer::get` will be replaced by `PyBuffer::get_bound` in a future PyO3 version" )] pub fn get(obj: &PyAny) -> PyResult> { Self::get_bound(&obj.as_borrowed()) From a454f6e9cc2e67e07943a15bb04f37214c93eca6 Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Wed, 1 May 2024 19:13:49 +0200 Subject: [PATCH 048/495] feature gate deprecated APIs for `PyFloat` and `PyComplex` (#4145) --- src/conversions/num_complex.rs | 10 ++++------ src/types/complex.rs | 10 ++++------ src/types/float.rs | 18 +++++++++--------- 3 files changed, 17 insertions(+), 21 deletions(-) diff --git a/src/conversions/num_complex.rs b/src/conversions/num_complex.rs index a57b2745ec9..12f208aa8d1 100644 --- a/src/conversions/num_complex.rs +++ b/src/conversions/num_complex.rs @@ -104,12 +104,10 @@ use std::os::raw::c_double; impl PyComplex { /// Deprecated form of [`PyComplex::from_complex_bound`] - #[cfg_attr( - not(feature = "gil-refs"), - deprecated( - since = "0.21.0", - note = "`PyComplex::from_complex` will be replaced by `PyComplex::from_complex_bound` in a future PyO3 version" - ) + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`PyComplex::from_complex` will be replaced by `PyComplex::from_complex_bound` in a future PyO3 version" )] pub fn from_complex>(py: Python<'_>, complex: Complex) -> &PyComplex { Self::from_complex_bound(py, complex).into_gil_ref() diff --git a/src/types/complex.rs b/src/types/complex.rs index e711b054fe3..4a0c3e30732 100644 --- a/src/types/complex.rs +++ b/src/types/complex.rs @@ -20,12 +20,10 @@ pyobject_native_type!( impl PyComplex { /// Deprecated form of [`PyComplex::from_doubles_bound`] - #[cfg_attr( - not(feature = "gil-refs"), - deprecated( - since = "0.21.0", - note = "`PyComplex::from_doubles` will be replaced by `PyComplex::from_doubles_bound` in a future PyO3 version" - ) + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`PyComplex::from_doubles` will be replaced by `PyComplex::from_doubles_bound` in a future PyO3 version" )] pub fn from_doubles(py: Python<'_>, real: c_double, imag: c_double) -> &PyComplex { Self::from_doubles_bound(py, real, imag).into_gil_ref() diff --git a/src/types/float.rs b/src/types/float.rs index 6db1fdae038..3a64694a624 100644 --- a/src/types/float.rs +++ b/src/types/float.rs @@ -26,12 +26,10 @@ pyobject_native_type!( impl PyFloat { /// Deprecated form of [`PyFloat::new_bound`]. #[inline] - #[cfg_attr( - not(feature = "gil-refs"), - deprecated( - since = "0.21.0", - note = "`PyFloat::new` will be replaced by `PyFloat::new_bound` in a future PyO3 version" - ) + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`PyFloat::new` will be replaced by `PyFloat::new_bound` in a future PyO3 version" )] pub fn new(py: Python<'_>, val: f64) -> &'_ Self { Self::new_bound(py, val).into_gil_ref() @@ -154,9 +152,11 @@ impl<'py> FromPyObject<'py> for f32 { } #[cfg(test)] -#[cfg_attr(not(feature = "gil-refs"), allow(deprecated))] mod tests { - use crate::{types::PyFloat, Python, ToPyObject}; + use crate::{ + types::{PyFloat, PyFloatMethods}, + Python, ToPyObject, + }; macro_rules! num_to_py_object_and_back ( ($func_name:ident, $t1:ty, $t2:ty) => ( @@ -184,7 +184,7 @@ mod tests { Python::with_gil(|py| { let v = 1.23f64; - let obj = PyFloat::new(py, 1.23); + let obj = PyFloat::new_bound(py, 1.23); assert_approx_eq!(v, obj.value()); }); } From 9a808c35c69ffc9b37976cc43c8589a2a7f0fae8 Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Wed, 1 May 2024 21:05:51 +0200 Subject: [PATCH 049/495] fix `clippy-beta` ci workflow (#4147) --- pyo3-build-config/src/lib.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pyo3-build-config/src/lib.rs b/pyo3-build-config/src/lib.rs index 5b1cfdaf322..1aa15d7d62a 100644 --- a/pyo3-build-config/src/lib.rs +++ b/pyo3-build-config/src/lib.rs @@ -86,6 +86,8 @@ pub fn get() -> &'static InterpreterConfig { .map(|path| path.exists()) .unwrap_or(false); + // CONFIG_FILE is generated in build.rs, so it's content can vary + #[allow(unknown_lints, clippy::const_is_empty)] if let Some(interpreter_config) = InterpreterConfig::from_cargo_dep_env() { interpreter_config } else if !CONFIG_FILE.is_empty() { @@ -177,6 +179,8 @@ pub mod pyo3_build_script_impl { /// correct value for CARGO_CFG_TARGET_OS). #[cfg(feature = "resolve-config")] pub fn resolve_interpreter_config() -> Result { + // CONFIG_FILE is generated in build.rs, so it's content can vary + #[allow(unknown_lints, clippy::const_is_empty)] if !CONFIG_FILE.is_empty() { let mut interperter_config = InterpreterConfig::from_reader(Cursor::new(CONFIG_FILE))?; interperter_config.generate_import_libs()?; From cd3f3ed67c963b758cb7e399e6f582d027a76707 Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Fri, 3 May 2024 09:42:30 +0200 Subject: [PATCH 050/495] ci: updates for Rust 1.78 (#4150) * ci: updates for Rust 1.78 * ci: fix clippy * restrict `invalid_pymethods_duplicates` to unlimited api with `full` --- pytests/src/othermod.rs | 4 +- src/pyclass/create_type_object.rs | 6 +- tests/test_compile_error.rs | 2 + tests/ui/abi3_nativetype_inheritance.stderr | 6 +- tests/ui/invalid_argument_attributes.rs | 10 +- tests/ui/invalid_argument_attributes.stderr | 10 +- tests/ui/invalid_cancel_handle.stderr | 10 +- tests/ui/invalid_intern_arg.rs | 4 +- tests/ui/invalid_intern_arg.stderr | 13 ++- tests/ui/invalid_property_args.rs | 6 +- tests/ui/invalid_property_args.stderr | 14 +-- tests/ui/invalid_proto_pymethods.rs | 4 +- tests/ui/invalid_proto_pymethods.stderr | 36 +++++++ tests/ui/invalid_pyfunction_signatures.rs | 1 + tests/ui/invalid_pyfunction_signatures.stderr | 12 +-- tests/ui/invalid_pyfunctions.rs | 18 ++-- tests/ui/invalid_pyfunctions.stderr | 28 ++--- tests/ui/invalid_pymethod_receiver.rs | 2 +- tests/ui/invalid_pymethod_receiver.stderr | 9 +- tests/ui/invalid_pymethods.rs | 10 +- tests/ui/invalid_pymethods.stderr | 21 ++-- tests/ui/invalid_pymethods_duplicates.stderr | 102 ++++++++++++++++++ tests/ui/missing_intopy.stderr | 13 +-- tests/ui/pyclass_send.rs | 2 +- tests/ui/traverse.rs | 12 +-- tests/ui/traverse.stderr | 26 ++--- 26 files changed, 264 insertions(+), 117 deletions(-) diff --git a/pytests/src/othermod.rs b/pytests/src/othermod.rs index 29ca8121890..36ad4b5e23e 100644 --- a/pytests/src/othermod.rs +++ b/pytests/src/othermod.rs @@ -34,8 +34,8 @@ pub fn othermod(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; - m.add("USIZE_MIN", usize::min_value())?; - m.add("USIZE_MAX", usize::max_value())?; + m.add("USIZE_MIN", usize::MIN)?; + m.add("USIZE_MAX", usize::MAX)?; Ok(()) } diff --git a/src/pyclass/create_type_object.rs b/src/pyclass/create_type_object.rs index 52e346212f0..e90c5736e5c 100644 --- a/src/pyclass/create_type_object.rs +++ b/src/pyclass/create_type_object.rs @@ -145,12 +145,14 @@ impl PyTypeBuilder { #[cfg(all(not(Py_3_9), not(Py_LIMITED_API)))] ffi::Py_bf_getbuffer => { // Safety: slot.pfunc is a valid function pointer - self.buffer_procs.bf_getbuffer = Some(std::mem::transmute(pfunc)); + self.buffer_procs.bf_getbuffer = + Some(std::mem::transmute::<*mut T, ffi::getbufferproc>(pfunc)); } #[cfg(all(not(Py_3_9), not(Py_LIMITED_API)))] ffi::Py_bf_releasebuffer => { // Safety: slot.pfunc is a valid function pointer - self.buffer_procs.bf_releasebuffer = Some(std::mem::transmute(pfunc)); + self.buffer_procs.bf_releasebuffer = + Some(std::mem::transmute::<*mut T, ffi::releasebufferproc>(pfunc)); } _ => {} } diff --git a/tests/test_compile_error.rs b/tests/test_compile_error.rs index 44049620598..30e77888cfd 100644 --- a/tests/test_compile_error.rs +++ b/tests/test_compile_error.rs @@ -13,6 +13,8 @@ fn test_compile_errors() { t.compile_fail("tests/ui/invalid_pyfunction_signatures.rs"); #[cfg(any(not(Py_LIMITED_API), Py_3_11))] t.compile_fail("tests/ui/invalid_pymethods_buffer.rs"); + // The output is not stable across abi3 / not abi3 and features + #[cfg(all(not(Py_LIMITED_API), feature = "full"))] t.compile_fail("tests/ui/invalid_pymethods_duplicates.rs"); t.compile_fail("tests/ui/invalid_pymethod_enum.rs"); t.compile_fail("tests/ui/invalid_pymethod_names.rs"); diff --git a/tests/ui/abi3_nativetype_inheritance.stderr b/tests/ui/abi3_nativetype_inheritance.stderr index 784da4f55ba..f9ca7c61b89 100644 --- a/tests/ui/abi3_nativetype_inheritance.stderr +++ b/tests/ui/abi3_nativetype_inheritance.stderr @@ -1,12 +1,10 @@ -error[E0277]: the trait bound `PyDict: PyClass` is not satisfied +error[E0277]: the trait bound `PyDict: PyClassBaseType` is not satisfied --> tests/ui/abi3_nativetype_inheritance.rs:5:19 | 5 | #[pyclass(extends=PyDict)] | ^^^^^^ the trait `PyClass` is not implemented for `PyDict`, which is required by `PyDict: PyClassBaseType` | - = help: the following other types implement trait `PyClass`: - TestClass - pyo3::coroutine::Coroutine + = help: the trait `PyClassBaseType` is implemented for `PyAny` = note: required for `PyDict` to implement `PyClassBaseType` note: required by a bound in `PyClassImpl::BaseType` --> src/impl_/pyclass.rs diff --git a/tests/ui/invalid_argument_attributes.rs b/tests/ui/invalid_argument_attributes.rs index 311c6c03e0e..6797642d77b 100644 --- a/tests/ui/invalid_argument_attributes.rs +++ b/tests/ui/invalid_argument_attributes.rs @@ -1,18 +1,18 @@ use pyo3::prelude::*; #[pyfunction] -fn invalid_attribute(#[pyo3(get)] param: String) {} +fn invalid_attribute(#[pyo3(get)] _param: String) {} #[pyfunction] -fn from_py_with_no_value(#[pyo3(from_py_with)] param: String) {} +fn from_py_with_no_value(#[pyo3(from_py_with)] _param: String) {} #[pyfunction] -fn from_py_with_string(#[pyo3("from_py_with")] param: String) {} +fn from_py_with_string(#[pyo3("from_py_with")] _param: String) {} #[pyfunction] -fn from_py_with_value_not_a_string(#[pyo3(from_py_with = func)] param: String) {} +fn from_py_with_value_not_a_string(#[pyo3(from_py_with = func)] _param: String) {} #[pyfunction] -fn from_py_with_repeated(#[pyo3(from_py_with = "func", from_py_with = "func")] param: String) {} +fn from_py_with_repeated(#[pyo3(from_py_with = "func", from_py_with = "func")] _param: String) {} fn main() {} diff --git a/tests/ui/invalid_argument_attributes.stderr b/tests/ui/invalid_argument_attributes.stderr index d27c25fd179..e6c42f82a87 100644 --- a/tests/ui/invalid_argument_attributes.stderr +++ b/tests/ui/invalid_argument_attributes.stderr @@ -1,29 +1,29 @@ error: expected `cancel_handle` or `from_py_with` --> tests/ui/invalid_argument_attributes.rs:4:29 | -4 | fn invalid_attribute(#[pyo3(get)] param: String) {} +4 | fn invalid_attribute(#[pyo3(get)] _param: String) {} | ^^^ error: expected `=` --> tests/ui/invalid_argument_attributes.rs:7:45 | -7 | fn from_py_with_no_value(#[pyo3(from_py_with)] param: String) {} +7 | fn from_py_with_no_value(#[pyo3(from_py_with)] _param: String) {} | ^ error: expected `cancel_handle` or `from_py_with` --> tests/ui/invalid_argument_attributes.rs:10:31 | -10 | fn from_py_with_string(#[pyo3("from_py_with")] param: String) {} +10 | fn from_py_with_string(#[pyo3("from_py_with")] _param: String) {} | ^^^^^^^^^^^^^^ error: expected string literal --> tests/ui/invalid_argument_attributes.rs:13:58 | -13 | fn from_py_with_value_not_a_string(#[pyo3(from_py_with = func)] param: String) {} +13 | fn from_py_with_value_not_a_string(#[pyo3(from_py_with = func)] _param: String) {} | ^^^^ error: `from_py_with` may only be specified once per argument --> tests/ui/invalid_argument_attributes.rs:16:56 | -16 | fn from_py_with_repeated(#[pyo3(from_py_with = "func", from_py_with = "func")] param: String) {} +16 | fn from_py_with_repeated(#[pyo3(from_py_with = "func", from_py_with = "func")] _param: String) {} | ^^^^^^^^^^^^ diff --git a/tests/ui/invalid_cancel_handle.stderr b/tests/ui/invalid_cancel_handle.stderr index 41a2c0854b7..f6452611679 100644 --- a/tests/ui/invalid_cancel_handle.stderr +++ b/tests/ui/invalid_cancel_handle.stderr @@ -38,13 +38,17 @@ note: function defined here | ^^^^^^^^^^^^^^^^^^^^^^^^ -------------- = note: this error originates in the attribute macro `pyfunction` (in Nightly builds, run with -Z macro-backtrace for more info) -error[E0277]: the trait bound `CancelHandle: PyClass` is not satisfied +error[E0277]: the trait bound `CancelHandle: PyFunctionArgument<'_, '_>` is not satisfied --> tests/ui/invalid_cancel_handle.rs:20:50 | 20 | async fn missing_cancel_handle_attribute(_param: pyo3::coroutine::CancelHandle) {} | ^^^^ the trait `PyClass` is not implemented for `CancelHandle`, which is required by `CancelHandle: PyFunctionArgument<'_, '_>` | - = help: the trait `PyClass` is implemented for `pyo3::coroutine::Coroutine` + = help: the following other types implement trait `PyFunctionArgument<'a, 'py>`: + Option<&'a pyo3::Bound<'py, T>> + &'a pyo3::Bound<'py, T> + &'a pyo3::coroutine::Coroutine + &'a mut pyo3::coroutine::Coroutine = note: required for `CancelHandle` to implement `FromPyObject<'_>` = note: required for `CancelHandle` to implement `FromPyObjectBound<'_, '_>` = note: required for `CancelHandle` to implement `PyFunctionArgument<'_, '_>` @@ -57,7 +61,7 @@ note: required by a bound in `extract_argument` | T: PyFunctionArgument<'a, 'py>, | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `extract_argument` -error[E0277]: the trait bound `CancelHandle: Clone` is not satisfied +error[E0277]: the trait bound `CancelHandle: PyFunctionArgument<'_, '_>` is not satisfied --> tests/ui/invalid_cancel_handle.rs:20:50 | 20 | async fn missing_cancel_handle_attribute(_param: pyo3::coroutine::CancelHandle) {} diff --git a/tests/ui/invalid_intern_arg.rs b/tests/ui/invalid_intern_arg.rs index 3c7bd592175..eb479431b90 100644 --- a/tests/ui/invalid_intern_arg.rs +++ b/tests/ui/invalid_intern_arg.rs @@ -1,6 +1,6 @@ use pyo3::Python; fn main() { - let foo = if true { "foo" } else { "bar" }; - Python::with_gil(|py| py.import_bound(pyo3::intern!(py, foo)).unwrap()); + let _foo = if true { "foo" } else { "bar" }; + Python::with_gil(|py| py.import_bound(pyo3::intern!(py, _foo)).unwrap()); } diff --git a/tests/ui/invalid_intern_arg.stderr b/tests/ui/invalid_intern_arg.stderr index dce2d85bf09..5d2131bd845 100644 --- a/tests/ui/invalid_intern_arg.stderr +++ b/tests/ui/invalid_intern_arg.stderr @@ -1,8 +1,17 @@ error[E0435]: attempt to use a non-constant value in a constant --> tests/ui/invalid_intern_arg.rs:5:61 | -5 | Python::with_gil(|py| py.import_bound(pyo3::intern!(py, foo)).unwrap()); - | ------------------^^^- +5 | Python::with_gil(|py| py.import_bound(pyo3::intern!(py, _foo)).unwrap()); + | ------------------^^^^- | | | | | non-constant value | help: consider using `let` instead of `static`: `let INTERNED` + +error: lifetime may not live long enough + --> tests/ui/invalid_intern_arg.rs:5:27 + | +5 | Python::with_gil(|py| py.import_bound(pyo3::intern!(py, _foo)).unwrap()); + | --- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ returning this value requires that `'1` must outlive `'2` + | | | + | | return type of closure is pyo3::Bound<'2, pyo3::prelude::PyModule> + | has type `pyo3::Python<'1>` diff --git a/tests/ui/invalid_property_args.rs b/tests/ui/invalid_property_args.rs index 3f335952235..b5eba27eb60 100644 --- a/tests/ui/invalid_property_args.rs +++ b/tests/ui/invalid_property_args.rs @@ -6,7 +6,7 @@ struct ClassWithGetter {} #[pymethods] impl ClassWithGetter { #[getter] - fn getter_with_arg(&self, py: Python<'_>, index: u32) {} + fn getter_with_arg(&self, _py: Python<'_>, _index: u32) {} } #[pyclass] @@ -15,13 +15,13 @@ struct ClassWithSetter {} #[pymethods] impl ClassWithSetter { #[setter] - fn setter_with_no_arg(&mut self, py: Python<'_>) {} + fn setter_with_no_arg(&mut self, _py: Python<'_>) {} } #[pymethods] impl ClassWithSetter { #[setter] - fn setter_with_too_many_args(&mut self, py: Python<'_>, foo: u32, bar: u32) {} + fn setter_with_too_many_args(&mut self, _py: Python<'_>, _foo: u32, _bar: u32) {} } #[pyclass] diff --git a/tests/ui/invalid_property_args.stderr b/tests/ui/invalid_property_args.stderr index a41b6c79b3a..dea2e3fb2b4 100644 --- a/tests/ui/invalid_property_args.stderr +++ b/tests/ui/invalid_property_args.stderr @@ -1,20 +1,20 @@ error: getter function can only have one argument (of type pyo3::Python) - --> tests/ui/invalid_property_args.rs:9:54 + --> tests/ui/invalid_property_args.rs:9:56 | -9 | fn getter_with_arg(&self, py: Python<'_>, index: u32) {} - | ^^^ +9 | fn getter_with_arg(&self, _py: Python<'_>, _index: u32) {} + | ^^^ error: setter function expected to have one argument --> tests/ui/invalid_property_args.rs:18:8 | -18 | fn setter_with_no_arg(&mut self, py: Python<'_>) {} +18 | fn setter_with_no_arg(&mut self, _py: Python<'_>) {} | ^^^^^^^^^^^^^^^^^^ error: setter function can have at most two arguments ([pyo3::Python,] and value) - --> tests/ui/invalid_property_args.rs:24:76 + --> tests/ui/invalid_property_args.rs:24:79 | -24 | fn setter_with_too_many_args(&mut self, py: Python<'_>, foo: u32, bar: u32) {} - | ^^^ +24 | fn setter_with_too_many_args(&mut self, _py: Python<'_>, _foo: u32, _bar: u32) {} + | ^^^ error: `get` and `set` with tuple struct fields require `name` --> tests/ui/invalid_property_args.rs:28:50 diff --git a/tests/ui/invalid_proto_pymethods.rs b/tests/ui/invalid_proto_pymethods.rs index d370c4fddb5..c40790c3168 100644 --- a/tests/ui/invalid_proto_pymethods.rs +++ b/tests/ui/invalid_proto_pymethods.rs @@ -54,11 +54,11 @@ struct EqAndRichcmp; #[pymethods] impl EqAndRichcmp { - fn __eq__(&self, other: &Self) -> bool { + fn __eq__(&self, _other: &Self) -> bool { true } - fn __richcmp__(&self, other: &Self, op: CompareOp) -> bool { + fn __richcmp__(&self, _other: &Self, _op: CompareOp) -> bool { true } } diff --git a/tests/ui/invalid_proto_pymethods.stderr b/tests/ui/invalid_proto_pymethods.stderr index c9f6adff3a1..82c99c2ddc3 100644 --- a/tests/ui/invalid_proto_pymethods.stderr +++ b/tests/ui/invalid_proto_pymethods.stderr @@ -22,6 +22,24 @@ error: `text_signature` cannot be used with magic method `__bool__` 46 | #[pyo3(name = "__bool__", text_signature = "")] | ^^^^^^^^^^^^^^ +error[E0034]: multiple applicable items in scope + --> tests/ui/invalid_proto_pymethods.rs:55:1 + | +55 | #[pymethods] + | ^^^^^^^^^^^^ multiple `__pymethod___richcmp____` found + | +note: candidate #1 is defined in an impl for the type `EqAndRichcmp` + --> tests/ui/invalid_proto_pymethods.rs:55:1 + | +55 | #[pymethods] + | ^^^^^^^^^^^^ +note: candidate #2 is defined in an impl for the type `EqAndRichcmp` + --> tests/ui/invalid_proto_pymethods.rs:55:1 + | +55 | #[pymethods] + | ^^^^^^^^^^^^ + = note: this error originates in the macro `::pyo3::impl_::pyclass::generate_pyclass_richcompare_slot` which comes from the expansion of the attribute macro `pymethods` (in Nightly builds, run with -Z macro-backtrace for more info) + error[E0592]: duplicate definitions with name `__pymethod___richcmp____` --> tests/ui/invalid_proto_pymethods.rs:55:1 | @@ -32,3 +50,21 @@ error[E0592]: duplicate definitions with name `__pymethod___richcmp____` | other definition for `__pymethod___richcmp____` | = note: this error originates in the macro `::pyo3::impl_::pyclass::generate_pyclass_richcompare_slot` which comes from the expansion of the attribute macro `pymethods` (in Nightly builds, run with -Z macro-backtrace for more info) + +error[E0034]: multiple applicable items in scope + --> tests/ui/invalid_proto_pymethods.rs:55:1 + | +55 | #[pymethods] + | ^^^^^^^^^^^^ multiple `__pymethod___richcmp____` found + | +note: candidate #1 is defined in an impl for the type `EqAndRichcmp` + --> tests/ui/invalid_proto_pymethods.rs:55:1 + | +55 | #[pymethods] + | ^^^^^^^^^^^^ +note: candidate #2 is defined in an impl for the type `EqAndRichcmp` + --> tests/ui/invalid_proto_pymethods.rs:55:1 + | +55 | #[pymethods] + | ^^^^^^^^^^^^ + = note: this error originates in the attribute macro `pymethods` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/tests/ui/invalid_pyfunction_signatures.rs b/tests/ui/invalid_pyfunction_signatures.rs index f5a9bee4e6c..86033aa12ea 100644 --- a/tests/ui/invalid_pyfunction_signatures.rs +++ b/tests/ui/invalid_pyfunction_signatures.rs @@ -35,6 +35,7 @@ fn function_with_args_sep_after_args_sep() {} #[pyo3(signature = (**kwargs, *args))] fn function_with_args_after_kwargs(kwargs: Option<&PyDict>, args: &PyTuple) { let _ = args; + let _ = kwargs; } #[pyfunction] diff --git a/tests/ui/invalid_pyfunction_signatures.stderr b/tests/ui/invalid_pyfunction_signatures.stderr index dbca169d8ea..97d0fd3b4af 100644 --- a/tests/ui/invalid_pyfunction_signatures.stderr +++ b/tests/ui/invalid_pyfunction_signatures.stderr @@ -41,19 +41,19 @@ error: `*args` not allowed after `**kwargs` | ^ error: `**kwargs_b` not allowed after `**kwargs_a` - --> tests/ui/invalid_pyfunction_signatures.rs:41:33 + --> tests/ui/invalid_pyfunction_signatures.rs:42:33 | -41 | #[pyo3(signature = (**kwargs_a, **kwargs_b))] +42 | #[pyo3(signature = (**kwargs_a, **kwargs_b))] | ^ error: arguments of type `Python` must not be part of the signature - --> tests/ui/invalid_pyfunction_signatures.rs:47:27 + --> tests/ui/invalid_pyfunction_signatures.rs:48:27 | -47 | #[pyfunction(signature = (py))] +48 | #[pyfunction(signature = (py))] | ^^ error: cannot find attribute `args` in this scope - --> tests/ui/invalid_pyfunction_signatures.rs:57:7 + --> tests/ui/invalid_pyfunction_signatures.rs:58:7 | -57 | #[args(x)] +58 | #[args(x)] | ^^^^ diff --git a/tests/ui/invalid_pyfunctions.rs b/tests/ui/invalid_pyfunctions.rs index 1a95c9e4a34..1c0c45d6b95 100644 --- a/tests/ui/invalid_pyfunctions.rs +++ b/tests/ui/invalid_pyfunctions.rs @@ -2,35 +2,39 @@ use pyo3::prelude::*; use pyo3::types::{PyDict, PyString, PyTuple}; #[pyfunction] -fn generic_function(value: T) {} +fn generic_function(_value: T) {} #[pyfunction] -fn impl_trait_function(impl_trait: impl AsRef) {} +fn impl_trait_function(_impl_trait: impl AsRef) {} #[pyfunction] fn wildcard_argument(_: i32) {} #[pyfunction] -fn destructured_argument((a, b): (i32, i32)) {} +fn destructured_argument((_a, _b): (i32, i32)) {} #[pyfunction] fn function_with_required_after_option(_opt: Option, _x: i32) {} #[pyfunction] #[pyo3(signature=(*args))] -fn function_with_optional_args(args: Option>) {} +fn function_with_optional_args(args: Option>) { + let _ = args; +} #[pyfunction] #[pyo3(signature=(**kwargs))] -fn function_with_required_kwargs(kwargs: Bound<'_, PyDict>) {} +fn function_with_required_kwargs(kwargs: Bound<'_, PyDict>) { + let _ = kwargs; +} #[pyfunction(pass_module)] fn pass_module_but_no_arguments<'py>() {} #[pyfunction(pass_module)] fn first_argument_not_module<'a, 'py>( - string: &str, - module: &'a Bound<'_, PyModule>, + _string: &str, + module: &'a Bound<'py, PyModule>, ) -> PyResult> { module.name() } diff --git a/tests/ui/invalid_pyfunctions.stderr b/tests/ui/invalid_pyfunctions.stderr index 893d7cbec2c..830f17ee877 100644 --- a/tests/ui/invalid_pyfunctions.stderr +++ b/tests/ui/invalid_pyfunctions.stderr @@ -1,14 +1,14 @@ error: Python functions cannot have generic type parameters --> tests/ui/invalid_pyfunctions.rs:5:21 | -5 | fn generic_function(value: T) {} +5 | fn generic_function(_value: T) {} | ^ error: Python functions cannot have `impl Trait` arguments - --> tests/ui/invalid_pyfunctions.rs:8:36 + --> tests/ui/invalid_pyfunctions.rs:8:37 | -8 | fn impl_trait_function(impl_trait: impl AsRef) {} - | ^^^^ +8 | fn impl_trait_function(_impl_trait: impl AsRef) {} + | ^^^^ error: wildcard argument names are not supported --> tests/ui/invalid_pyfunctions.rs:11:22 @@ -19,8 +19,8 @@ error: wildcard argument names are not supported error: destructuring in arguments is not supported --> tests/ui/invalid_pyfunctions.rs:14:26 | -14 | fn destructured_argument((a, b): (i32, i32)) {} - | ^^^^^^ +14 | fn destructured_argument((_a, _b): (i32, i32)) {} + | ^^^^^^^^ error: required arguments after an `Option<_>` argument are ambiguous = help: add a `#[pyo3(signature)]` annotation on this function to unambiguously specify the default values for all optional parameters @@ -32,26 +32,26 @@ error: required arguments after an `Option<_>` argument are ambiguous error: args cannot be optional --> tests/ui/invalid_pyfunctions.rs:21:32 | -21 | fn function_with_optional_args(args: Option>) {} +21 | fn function_with_optional_args(args: Option>) { | ^^^^ error: kwargs must be Option<_> - --> tests/ui/invalid_pyfunctions.rs:25:34 + --> tests/ui/invalid_pyfunctions.rs:27:34 | -25 | fn function_with_required_kwargs(kwargs: Bound<'_, PyDict>) {} +27 | fn function_with_required_kwargs(kwargs: Bound<'_, PyDict>) { | ^^^^^^ error: expected `&PyModule` or `Py` as first argument with `pass_module` - --> tests/ui/invalid_pyfunctions.rs:28:37 + --> tests/ui/invalid_pyfunctions.rs:32:37 | -28 | fn pass_module_but_no_arguments<'py>() {} +32 | fn pass_module_but_no_arguments<'py>() {} | ^^ error[E0277]: the trait bound `&str: From>` is not satisfied - --> tests/ui/invalid_pyfunctions.rs:32:13 + --> tests/ui/invalid_pyfunctions.rs:36:14 | -32 | string: &str, - | ^ the trait `From>` is not implemented for `&str`, which is required by `BoundRef<'_, '_, pyo3::prelude::PyModule>: Into<_>` +36 | _string: &str, + | ^ the trait `From>` is not implemented for `&str`, which is required by `BoundRef<'_, '_, pyo3::prelude::PyModule>: Into<_>` | = help: the following other types implement trait `From`: > diff --git a/tests/ui/invalid_pymethod_receiver.rs b/tests/ui/invalid_pymethod_receiver.rs index 77832e12857..4949ff96022 100644 --- a/tests/ui/invalid_pymethod_receiver.rs +++ b/tests/ui/invalid_pymethod_receiver.rs @@ -5,7 +5,7 @@ struct MyClass {} #[pymethods] impl MyClass { - fn method_with_invalid_self_type(slf: i32, py: Python<'_>, index: u32) {} + fn method_with_invalid_self_type(_slf: i32, _py: Python<'_>, _index: u32) {} } fn main() {} diff --git a/tests/ui/invalid_pymethod_receiver.stderr b/tests/ui/invalid_pymethod_receiver.stderr index a79e289716a..2c8ec045819 100644 --- a/tests/ui/invalid_pymethod_receiver.stderr +++ b/tests/ui/invalid_pymethod_receiver.stderr @@ -1,8 +1,8 @@ -error[E0277]: the trait bound `i32: From>` is not satisfied - --> tests/ui/invalid_pymethod_receiver.rs:8:43 +error[E0277]: the trait bound `i32: TryFrom>` is not satisfied + --> tests/ui/invalid_pymethod_receiver.rs:8:44 | -8 | fn method_with_invalid_self_type(slf: i32, py: Python<'_>, index: u32) {} - | ^^^ the trait `From>` is not implemented for `i32`, which is required by `i32: TryFrom>` +8 | fn method_with_invalid_self_type(_slf: i32, _py: Python<'_>, _index: u32) {} + | ^^^ the trait `From>` is not implemented for `i32`, which is required by `i32: TryFrom>` | = help: the following other types implement trait `From`: > @@ -10,6 +10,5 @@ error[E0277]: the trait bound `i32: From>` is not sati > > > - >> = note: required for `BoundRef<'_, '_, MyClass>` to implement `Into` = note: required for `i32` to implement `TryFrom>` diff --git a/tests/ui/invalid_pymethods.rs b/tests/ui/invalid_pymethods.rs index 00bddfe2fad..41786cd7895 100644 --- a/tests/ui/invalid_pymethods.rs +++ b/tests/ui/invalid_pymethods.rs @@ -6,7 +6,7 @@ struct MyClass {} #[pymethods] impl MyClass { #[classattr] - fn class_attr_with_args(foo: i32) {} + fn class_attr_with_args(_foo: i32) {} } #[pymethods] @@ -164,23 +164,23 @@ impl MyClass { #[pymethods] impl MyClass { - fn generic_method(value: T) {} + fn generic_method(_value: T) {} } #[pymethods] impl MyClass { - fn impl_trait_method_first_arg(impl_trait: impl AsRef) {} + fn impl_trait_method_first_arg(_impl_trait: impl AsRef) {} } #[pymethods] impl MyClass { - fn impl_trait_method_second_arg(&self, impl_trait: impl AsRef) {} + fn impl_trait_method_second_arg(&self, _impl_trait: impl AsRef) {} } #[pymethods] impl MyClass { #[pyo3(pass_module)] - fn method_cannot_pass_module(&self, m: &PyModule) {} + fn method_cannot_pass_module(&self, _m: &PyModule) {} } #[pymethods] diff --git a/tests/ui/invalid_pymethods.stderr b/tests/ui/invalid_pymethods.stderr index 5bcb6aa1be6..9b090e31adc 100644 --- a/tests/ui/invalid_pymethods.stderr +++ b/tests/ui/invalid_pymethods.stderr @@ -1,8 +1,8 @@ error: #[classattr] can only have one argument (of type pyo3::Python) - --> tests/ui/invalid_pymethods.rs:9:34 + --> tests/ui/invalid_pymethods.rs:9:35 | -9 | fn class_attr_with_args(foo: i32) {} - | ^^^ +9 | fn class_attr_with_args(_foo: i32) {} + | ^^^ error: `#[classattr]` does not take any arguments --> tests/ui/invalid_pymethods.rs:14:5 @@ -144,20 +144,20 @@ error: `#[classattr]` does not take any arguments error: Python functions cannot have generic type parameters --> tests/ui/invalid_pymethods.rs:167:23 | -167 | fn generic_method(value: T) {} +167 | fn generic_method(_value: T) {} | ^ error: Python functions cannot have `impl Trait` arguments - --> tests/ui/invalid_pymethods.rs:172:48 + --> tests/ui/invalid_pymethods.rs:172:49 | -172 | fn impl_trait_method_first_arg(impl_trait: impl AsRef) {} - | ^^^^ +172 | fn impl_trait_method_first_arg(_impl_trait: impl AsRef) {} + | ^^^^ error: Python functions cannot have `impl Trait` arguments - --> tests/ui/invalid_pymethods.rs:177:56 + --> tests/ui/invalid_pymethods.rs:177:57 | -177 | fn impl_trait_method_second_arg(&self, impl_trait: impl AsRef) {} - | ^^^^ +177 | fn impl_trait_method_second_arg(&self, _impl_trait: impl AsRef) {} + | ^^^^ error: `pass_module` cannot be used on Python methods --> tests/ui/invalid_pymethods.rs:182:12 @@ -191,5 +191,4 @@ error[E0277]: the trait bound `i32: From>` is not satis > > > - >> = note: required for `BoundRef<'_, '_, PyType>` to implement `Into` diff --git a/tests/ui/invalid_pymethods_duplicates.stderr b/tests/ui/invalid_pymethods_duplicates.stderr index 38bb6f8655b..753c4b1b8dc 100644 --- a/tests/ui/invalid_pymethods_duplicates.stderr +++ b/tests/ui/invalid_pymethods_duplicates.stderr @@ -9,6 +9,40 @@ error[E0119]: conflicting implementations of trait `pyo3::impl_::pyclass::PyClas | = note: this error originates in the attribute macro `pymethods` (in Nightly builds, run with -Z macro-backtrace for more info) +error[E0277]: the trait bound `TwoNew: PyTypeInfo` is not satisfied + --> tests/ui/invalid_pymethods_duplicates.rs:9:6 + | +9 | impl TwoNew { + | ^^^^^^ the trait `PyTypeInfo` is not implemented for `TwoNew` + | + = help: the following other types implement trait `PyTypeInfo`: + PyAny + PyBool + PyByteArray + PyBytes + PyCapsule + PyCode + PyComplex + PyDate + and $N others + +error[E0277]: the trait bound `TwoNew: HasPyGilRef` is not satisfied + --> tests/ui/invalid_pymethods_duplicates.rs:9:6 + | +9 | impl TwoNew { + | ^^^^^^ the trait `PyNativeType` is not implemented for `TwoNew`, which is required by `TwoNew: HasPyGilRef` + | + = help: the trait `HasPyGilRef` is implemented for `pyo3::coroutine::Coroutine` + = note: required for `TwoNew` to implement `HasPyGilRef` +note: required by a bound in `pyo3::PyTypeInfo::NAME` + --> src/type_object.rs + | + | pub unsafe trait PyTypeInfo: Sized + HasPyGilRef { + | ^^^^^^^^^^^ required by this bound in `PyTypeInfo::NAME` + | /// Class name. + | const NAME: &'static str; + | ---- required by a bound in this associated constant + error[E0592]: duplicate definitions with name `__pymethod___new____` --> tests/ui/invalid_pymethods_duplicates.rs:8:1 | @@ -30,3 +64,71 @@ error[E0592]: duplicate definitions with name `__pymethod_func__` | other definition for `__pymethod_func__` | = note: this error originates in the attribute macro `pymethods` (in Nightly builds, run with -Z macro-backtrace for more info) + +error[E0277]: the trait bound `TwoNew: PyClass` is not satisfied + --> tests/ui/invalid_pymethods_duplicates.rs:8:1 + | +8 | #[pymethods] + | ^^^^^^^^^^^^ the trait `PyClass` is not implemented for `TwoNew` + | + = help: the trait `PyClass` is implemented for `pyo3::coroutine::Coroutine` +note: required by a bound in `PyClassInitializer` + --> src/pyclass_init.rs + | + | pub struct PyClassInitializer(PyClassInitializerImpl); + | ^^^^^^^ required by this bound in `PyClassInitializer` + = note: this error originates in the attribute macro `pymethods` (in Nightly builds, run with -Z macro-backtrace for more info) + +error[E0599]: no method named `convert` found for struct `TwoNew` in the current scope + --> tests/ui/invalid_pymethods_duplicates.rs:8:1 + | +6 | struct TwoNew {} + | ------------- method `convert` not found for this struct +7 | +8 | #[pymethods] + | ^^^^^^^^^^^^ method not found in `TwoNew` + | + = help: items from traits can only be used if the trait is implemented and in scope + = note: the following trait defines an item `convert`, perhaps you need to implement it: + candidate #1: `IntoPyCallbackOutput` + = note: this error originates in the attribute macro `pymethods` (in Nightly builds, run with -Z macro-backtrace for more info) + +error[E0277]: the trait bound `DuplicateMethod: PyClass` is not satisfied + --> tests/ui/invalid_pymethods_duplicates.rs:26:15 + | +26 | fn func_a(&self) {} + | ^ the trait `PyClass` is not implemented for `DuplicateMethod` + | + = help: the trait `PyClass` is implemented for `pyo3::coroutine::Coroutine` +note: required by a bound in `extract_pyclass_ref` + --> src/impl_/extract_argument.rs + | + | pub fn extract_pyclass_ref<'a, 'py: 'a, T: PyClass>( + | ^^^^^^^ required by this bound in `extract_pyclass_ref` + +error[E0277]: the trait bound `DuplicateMethod: PyClass` is not satisfied + --> tests/ui/invalid_pymethods_duplicates.rs:23:1 + | +23 | #[pymethods] + | ^^^^^^^^^^^^ the trait `PyClass` is not implemented for `DuplicateMethod` + | + = help: the trait `PyClass` is implemented for `pyo3::coroutine::Coroutine` +note: required by a bound in `PyRef` + --> src/pycell.rs + | + | pub struct PyRef<'p, T: PyClass> { + | ^^^^^^^ required by this bound in `PyRef` + = note: this error originates in the attribute macro `pymethods` (in Nightly builds, run with -Z macro-backtrace for more info) + +error[E0277]: the trait bound `DuplicateMethod: PyClass` is not satisfied + --> tests/ui/invalid_pymethods_duplicates.rs:29:15 + | +29 | fn func_b(&self) {} + | ^ the trait `PyClass` is not implemented for `DuplicateMethod` + | + = help: the trait `PyClass` is implemented for `pyo3::coroutine::Coroutine` +note: required by a bound in `extract_pyclass_ref` + --> src/impl_/extract_argument.rs + | + | pub fn extract_pyclass_ref<'a, 'py: 'a, T: PyClass>( + | ^^^^^^^ required by this bound in `extract_pyclass_ref` diff --git a/tests/ui/missing_intopy.stderr b/tests/ui/missing_intopy.stderr index 1d233b28b6c..c0a60143671 100644 --- a/tests/ui/missing_intopy.stderr +++ b/tests/ui/missing_intopy.stderr @@ -1,18 +1,9 @@ -error[E0277]: the trait bound `Blah: IntoPy>` is not satisfied +error[E0277]: the trait bound `Blah: OkWrap` is not satisfied --> tests/ui/missing_intopy.rs:3:1 | 3 | #[pyo3::pyfunction] | ^^^^^^^^^^^^^^^^^^^ the trait `IntoPy>` is not implemented for `Blah`, which is required by `Blah: OkWrap<_>` | - = help: the following other types implement trait `IntoPy`: - >> - >> - >> - >> - >> - >> - >> - >> - and $N others + = help: the trait `OkWrap` is implemented for `Result` = note: required for `Blah` to implement `OkWrap` = note: this error originates in the attribute macro `pyo3::pyfunction` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/tests/ui/pyclass_send.rs b/tests/ui/pyclass_send.rs index 2747c2cb3bb..a587c071f51 100644 --- a/tests/ui/pyclass_send.rs +++ b/tests/ui/pyclass_send.rs @@ -10,7 +10,7 @@ fn main() { let obj = Python::with_gil(|py| { Bound::new(py, NotThreadSafe { data: Rc::new(5) }) .unwrap() - .unbind(py) + .unbind() }); std::thread::spawn(move || { diff --git a/tests/ui/traverse.rs b/tests/ui/traverse.rs index 0cf7170db21..faa7b5c041d 100644 --- a/tests/ui/traverse.rs +++ b/tests/ui/traverse.rs @@ -7,7 +7,7 @@ struct TraverseTriesToTakePyRef {} #[pymethods] impl TraverseTriesToTakePyRef { - fn __traverse__(slf: PyRef, visit: PyVisit) -> Result<(), PyTraverseError> { + fn __traverse__(_slf: PyRef, _visit: PyVisit) -> Result<(), PyTraverseError> { Ok(()) } } @@ -17,7 +17,7 @@ struct TraverseTriesToTakePyRefMut {} #[pymethods] impl TraverseTriesToTakePyRefMut { - fn __traverse__(slf: PyRefMut, visit: PyVisit) -> Result<(), PyTraverseError> { + fn __traverse__(_slf: PyRefMut, _visit: PyVisit) -> Result<(), PyTraverseError> { Ok(()) } } @@ -27,7 +27,7 @@ struct TraverseTriesToTakeBound {} #[pymethods] impl TraverseTriesToTakeBound { - fn __traverse__(slf: Bound<'_, Self>, visit: PyVisit) -> Result<(), PyTraverseError> { + fn __traverse__(_slf: Bound<'_, Self>, _visit: PyVisit) -> Result<(), PyTraverseError> { Ok(()) } } @@ -37,7 +37,7 @@ struct TraverseTriesToTakeMutSelf {} #[pymethods] impl TraverseTriesToTakeMutSelf { - fn __traverse__(&mut self, visit: PyVisit) -> Result<(), PyTraverseError> { + fn __traverse__(&mut self, _visit: PyVisit) -> Result<(), PyTraverseError> { Ok(()) } } @@ -47,7 +47,7 @@ struct TraverseTriesToTakeSelf {} #[pymethods] impl TraverseTriesToTakeSelf { - fn __traverse__(&self, visit: PyVisit) -> Result<(), PyTraverseError> { + fn __traverse__(&self, _visit: PyVisit) -> Result<(), PyTraverseError> { Ok(()) } } @@ -57,7 +57,7 @@ struct Class; #[pymethods] impl Class { - fn __traverse__(&self, py: Python<'_>, visit: PyVisit<'_>) -> Result<(), PyTraverseError> { + fn __traverse__(&self, _py: Python<'_>, _visit: PyVisit<'_>) -> Result<(), PyTraverseError> { Ok(()) } diff --git a/tests/ui/traverse.stderr b/tests/ui/traverse.stderr index 5b1d1b6b2ec..504a6dfa671 100644 --- a/tests/ui/traverse.stderr +++ b/tests/ui/traverse.stderr @@ -1,29 +1,29 @@ error: __traverse__ may not take a receiver other than `&self`. Usually, an implementation of `__traverse__(&self, visit: PyVisit<'_>) -> Result<(), PyTraverseError>` should do nothing but calls to `visit.call`. Most importantly, safe access to the GIL is prohibited inside implementations of `__traverse__`, i.e. `Python::with_gil` will panic. - --> tests/ui/traverse.rs:10:26 + --> tests/ui/traverse.rs:10:27 | -10 | fn __traverse__(slf: PyRef, visit: PyVisit) -> Result<(), PyTraverseError> { - | ^^^^^ +10 | fn __traverse__(_slf: PyRef, _visit: PyVisit) -> Result<(), PyTraverseError> { + | ^^^^^ error: __traverse__ may not take a receiver other than `&self`. Usually, an implementation of `__traverse__(&self, visit: PyVisit<'_>) -> Result<(), PyTraverseError>` should do nothing but calls to `visit.call`. Most importantly, safe access to the GIL is prohibited inside implementations of `__traverse__`, i.e. `Python::with_gil` will panic. - --> tests/ui/traverse.rs:20:26 + --> tests/ui/traverse.rs:20:27 | -20 | fn __traverse__(slf: PyRefMut, visit: PyVisit) -> Result<(), PyTraverseError> { - | ^^^^^^^^ +20 | fn __traverse__(_slf: PyRefMut, _visit: PyVisit) -> Result<(), PyTraverseError> { + | ^^^^^^^^ error: __traverse__ may not take a receiver other than `&self`. Usually, an implementation of `__traverse__(&self, visit: PyVisit<'_>) -> Result<(), PyTraverseError>` should do nothing but calls to `visit.call`. Most importantly, safe access to the GIL is prohibited inside implementations of `__traverse__`, i.e. `Python::with_gil` will panic. - --> tests/ui/traverse.rs:30:26 + --> tests/ui/traverse.rs:30:27 | -30 | fn __traverse__(slf: Bound<'_, Self>, visit: PyVisit) -> Result<(), PyTraverseError> { - | ^^^^^ +30 | fn __traverse__(_slf: Bound<'_, Self>, _visit: PyVisit) -> Result<(), PyTraverseError> { + | ^^^^^ error: __traverse__ may not take a receiver other than `&self`. Usually, an implementation of `__traverse__(&self, visit: PyVisit<'_>) -> Result<(), PyTraverseError>` should do nothing but calls to `visit.call`. Most importantly, safe access to the GIL is prohibited inside implementations of `__traverse__`, i.e. `Python::with_gil` will panic. --> tests/ui/traverse.rs:40:21 | -40 | fn __traverse__(&mut self, visit: PyVisit) -> Result<(), PyTraverseError> { +40 | fn __traverse__(&mut self, _visit: PyVisit) -> Result<(), PyTraverseError> { | ^ error: __traverse__ may not take `Python`. Usually, an implementation of `__traverse__(&self, visit: PyVisit<'_>) -> Result<(), PyTraverseError>` should do nothing but calls to `visit.call`. Most importantly, safe access to the GIL is prohibited inside implementations of `__traverse__`, i.e. `Python::with_gil` will panic. - --> tests/ui/traverse.rs:60:32 + --> tests/ui/traverse.rs:60:33 | -60 | fn __traverse__(&self, py: Python<'_>, visit: PyVisit<'_>) -> Result<(), PyTraverseError> { - | ^^^^^^^^^^ +60 | fn __traverse__(&self, _py: Python<'_>, _visit: PyVisit<'_>) -> Result<(), PyTraverseError> { + | ^^^^^^^^^^ From 7cbb85476c920cb80d2906b08b2a25d96fe4b5c8 Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Fri, 3 May 2024 12:17:14 +0200 Subject: [PATCH 051/495] fix `check-guide` ci workflow (#4146) --- noxfile.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/noxfile.py b/noxfile.py index c8d2ead63d3..84676b1ff0c 100644 --- a/noxfile.py +++ b/noxfile.py @@ -415,10 +415,11 @@ def check_guide(session: nox.Session): _run( session, "lychee", - PYO3_DOCS_TARGET, + str(PYO3_DOCS_TARGET), f"--remap=https://pyo3.rs/main/ file://{PYO3_GUIDE_TARGET}/", f"--remap=https://pyo3.rs/latest/ file://{PYO3_GUIDE_TARGET}/", f"--exclude=file://{PYO3_DOCS_TARGET}", + "--exclude=http://www.adobe.com/", *session.posargs, ) From 93cfb51ebbdfb598e4a08539155ac4d9e9194a28 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Fri, 3 May 2024 12:02:19 -0400 Subject: [PATCH 052/495] feature gate deprecated APIs for `PyMemoryView` (#4152) --- src/types/memoryview.rs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/types/memoryview.rs b/src/types/memoryview.rs index c04a98e7886..ffc1c81efa0 100644 --- a/src/types/memoryview.rs +++ b/src/types/memoryview.rs @@ -11,12 +11,10 @@ pyobject_native_type_core!(PyMemoryView, pyobject_native_static_type_object!(ffi impl PyMemoryView { /// Deprecated form of [`PyMemoryView::from_bound`] - #[cfg_attr( - not(feature = "gil-refs"), - deprecated( - since = "0.21.0", - note = "`PyMemoryView::from` will be replaced by `PyMemoryView::from_bound` in a future PyO3 version" - ) + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`PyMemoryView::from` will be replaced by `PyMemoryView::from_bound` in a future PyO3 version" )] pub fn from(src: &PyAny) -> PyResult<&PyMemoryView> { PyMemoryView::from_bound(&src.as_borrowed()).map(Bound::into_gil_ref) From f3ab62cb7ec17752e776334957b00894ede97d37 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Fri, 3 May 2024 13:10:49 -0400 Subject: [PATCH 053/495] feature gate deprecated APIs for `PyModule` (#4151) --- .../python-from-rust/calling-existing-code.md | 12 +++---- src/marker.rs | 2 +- src/types/module.rs | 31 +++++++------------ 3 files changed, 19 insertions(+), 26 deletions(-) diff --git a/guide/src/python-from-rust/calling-existing-code.md b/guide/src/python-from-rust/calling-existing-code.md index 53051d4ce51..572f4b4414f 100644 --- a/guide/src/python-from-rust/calling-existing-code.md +++ b/guide/src/python-from-rust/calling-existing-code.md @@ -2,9 +2,9 @@ If you already have some existing Python code that you need to execute from Rust, the following FAQs can help you select the right PyO3 functionality for your situation: -## Want to access Python APIs? Then use `PyModule::import`. +## Want to access Python APIs? Then use `PyModule::import_bound`. -[`Pymodule::import`]({{#PYO3_DOCS_URL}}/pyo3/types/struct.PyModule.html#method.import) can +[`PyModule::import_bound`]({{#PYO3_DOCS_URL}}/pyo3/types/struct.PyModule.html#method.import_bound) can be used to get handle to a Python module from Rust. You can use this to import and use any Python module available in your environment. @@ -95,9 +95,9 @@ assert userdata.as_tuple() == userdata_as_tuple # } ``` -## You have a Python file or code snippet? Then use `PyModule::from_code`. +## You have a Python file or code snippet? Then use `PyModule::from_code_bound`. -[`PyModule::from_code`]({{#PYO3_DOCS_URL}}/pyo3/types/struct.PyModule.html#method.from_code) +[`PyModule::from_code_bound`]({{#PYO3_DOCS_URL}}/pyo3/types/struct.PyModule.html#method.from_code_bound) can be used to generate a Python module which can then be used just as if it was imported with `PyModule::import`. @@ -171,7 +171,7 @@ fn main() -> PyResult<()> { ``` If `append_to_inittab` cannot be used due to constraints in the program, -an alternative is to create a module using [`PyModule::new`] +an alternative is to create a module using [`PyModule::new_bound`] and insert it manually into `sys.modules`: ```rust @@ -394,4 +394,4 @@ Python::with_gil(|py| -> PyResult<()> { ``` -[`PyModule::new`]: {{#PYO3_DOCS_URL}}/pyo3/types/struct.PyModule.html#method.new +[`PyModule::new_bound`]: {{#PYO3_DOCS_URL}}/pyo3/types/struct.PyModule.html#method.new_bound diff --git a/src/marker.rs b/src/marker.rs index 29aae69d4f2..c975a4a338e 100644 --- a/src/marker.rs +++ b/src/marker.rs @@ -352,7 +352,7 @@ pub use nightly::Ungil; /// # Releasing and freeing memory /// /// The [`Python`] type can be used to create references to variables owned by the Python -/// interpreter, using functions such as [`Python::eval`] and [`PyModule::import`]. These +/// interpreter, using functions such as [`Python::eval`] and `PyModule::import`. These /// references are tied to a [`GILPool`] whose references are not cleared until it is dropped. /// This can cause apparent "memory leaks" if it is kept around for a long time. /// diff --git a/src/types/module.rs b/src/types/module.rs index 11afcf76c83..c8b2cf04551 100644 --- a/src/types/module.rs +++ b/src/types/module.rs @@ -27,12 +27,10 @@ pyobject_native_type_core!(PyModule, pyobject_native_static_type_object!(ffi::Py impl PyModule { /// Deprecated form of [`PyModule::new_bound`]. #[inline] - #[cfg_attr( - not(feature = "gil-refs"), - deprecated( - since = "0.21.0", - note = "`PyModule::new` will be replaced by `PyModule::new_bound` in a future PyO3 version" - ) + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`PyModule::new` will be replaced by `PyModule::new_bound` in a future PyO3 version" )] pub fn new<'py>(py: Python<'py>, name: &str) -> PyResult<&'py PyModule> { Self::new_bound(py, name).map(Bound::into_gil_ref) @@ -66,12 +64,10 @@ impl PyModule { /// Deprecated form of [`PyModule::import_bound`]. #[inline] - #[cfg_attr( - not(feature = "gil-refs"), - deprecated( - since = "0.21.0", - note = "`PyModule::import` will be replaced by `PyModule::import_bound` in a future PyO3 version" - ) + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`PyModule::import` will be replaced by `PyModule::import_bound` in a future PyO3 version" )] pub fn import(py: Python<'_>, name: N) -> PyResult<&PyModule> where @@ -112,12 +108,10 @@ impl PyModule { /// Deprecated form of [`PyModule::from_code_bound`]. #[inline] - #[cfg_attr( - not(feature = "gil-refs"), - deprecated( - since = "0.21.0", - note = "`PyModule::from_code` will be replaced by `PyModule::from_code_bound` in a future PyO3 version" - ) + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`PyModule::from_code` will be replaced by `PyModule::from_code_bound` in a future PyO3 version" )] pub fn from_code<'py>( py: Python<'py>, @@ -722,7 +716,6 @@ fn __name__(py: Python<'_>) -> &Bound<'_, PyString> { } #[cfg(test)] -#[cfg_attr(not(feature = "gil-refs"), allow(deprecated))] mod tests { use crate::{ types::{module::PyModuleMethods, string::PyStringMethods, PyModule}, From c08f6c77a60be2c5cdfc5216aa8869ff48738d51 Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Fri, 3 May 2024 20:15:25 +0200 Subject: [PATCH 054/495] feature gate deprecated APIs for `marshal` (#4149) --- src/marshal.rs | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/src/marshal.rs b/src/marshal.rs index 9526813976b..3978f4873e1 100644 --- a/src/marshal.rs +++ b/src/marshal.rs @@ -13,12 +13,10 @@ use std::os::raw::c_int; pub const VERSION: i32 = 4; /// Deprecated form of [`dumps_bound`] -#[cfg_attr( - not(feature = "gil-refs"), - deprecated( - since = "0.21.0", - note = "`dumps` will be replaced by `dumps_bound` in a future PyO3 version" - ) +#[cfg(feature = "gil-refs")] +#[deprecated( + since = "0.21.0", + note = "`dumps` will be replaced by `dumps_bound` in a future PyO3 version" )] pub fn dumps<'py>( py: Python<'py>, @@ -61,12 +59,10 @@ pub fn dumps_bound<'py>( } /// Deprecated form of [`loads_bound`] -#[cfg_attr( - not(feature = "gil-refs"), - deprecated( - since = "0.21.0", - note = "`loads` will be replaced by `loads_bound` in a future PyO3 version" - ) +#[cfg(feature = "gil-refs")] +#[deprecated( + since = "0.21.0", + note = "`loads` will be replaced by `loads_bound` in a future PyO3 version" )] pub fn loads<'py, B>(py: Python<'py>, data: &B) -> PyResult<&'py PyAny> where From d1a0c7278f16925c9075101c763d73bbc5f673c5 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Fri, 3 May 2024 15:50:38 -0400 Subject: [PATCH 055/495] feature gate deprecated APIs for `PyCFunction` (#4154) --- src/types/function.rs | 35 ++++++++++++++++------------------- 1 file changed, 16 insertions(+), 19 deletions(-) diff --git a/src/types/function.rs b/src/types/function.rs index f5305e31886..09c5004b77b 100644 --- a/src/types/function.rs +++ b/src/types/function.rs @@ -1,14 +1,17 @@ +#[cfg(feature = "gil-refs")] use crate::derive_utils::PyFunctionArguments; use crate::ffi_ptr_ext::FfiPtrExt; use crate::py_result_ext::PyResultExt; use crate::types::capsule::PyCapsuleMethods; use crate::types::module::PyModuleMethods; +#[cfg(feature = "gil-refs")] +use crate::PyNativeType; use crate::{ ffi, impl_::pymethods::{self, PyMethodDef, PyMethodDefDestructor}, types::{PyCapsule, PyDict, PyModule, PyString, PyTuple}, }; -use crate::{Bound, IntoPy, Py, PyAny, PyNativeType, PyResult, Python}; +use crate::{Bound, IntoPy, Py, PyAny, PyResult, Python}; use std::cell::UnsafeCell; use std::ffi::CStr; @@ -20,12 +23,10 @@ pyobject_native_type_core!(PyCFunction, pyobject_native_static_type_object!(ffi: impl PyCFunction { /// Deprecated form of [`PyCFunction::new_with_keywords_bound`] - #[cfg_attr( - not(feature = "gil-refs"), - deprecated( - since = "0.21.0", - note = "`PyCFunction::new_with_keywords` will be replaced by `PyCFunction::new_with_keywords_bound` in a future PyO3 version" - ) + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`PyCFunction::new_with_keywords` will be replaced by `PyCFunction::new_with_keywords_bound` in a future PyO3 version" )] pub fn new_with_keywords<'a>( fun: ffi::PyCFunctionWithKeywords, @@ -66,12 +67,10 @@ impl PyCFunction { } /// Deprecated form of [`PyCFunction::new`] - #[cfg_attr( - not(feature = "gil-refs"), - deprecated( - since = "0.21.0", - note = "`PyCFunction::new` will be replaced by `PyCFunction::new_bound` in a future PyO3 version" - ) + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`PyCFunction::new` will be replaced by `PyCFunction::new_bound` in a future PyO3 version" )] pub fn new<'a>( fun: ffi::PyCFunction, @@ -104,12 +103,10 @@ impl PyCFunction { } /// Deprecated form of [`PyCFunction::new_closure`] - #[cfg_attr( - not(feature = "gil-refs"), - deprecated( - since = "0.21.0", - note = "`PyCFunction::new_closure` will be replaced by `PyCFunction::new_closure_bound` in a future PyO3 version" - ) + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`PyCFunction::new_closure` will be replaced by `PyCFunction::new_closure_bound` in a future PyO3 version" )] pub fn new_closure<'a, F, R>( py: Python<'a>, From c10c7429d8b5f16a6c28650b21e214f35cbbfe48 Mon Sep 17 00:00:00 2001 From: Heran Lin Date: Sat, 4 May 2024 15:32:27 +0800 Subject: [PATCH 056/495] docs: Remove out-dated information for pyenv (#4138) --- guide/src/building-and-distribution.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/guide/src/building-and-distribution.md b/guide/src/building-and-distribution.md index b7aa2d2abc2..33280a5a180 100644 --- a/guide/src/building-and-distribution.md +++ b/guide/src/building-and-distribution.md @@ -232,7 +232,7 @@ not work when compiling for `abi3`. These are: If you want to embed the Python interpreter inside a Rust program, there are two modes in which this can be done: dynamically and statically. We'll cover each of these modes in the following sections. Each of them affect how you must distribute your program. Instead of learning how to do this yourself, you might want to consider using a project like [PyOxidizer] to ship your application and all of its dependencies in a single file. -PyO3 automatically switches between the two linking modes depending on whether the Python distribution you have configured PyO3 to use ([see above](#configuring-the-python-version)) contains a shared library or a static library. The static library is most often seen in Python distributions compiled from source without the `--enable-shared` configuration option. For example, this is the default for `pyenv` on macOS. +PyO3 automatically switches between the two linking modes depending on whether the Python distribution you have configured PyO3 to use ([see above](#configuring-the-python-version)) contains a shared library or a static library. The static library is most often seen in Python distributions compiled from source without the `--enable-shared` configuration option. ### Dynamically embedding the Python interpreter From e835ff0ec30e0e391fdfcbef017241004ed9d896 Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Sat, 4 May 2024 09:39:40 +0200 Subject: [PATCH 057/495] handle `#[pyo3(from_py_with = ...)]` on dunder (`__magic__`) methods (#4117) * handle `#[pyo3(from_py_with = ...)]` on dunder (__magic__) methods * add newsfragment --- newsfragments/4117.fixed.md | 1 + pyo3-macros-backend/src/params.rs | 34 +++++++++++++++++--- pyo3-macros-backend/src/pymethod.rs | 42 ++++++++++++++---------- tests/test_class_basics.rs | 11 +++++++ tests/ui/deprecations.rs | 8 +++++ tests/ui/deprecations.stderr | 50 ++++++++++++++++------------- 6 files changed, 103 insertions(+), 43 deletions(-) create mode 100644 newsfragments/4117.fixed.md diff --git a/newsfragments/4117.fixed.md b/newsfragments/4117.fixed.md new file mode 100644 index 00000000000..c3bb4c144b6 --- /dev/null +++ b/newsfragments/4117.fixed.md @@ -0,0 +1 @@ +Correctly handle `#[pyo3(from_py_with = ...)]` attribute on dunder (`__magic__`) method arguments instead of silently ignoring it. diff --git a/pyo3-macros-backend/src/params.rs b/pyo3-macros-backend/src/params.rs index d9f77fa07bc..cab9d2a7d29 100644 --- a/pyo3-macros-backend/src/params.rs +++ b/pyo3-macros-backend/src/params.rs @@ -10,7 +10,7 @@ use syn::spanned::Spanned; pub struct Holders { holders: Vec, - gil_refs_checkers: Vec, + gil_refs_checkers: Vec, } impl Holders { @@ -32,14 +32,28 @@ impl Holders { &format!("gil_refs_checker_{}", self.gil_refs_checkers.len()), span, ); - self.gil_refs_checkers.push(gil_refs_checker.clone()); + self.gil_refs_checkers + .push(GilRefChecker::FunctionArg(gil_refs_checker.clone())); + gil_refs_checker + } + + pub fn push_from_py_with_checker(&mut self, span: Span) -> syn::Ident { + let gil_refs_checker = syn::Ident::new( + &format!("gil_refs_checker_{}", self.gil_refs_checkers.len()), + span, + ); + self.gil_refs_checkers + .push(GilRefChecker::FromPyWith(gil_refs_checker.clone())); gil_refs_checker } pub fn init_holders(&self, ctx: &Ctx) -> TokenStream { let Ctx { pyo3_path } = ctx; let holders = &self.holders; - let gil_refs_checkers = &self.gil_refs_checkers; + let gil_refs_checkers = self.gil_refs_checkers.iter().map(|checker| match checker { + GilRefChecker::FunctionArg(ident) => ident, + GilRefChecker::FromPyWith(ident) => ident, + }); quote! { #[allow(clippy::let_unit_value)] #(let mut #holders = #pyo3_path::impl_::extract_argument::FunctionArgumentHolder::INIT;)* @@ -50,11 +64,23 @@ impl Holders { pub fn check_gil_refs(&self) -> TokenStream { self.gil_refs_checkers .iter() - .map(|e| quote_spanned! { e.span() => #e.function_arg(); }) + .map(|checker| match checker { + GilRefChecker::FunctionArg(ident) => { + quote_spanned! { ident.span() => #ident.function_arg(); } + } + GilRefChecker::FromPyWith(ident) => { + quote_spanned! { ident.span() => #ident.from_py_with_arg(); } + } + }) .collect() } } +enum GilRefChecker { + FunctionArg(syn::Ident), + FromPyWith(syn::Ident), +} + /// Return true if the argument list is simply (*args, **kwds). pub fn is_forwarded_args(signature: &FunctionSignature<'_>) -> bool { matches!( diff --git a/pyo3-macros-backend/src/pymethod.rs b/pyo3-macros-backend/src/pymethod.rs index aac804316f8..1ef137cfcc8 100644 --- a/pyo3-macros-backend/src/pymethod.rs +++ b/pyo3-macros-backend/src/pymethod.rs @@ -1053,20 +1053,18 @@ impl Ty { ctx: &Ctx, ) -> TokenStream { let Ctx { pyo3_path } = ctx; - let name_str = arg.name().unraw().to_string(); match self { Ty::Object => extract_object( extract_error_mode, holders, - &name_str, + arg, quote! { #ident }, - arg.ty().span(), ctx ), Ty::MaybeNullObject => extract_object( extract_error_mode, holders, - &name_str, + arg, quote! { if #ident.is_null() { #pyo3_path::ffi::Py_None() @@ -1074,23 +1072,20 @@ impl Ty { #ident } }, - arg.ty().span(), ctx ), Ty::NonNullObject => extract_object( extract_error_mode, holders, - &name_str, + arg, quote! { #ident.as_ptr() }, - arg.ty().span(), ctx ), Ty::IPowModulo => extract_object( extract_error_mode, holders, - &name_str, + arg, quote! { #ident.as_ptr() }, - arg.ty().span(), ctx ), Ty::CompareOp => extract_error_mode.handle_error( @@ -1118,24 +1113,37 @@ impl Ty { fn extract_object( extract_error_mode: ExtractErrorMode, holders: &mut Holders, - name: &str, + arg: &FnArg<'_>, source_ptr: TokenStream, - span: Span, ctx: &Ctx, ) -> TokenStream { let Ctx { pyo3_path } = ctx; - let holder = holders.push_holder(Span::call_site()); - let gil_refs_checker = holders.push_gil_refs_checker(span); - let extracted = extract_error_mode.handle_error( + let gil_refs_checker = holders.push_gil_refs_checker(arg.ty().span()); + let name = arg.name().unraw().to_string(); + + let extract = if let Some(from_py_with) = + arg.from_py_with().map(|from_py_with| &from_py_with.value) + { + let from_py_with_checker = holders.push_from_py_with_checker(from_py_with.span()); + quote! { + #pyo3_path::impl_::extract_argument::from_py_with( + #pyo3_path::impl_::pymethods::BoundRef::ref_from_ptr(py, &#source_ptr).0, + #name, + #pyo3_path::impl_::deprecations::inspect_fn(#from_py_with, &#from_py_with_checker) as fn(_) -> _, + ) + } + } else { + let holder = holders.push_holder(Span::call_site()); quote! { #pyo3_path::impl_::extract_argument::extract_argument( #pyo3_path::impl_::pymethods::BoundRef::ref_from_ptr(py, &#source_ptr).0, &mut #holder, #name ) - }, - ctx, - ); + } + }; + + let extracted = extract_error_mode.handle_error(extract, ctx); quote! { #pyo3_path::impl_::deprecations::inspect_type(#extracted, &#gil_refs_checker) } diff --git a/tests/test_class_basics.rs b/tests/test_class_basics.rs index 8c6a1c04915..8ff61bd2d6b 100644 --- a/tests/test_class_basics.rs +++ b/tests/test_class_basics.rs @@ -290,6 +290,10 @@ fn get_length(obj: &Bound<'_, PyAny>) -> PyResult { Ok(length) } +fn is_even(obj: &Bound<'_, PyAny>) -> PyResult { + obj.extract::().map(|i| i % 2 == 0) +} + #[pyclass] struct ClassWithFromPyWithMethods {} @@ -319,6 +323,10 @@ impl ClassWithFromPyWithMethods { fn staticmethod(#[pyo3(from_py_with = "get_length")] argument: usize) -> usize { argument } + + fn __contains__(&self, #[pyo3(from_py_with = "is_even")] obj: bool) -> bool { + obj + } } #[test] @@ -339,6 +347,9 @@ fn test_pymethods_from_py_with() { if has_gil_refs: assert instance.classmethod_gil_ref(arg) == 2 assert instance.staticmethod(arg) == 2 + + assert 42 in instance + assert 73 not in instance "# ); }) diff --git a/tests/ui/deprecations.rs b/tests/ui/deprecations.rs index dcc9b7b1d84..96f652d9679 100644 --- a/tests/ui/deprecations.rs +++ b/tests/ui/deprecations.rs @@ -38,6 +38,14 @@ impl MyClass { #[setter] fn set_bar_bound(&self, _value: &Bound<'_, PyAny>) {} + + fn __eq__(&self, #[pyo3(from_py_with = "extract_gil_ref")] _other: i32) -> bool { + true + } + + fn __contains__(&self, #[pyo3(from_py_with = "extract_bound")] _value: i32) -> bool { + true + } } fn main() {} diff --git a/tests/ui/deprecations.stderr b/tests/ui/deprecations.stderr index e692702f23e..2b75ee23e10 100644 --- a/tests/ui/deprecations.stderr +++ b/tests/ui/deprecations.stderr @@ -16,6 +16,12 @@ error: use of deprecated struct `pyo3::PyCell`: `PyCell` was merged into `Bound` 23 | fn method_gil_ref(_slf: &PyCell) {} | ^^^^^^ +error: use of deprecated method `pyo3::deprecations::GilRefs::::from_py_with_arg`: use `&Bound<'_, PyAny>` as the argument for this `from_py_with` extractor + --> tests/ui/deprecations.rs:42:44 + | +42 | fn __eq__(&self, #[pyo3(from_py_with = "extract_gil_ref")] _other: i32) -> bool { + | ^^^^^^^^^^^^^^^^^ + error: use of deprecated method `pyo3::deprecations::GilRefs::::function_arg`: use `&Bound<'_, T>` instead for this function argument --> tests/ui/deprecations.rs:18:33 | @@ -47,69 +53,69 @@ error: use of deprecated method `pyo3::deprecations::GilRefs::::function_arg` | ^ error: use of deprecated method `pyo3::deprecations::GilRefs::::function_arg`: use `&Bound<'_, T>` instead for this function argument - --> tests/ui/deprecations.rs:53:43 + --> tests/ui/deprecations.rs:61:43 | -53 | fn pyfunction_with_module_gil_ref(module: &PyModule) -> PyResult<&str> { +61 | fn pyfunction_with_module_gil_ref(module: &PyModule) -> PyResult<&str> { | ^ error: use of deprecated method `pyo3::deprecations::GilRefs::::function_arg`: use `&Bound<'_, T>` instead for this function argument - --> tests/ui/deprecations.rs:63:19 + --> tests/ui/deprecations.rs:71:19 | -63 | fn module_gil_ref(m: &PyModule) -> PyResult<()> { +71 | fn module_gil_ref(m: &PyModule) -> PyResult<()> { | ^ error: use of deprecated method `pyo3::deprecations::GilRefs::::function_arg`: use `&Bound<'_, T>` instead for this function argument - --> tests/ui/deprecations.rs:69:57 + --> tests/ui/deprecations.rs:77:57 | -69 | fn module_gil_ref_with_explicit_py_arg(_py: Python<'_>, m: &PyModule) -> PyResult<()> { +77 | fn module_gil_ref_with_explicit_py_arg(_py: Python<'_>, m: &PyModule) -> PyResult<()> { | ^ error: use of deprecated method `pyo3::deprecations::GilRefs::::from_py_with_arg`: use `&Bound<'_, PyAny>` as the argument for this `from_py_with` extractor - --> tests/ui/deprecations.rs:102:27 + --> tests/ui/deprecations.rs:110:27 | -102 | #[pyo3(from_py_with = "extract_gil_ref")] _gil_ref: i32, +110 | #[pyo3(from_py_with = "extract_gil_ref")] _gil_ref: i32, | ^^^^^^^^^^^^^^^^^ error: use of deprecated method `pyo3::deprecations::GilRefs::::function_arg`: use `&Bound<'_, T>` instead for this function argument - --> tests/ui/deprecations.rs:108:29 + --> tests/ui/deprecations.rs:116:29 | -108 | fn pyfunction_gil_ref(_any: &PyAny) {} +116 | fn pyfunction_gil_ref(_any: &PyAny) {} | ^ error: use of deprecated method `pyo3::deprecations::OptionGilRefs::>::function_arg`: use `Option<&Bound<'_, T>>` instead for this function argument - --> tests/ui/deprecations.rs:111:36 + --> tests/ui/deprecations.rs:119:36 | -111 | fn pyfunction_option_gil_ref(_any: Option<&PyAny>) {} +119 | fn pyfunction_option_gil_ref(_any: Option<&PyAny>) {} | ^^^^^^ error: use of deprecated method `pyo3::deprecations::GilRefs::::from_py_with_arg`: use `&Bound<'_, PyAny>` as the argument for this `from_py_with` extractor - --> tests/ui/deprecations.rs:118:27 + --> tests/ui/deprecations.rs:126:27 | -118 | #[pyo3(from_py_with = "PyAny::len", item("my_object"))] +126 | #[pyo3(from_py_with = "PyAny::len", item("my_object"))] | ^^^^^^^^^^^^ error: use of deprecated method `pyo3::deprecations::GilRefs::::from_py_with_arg`: use `&Bound<'_, PyAny>` as the argument for this `from_py_with` extractor - --> tests/ui/deprecations.rs:128:27 + --> tests/ui/deprecations.rs:136:27 | -128 | #[pyo3(from_py_with = "PyAny::len")] usize, +136 | #[pyo3(from_py_with = "PyAny::len")] usize, | ^^^^^^^^^^^^ error: use of deprecated method `pyo3::deprecations::GilRefs::::from_py_with_arg`: use `&Bound<'_, PyAny>` as the argument for this `from_py_with` extractor - --> tests/ui/deprecations.rs:134:31 + --> tests/ui/deprecations.rs:142:31 | -134 | Zip(#[pyo3(from_py_with = "extract_gil_ref")] i32), +142 | Zip(#[pyo3(from_py_with = "extract_gil_ref")] i32), | ^^^^^^^^^^^^^^^^^ error: use of deprecated method `pyo3::deprecations::GilRefs::::from_py_with_arg`: use `&Bound<'_, PyAny>` as the argument for this `from_py_with` extractor - --> tests/ui/deprecations.rs:141:27 + --> tests/ui/deprecations.rs:149:27 | -141 | #[pyo3(from_py_with = "extract_gil_ref")] +149 | #[pyo3(from_py_with = "extract_gil_ref")] | ^^^^^^^^^^^^^^^^^ error: use of deprecated method `pyo3::deprecations::GilRefs::>::is_python`: use `wrap_pyfunction_bound!` instead - --> tests/ui/deprecations.rs:154:13 + --> tests/ui/deprecations.rs:162:13 | -154 | let _ = wrap_pyfunction!(double, py); +162 | let _ = wrap_pyfunction!(double, py); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | = note: this error originates in the macro `wrap_pyfunction` (in Nightly builds, run with -Z macro-backtrace for more info) From ef13bc66e9ffb4493085082bde0222eb00ba8b33 Mon Sep 17 00:00:00 2001 From: deedy5 <65482418+deedy5@users.noreply.github.com> Date: Sat, 4 May 2024 10:48:15 +0300 Subject: [PATCH 058/495] Add `pyreqwest_impersonate` to example projects (#4123) --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 72653e8fc77..5e34f35489d 100644 --- a/README.md +++ b/README.md @@ -210,6 +210,7 @@ about this topic. - [pyheck](https://github.com/kevinheavey/pyheck) _Fast case conversion library, built by wrapping [heck](https://github.com/withoutboats/heck)._ - Quite easy to follow as there's not much code. - [pyre](https://github.com/Project-Dream-Weaver/pyre-http) _Fast Python HTTP server written in Rust._ +- [pyreqwest_impersonate](https://github.com/deedy5/pyreqwest_impersonate) _The fastest python HTTP client that can impersonate web browsers by mimicking their headers and TLS/JA3/JA4/HTTP2 fingerprints._ - [ril-py](https://github.com/Cryptex-github/ril-py) _A performant and high-level image processing library for Python written in Rust._ - [river](https://github.com/online-ml/river) _Online machine learning in python, the computationally heavy statistics algorithms are implemented in Rust._ - [rust-python-coverage](https://github.com/cjermain/rust-python-coverage) _Example PyO3 project with automated test coverage for Rust and Python._ From 7263fa92ef5da663956d7d2b85188b6877b284b2 Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Sat, 4 May 2024 19:45:27 +0200 Subject: [PATCH 059/495] feature gate deprecated APIs for `PyBool` (#4159) --- src/types/boolobject.rs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/types/boolobject.rs b/src/types/boolobject.rs index 52e4c886aab..43184e31565 100644 --- a/src/types/boolobject.rs +++ b/src/types/boolobject.rs @@ -16,12 +16,10 @@ pyobject_native_type!(PyBool, ffi::PyObject, pyobject_native_static_type_object! impl PyBool { /// Deprecated form of [`PyBool::new_bound`] - #[cfg_attr( - not(feature = "gil-refs"), - deprecated( - since = "0.21.0", - note = "`PyBool::new` will be replaced by `PyBool::new_bound` in a future PyO3 version" - ) + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`PyBool::new` will be replaced by `PyBool::new_bound` in a future PyO3 version" )] #[inline] pub fn new(py: Python<'_>, val: bool) -> &PyBool { From 72be1cddba022d9fbd8ce3275455966095d5712d Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Wed, 8 May 2024 07:46:00 +0200 Subject: [PATCH 060/495] emit `cargo:rustc-check-cfg=CHECK_CFG` for `pyo3`s config names (#4163) --- build.rs | 1 + guide/src/migration.md | 4 ++-- pyo3-build-config/src/impl_.rs | 4 ++-- pyo3-build-config/src/lib.rs | 20 ++++++++++++++++++++ pyo3-ffi/build.rs | 1 + src/marker.rs | 2 +- src/tests/hygiene/pyclass.rs | 10 +++++----- 7 files changed, 32 insertions(+), 10 deletions(-) diff --git a/build.rs b/build.rs index 28592004df2..5d638291f3b 100644 --- a/build.rs +++ b/build.rs @@ -46,6 +46,7 @@ fn configure_pyo3() -> Result<()> { } fn main() { + pyo3_build_config::print_expected_cfgs(); if let Err(e) = configure_pyo3() { eprintln!("error: {}", e.report()); std::process::exit(1) diff --git a/guide/src/migration.md b/guide/src/migration.md index ad39adfa0e8..0a048bf02bc 100644 --- a/guide/src/migration.md +++ b/guide/src/migration.md @@ -339,12 +339,12 @@ To make PyO3's core functionality continue to work while the GIL Refs API is in PyO3 0.21 has introduced the [`PyBackedStr`]({{#PYO3_DOCS_URL}}/pyo3/pybacked/struct.PyBackedStr.html) and [`PyBackedBytes`]({{#PYO3_DOCS_URL}}/pyo3/pybacked/struct.PyBackedBytes.html) types to help with this case. The easiest way to avoid lifetime challenges from extracting `&str` is to use these. For more complex types like `Vec<&str>`, is now impossible to extract directly from a Python object and `Vec` is the recommended upgrade path. -A key thing to note here is because extracting to these types now ties them to the input lifetime, some extremely common patterns may need to be split into multiple Rust lines. For example, the following snippet of calling `.extract::<&str>()` directly on the result of `.getattr()` needs to be adjusted when deactivating the `gil-refs-migration` feature. +A key thing to note here is because extracting to these types now ties them to the input lifetime, some extremely common patterns may need to be split into multiple Rust lines. For example, the following snippet of calling `.extract::<&str>()` directly on the result of `.getattr()` needs to be adjusted when deactivating the `gil-refs` feature. Before: ```rust -# #[cfg(feature = "gil-refs-migration")] { +# #[cfg(feature = "gil-refs")] { # use pyo3::prelude::*; # use pyo3::types::{PyList, PyType}; # fn example<'py>(py: Python<'py>) -> PyResult<()> { diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index 2ee68503faa..35c300da190 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -30,7 +30,7 @@ use crate::{ }; /// Minimum Python version PyO3 supports. -const MINIMUM_SUPPORTED_VERSION: PythonVersion = PythonVersion { major: 3, minor: 7 }; +pub(crate) const MINIMUM_SUPPORTED_VERSION: PythonVersion = PythonVersion { major: 3, minor: 7 }; /// GraalPy may implement the same CPython version over multiple releases. const MINIMUM_SUPPORTED_VERSION_GRAALPY: PythonVersion = PythonVersion { @@ -39,7 +39,7 @@ const MINIMUM_SUPPORTED_VERSION_GRAALPY: PythonVersion = PythonVersion { }; /// Maximum Python version that can be used as minimum required Python version with abi3. -const ABI3_MAX_MINOR: u8 = 12; +pub(crate) const ABI3_MAX_MINOR: u8 = 12; /// Gets an environment variable owned by cargo. /// diff --git a/pyo3-build-config/src/lib.rs b/pyo3-build-config/src/lib.rs index 1aa15d7d62a..24d3ae28124 100644 --- a/pyo3-build-config/src/lib.rs +++ b/pyo3-build-config/src/lib.rs @@ -43,6 +43,7 @@ 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() { + print_expected_cfgs(); for cargo_command in get().build_script_outputs() { println!("{}", cargo_command) } @@ -153,6 +154,25 @@ pub fn print_feature_cfgs() { } } +/// Registers `pyo3`s config names as reachable cfg expressions +/// +/// - +/// - +#[doc(hidden)] +pub fn print_expected_cfgs() { + println!("cargo:rustc-check-cfg=cfg(Py_LIMITED_API)"); + println!("cargo:rustc-check-cfg=cfg(PyPy)"); + println!("cargo:rustc-check-cfg=cfg(GraalPy)"); + println!("cargo:rustc-check-cfg=cfg(py_sys_config, values(\"Py_DEBUG\", \"Py_REF_DEBUG\", \"Py_TRACE_REFS\", \"COUNT_ALLOCS\"))"); + println!("cargo:rustc-check-cfg=cfg(invalid_from_utf8_lint)"); + + // allow `Py_3_*` cfgs from the minimum supported version up to the + // maximum minor version (+1 for development for the next) + for i in impl_::MINIMUM_SUPPORTED_VERSION.minor..=impl_::ABI3_MAX_MINOR + 1 { + println!("cargo:rustc-check-cfg=cfg(Py_3_{i})"); + } +} + /// Private exports used in PyO3's build.rs /// /// Please don't use these - they could change at any time. diff --git a/pyo3-ffi/build.rs b/pyo3-ffi/build.rs index 405da889708..23f03f1a636 100644 --- a/pyo3-ffi/build.rs +++ b/pyo3-ffi/build.rs @@ -205,6 +205,7 @@ fn print_config_and_exit(config: &InterpreterConfig) { } fn main() { + pyo3_build_config::print_expected_cfgs(); if let Err(e) = configure_pyo3() { eprintln!("error: {}", e.report()); std::process::exit(1) diff --git a/src/marker.rs b/src/marker.rs index c975a4a338e..2230d776236 100644 --- a/src/marker.rs +++ b/src/marker.rs @@ -96,7 +96,7 @@ //! enabled, `Ungil` is defined as the following: //! //! ```rust -//! # #[cfg(FALSE)] +//! # #[cfg(any())] //! # { //! #![feature(auto_traits, negative_impls)] //! diff --git a/src/tests/hygiene/pyclass.rs b/src/tests/hygiene/pyclass.rs index 4d07009cad6..0bdb280d066 100644 --- a/src/tests/hygiene/pyclass.rs +++ b/src/tests/hygiene/pyclass.rs @@ -39,11 +39,11 @@ pub enum Enum { #[pyo3(crate = "crate")] pub struct Foo3 { #[pyo3(get, set)] - #[cfg(FALSE)] + #[cfg(any())] field: i32, #[pyo3(get, set)] - #[cfg(not(FALSE))] + #[cfg(not(any()))] field: u32, } @@ -51,11 +51,11 @@ pub struct Foo3 { #[pyo3(crate = "crate")] pub struct Foo4 { #[pyo3(get, set)] - #[cfg(FALSE)] - #[cfg(not(FALSE))] + #[cfg(any())] + #[cfg(not(any()))] field: i32, #[pyo3(get, set)] - #[cfg(not(FALSE))] + #[cfg(not(any()))] field: u32, } From d803f7f8df8bc9efaed086339b7ac67fc070aa07 Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Wed, 8 May 2024 10:04:42 +0200 Subject: [PATCH 061/495] store the `FnArg` ident as a `Cow` instead of a reference (#4157) This allow also storing idents that were generated as part of the macro instead of only ones the were present in the source code. This is needed for example in complex enum tuple variants. --- pyo3-macros-backend/src/method.rs | 21 ++++++++++++++------- pyo3-macros-backend/src/pyclass.rs | 2 +- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/pyo3-macros-backend/src/method.rs b/pyo3-macros-backend/src/method.rs index 982cf62946e..cb86e8ec606 100644 --- a/pyo3-macros-backend/src/method.rs +++ b/pyo3-macros-backend/src/method.rs @@ -1,3 +1,4 @@ +use std::borrow::Cow; use std::fmt::Display; use proc_macro2::{Span, TokenStream}; @@ -18,7 +19,7 @@ use crate::{ #[derive(Clone, Debug)] pub struct RegularArg<'a> { - pub name: &'a syn::Ident, + pub name: Cow<'a, syn::Ident>, pub ty: &'a syn::Type, pub from_py_with: Option, pub default_value: Option, @@ -28,14 +29,14 @@ pub struct RegularArg<'a> { /// Pythons *args argument #[derive(Clone, Debug)] pub struct VarargsArg<'a> { - pub name: &'a syn::Ident, + pub name: Cow<'a, syn::Ident>, pub ty: &'a syn::Type, } /// Pythons **kwarg argument #[derive(Clone, Debug)] pub struct KwargsArg<'a> { - pub name: &'a syn::Ident, + pub name: Cow<'a, syn::Ident>, pub ty: &'a syn::Type, } @@ -61,7 +62,7 @@ pub enum FnArg<'a> { } impl<'a> FnArg<'a> { - pub fn name(&self) -> &'a syn::Ident { + pub fn name(&self) -> &syn::Ident { match self { FnArg::Regular(RegularArg { name, .. }) => name, FnArg::VarArgs(VarargsArg { name, .. }) => name, @@ -98,7 +99,10 @@ impl<'a> FnArg<'a> { .. }) = self { - *self = Self::VarArgs(VarargsArg { name, ty }); + *self = Self::VarArgs(VarargsArg { + name: name.clone(), + ty, + }); Ok(self) } else { bail_spanned!(self.name().span() => "args cannot be optional") @@ -113,7 +117,10 @@ impl<'a> FnArg<'a> { .. }) = self { - *self = Self::KwArgs(KwargsArg { name, ty }); + *self = Self::KwArgs(KwargsArg { + name: name.clone(), + ty, + }); Ok(self) } else { bail_spanned!(self.name().span() => "kwargs must be Option<_>") @@ -159,7 +166,7 @@ impl<'a> FnArg<'a> { } Ok(Self::Regular(RegularArg { - name: ident, + name: Cow::Borrowed(ident), ty: &cap.ty, from_py_with, default_value: None, diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index d9c84655b42..f8bfa164d7d 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -1153,7 +1153,7 @@ fn complex_enum_struct_variant_new<'a>( for field in &variant.fields { args.push(FnArg::Regular(RegularArg { - name: field.ident, + name: Cow::Borrowed(field.ident), ty: field.ty, from_py_with: None, default_value: None, From 635cb8075cfc3b52e2b403eb5b12db9b0d81c88c Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Thu, 9 May 2024 09:58:44 +0200 Subject: [PATCH 062/495] feature gate APIs using `into_gil_ref` (Part 1) (#4160) --- src/conversions/std/array.rs | 20 ++++---- src/instance.rs | 1 + src/internal_tricks.rs | 5 ++ src/macros.rs | 11 ++-- src/tests/common.rs | 10 ++-- src/tests/hygiene/pymodule.rs | 1 + src/types/bytearray.rs | 1 + src/types/complex.rs | 3 ++ src/types/dict.rs | 55 +++++++++++--------- src/types/frozenset.rs | 53 ++++++++++--------- src/types/iterator.rs | 5 +- src/types/list.rs | 66 +++++++++++++----------- src/types/mapping.rs | 28 +++++++---- src/types/memoryview.rs | 5 +- src/types/mod.rs | 16 ++++-- src/types/module.rs | 95 ++++++++++++++++++----------------- src/types/sequence.rs | 31 ++++++++---- src/types/set.rs | 52 ++++++++++--------- src/types/tuple.rs | 70 +++++++++++++++----------- tests/test_no_imports.rs | 3 +- tests/ui/deprecations.rs | 10 ++-- tests/ui/deprecations.stderr | 48 +++++++++--------- 22 files changed, 336 insertions(+), 253 deletions(-) diff --git a/src/conversions/std/array.rs b/src/conversions/std/array.rs index bf21244e3b2..14ccfd35413 100644 --- a/src/conversions/std/array.rs +++ b/src/conversions/std/array.rs @@ -186,11 +186,11 @@ mod tests { Python::with_gil(|py| { let array: [f32; 4] = [0.0, -16.0, 16.0, 42.0]; let pyobject = array.to_object(py); - let pylist: &PyList = pyobject.extract(py).unwrap(); - assert_eq!(pylist[0].extract::().unwrap(), 0.0); - assert_eq!(pylist[1].extract::().unwrap(), -16.0); - assert_eq!(pylist[2].extract::().unwrap(), 16.0); - assert_eq!(pylist[3].extract::().unwrap(), 42.0); + let pylist = pyobject.downcast_bound::(py).unwrap(); + assert_eq!(pylist.get_item(0).unwrap().extract::().unwrap(), 0.0); + assert_eq!(pylist.get_item(1).unwrap().extract::().unwrap(), -16.0); + assert_eq!(pylist.get_item(2).unwrap().extract::().unwrap(), 16.0); + assert_eq!(pylist.get_item(3).unwrap().extract::().unwrap(), 42.0); }); } @@ -213,11 +213,11 @@ mod tests { Python::with_gil(|py| { let array: [f32; 4] = [0.0, -16.0, 16.0, 42.0]; let pyobject = array.into_py(py); - let pylist: &PyList = pyobject.extract(py).unwrap(); - assert_eq!(pylist[0].extract::().unwrap(), 0.0); - assert_eq!(pylist[1].extract::().unwrap(), -16.0); - assert_eq!(pylist[2].extract::().unwrap(), 16.0); - assert_eq!(pylist[3].extract::().unwrap(), 42.0); + let pylist = pyobject.downcast_bound::(py).unwrap(); + assert_eq!(pylist.get_item(0).unwrap().extract::().unwrap(), 0.0); + assert_eq!(pylist.get_item(1).unwrap().extract::().unwrap(), -16.0); + assert_eq!(pylist.get_item(2).unwrap().extract::().unwrap(), 16.0); + assert_eq!(pylist.get_item(3).unwrap().extract::().unwrap(), 42.0); }); } diff --git a/src/instance.rs b/src/instance.rs index 11d45df6f78..81ceaa95546 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -667,6 +667,7 @@ impl<'py, T> Borrowed<'py, 'py, T> where T: HasPyGilRef, { + #[cfg(feature = "gil-refs")] pub(crate) fn into_gil_ref(self) -> &'py T::AsRefTarget { // Safety: self is a borrow over `'py`. #[allow(deprecated)] diff --git a/src/internal_tricks.rs b/src/internal_tricks.rs index 551ddf5a910..75f23edbbd8 100644 --- a/src/internal_tricks.rs +++ b/src/internal_tricks.rs @@ -46,6 +46,7 @@ pub(crate) fn get_ssize_index(index: usize) -> Py_ssize_t { } /// Implementations used for slice indexing PySequence, PyTuple, and PyList +#[cfg(feature = "gil-refs")] macro_rules! index_impls { ( $ty:ty, @@ -154,6 +155,7 @@ macro_rules! index_impls { #[inline(never)] #[cold] #[track_caller] +#[cfg(feature = "gil-refs")] pub(crate) fn index_len_fail(index: usize, ty_name: &str, len: usize) -> ! { panic!( "index {} out of range for {} of length {}", @@ -164,6 +166,7 @@ pub(crate) fn index_len_fail(index: usize, ty_name: &str, len: usize) -> ! { #[inline(never)] #[cold] #[track_caller] +#[cfg(feature = "gil-refs")] pub(crate) fn slice_start_index_len_fail(index: usize, ty_name: &str, len: usize) -> ! { panic!( "range start index {} out of range for {} of length {}", @@ -174,6 +177,7 @@ pub(crate) fn slice_start_index_len_fail(index: usize, ty_name: &str, len: usize #[inline(never)] #[cold] #[track_caller] +#[cfg(feature = "gil-refs")] pub(crate) fn slice_end_index_len_fail(index: usize, ty_name: &str, len: usize) -> ! { panic!( "range end index {} out of range for {} of length {}", @@ -184,6 +188,7 @@ pub(crate) fn slice_end_index_len_fail(index: usize, ty_name: &str, len: usize) #[inline(never)] #[cold] #[track_caller] +#[cfg(feature = "gil-refs")] pub(crate) fn slice_index_order_fail(index: usize, end: usize) -> ! { panic!("slice index starts at {} but ends at {}", index, end); } diff --git a/src/macros.rs b/src/macros.rs index 0267c2663eb..8bd79408a56 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -120,8 +120,8 @@ macro_rules! py_run_impl { /// Wraps a Rust function annotated with [`#[pyfunction]`](macro@crate::pyfunction). /// -/// This can be used with [`PyModule::add_function`](crate::types::PyModule::add_function) to add free -/// functions to a [`PyModule`](crate::types::PyModule) - see its documentation for more +/// This can be used with [`PyModule::add_function`](crate::types::PyModuleMethods::add_function) to +/// add free functions to a [`PyModule`](crate::types::PyModule) - see its documentation for more /// information. /// /// During the migration from the GIL Ref API to the Bound API, the return type of this macro will @@ -157,8 +157,9 @@ macro_rules! wrap_pyfunction { /// Wraps a Rust function annotated with [`#[pyfunction]`](macro@crate::pyfunction). /// -/// This can be used with [`PyModule::add_function`](crate::types::PyModule::add_function) to add free -/// functions to a [`PyModule`](crate::types::PyModule) - see its documentation for more information. +/// This can be used with [`PyModule::add_function`](crate::types::PyModuleMethods::add_function) to +/// add free functions to a [`PyModule`](crate::types::PyModule) - see its documentation for more +/// information. #[macro_export] macro_rules! wrap_pyfunction_bound { ($function:path) => { @@ -183,7 +184,7 @@ macro_rules! wrap_pyfunction_bound { /// Python module. /// /// Use this together with [`#[pymodule]`](crate::pymodule) and -/// [`PyModule::add_wrapped`](crate::types::PyModule::add_wrapped). +/// [`PyModule::add_wrapped`](crate::types::PyModuleMethods::add_wrapped). #[macro_export] macro_rules! wrap_pymodule { ($module:path) => { diff --git a/src/tests/common.rs b/src/tests/common.rs index 854d73e4d7b..e1f2e7dfc28 100644 --- a/src/tests/common.rs +++ b/src/tests/common.rs @@ -114,15 +114,18 @@ mod inner { } impl<'py> CatchWarnings<'py> { - pub fn enter(py: Python<'py>, f: impl FnOnce(&PyList) -> PyResult) -> PyResult { + pub fn enter( + py: Python<'py>, + f: impl FnOnce(&Bound<'py, PyList>) -> PyResult, + ) -> PyResult { let warnings = py.import_bound("warnings")?; let kwargs = [("record", true)].into_py_dict_bound(py); let catch_warnings = warnings .getattr("catch_warnings")? .call((), Some(&kwargs))?; - let list = catch_warnings.call_method0("__enter__")?.extract()?; + let list = catch_warnings.call_method0("__enter__")?.downcast_into()?; let _guard = Self { catch_warnings }; - f(list) + f(&list) } } @@ -139,6 +142,7 @@ mod inner { macro_rules! assert_warnings { ($py:expr, $body:expr, [$(($category:ty, $message:literal)),+] $(,)? ) => {{ $crate::tests::common::CatchWarnings::enter($py, |w| { + use $crate::types::{PyListMethods, PyStringMethods}; $body; let expected_warnings = [$((<$category as $crate::type_object::PyTypeInfo>::type_object_bound($py), $message)),+]; assert_eq!(w.len(), expected_warnings.len()); diff --git a/src/tests/hygiene/pymodule.rs b/src/tests/hygiene/pymodule.rs index 6229708dd06..32b3632be22 100644 --- a/src/tests/hygiene/pymodule.rs +++ b/src/tests/hygiene/pymodule.rs @@ -14,6 +14,7 @@ fn foo(_py: crate::Python<'_>, _m: &crate::types::PyModule) -> crate::PyResult<( ::std::result::Result::Ok(()) } +#[cfg(feature = "gil-refs")] #[allow(deprecated)] #[crate::pymodule] #[pyo3(crate = "crate")] diff --git a/src/types/bytearray.rs b/src/types/bytearray.rs index 8bc4bf55d5b..bdf677c9019 100644 --- a/src/types/bytearray.rs +++ b/src/types/bytearray.rs @@ -491,6 +491,7 @@ impl<'a> Borrowed<'a, '_, PyByteArray> { } } +#[cfg(feature = "gil-refs")] impl<'py> TryFrom<&'py PyAny> for &'py PyByteArray { type Error = crate::PyErr; diff --git a/src/types/complex.rs b/src/types/complex.rs index 4a0c3e30732..cd3d5810d04 100644 --- a/src/types/complex.rs +++ b/src/types/complex.rs @@ -59,6 +59,7 @@ mod not_limited_impls { use super::*; use std::ops::{Add, Div, Mul, Neg, Sub}; + #[cfg(feature = "gil-refs")] impl PyComplex { /// Returns `|self|`. pub fn abs(&self) -> c_double { @@ -94,6 +95,7 @@ mod not_limited_impls { } } + #[cfg(feature = "gil-refs")] impl<'py> $trait for &'py PyComplex { type Output = &'py PyComplex; fn $fn(self, other: &'py PyComplex) -> &'py PyComplex { @@ -136,6 +138,7 @@ mod not_limited_impls { bin_ops!(Mul, mul, *, ffi::_Py_c_prod); bin_ops!(Div, div, /, ffi::_Py_c_quot); + #[cfg(feature = "gil-refs")] impl<'py> Neg for &'py PyComplex { type Output = &'py PyComplex; fn neg(self) -> &'py PyComplex { diff --git a/src/types/dict.rs b/src/types/dict.rs index 68cca1cd981..cab6d68124b 100644 --- a/src/types/dict.rs +++ b/src/types/dict.rs @@ -6,7 +6,9 @@ use crate::instance::{Borrowed, Bound}; use crate::py_result_ext::PyResultExt; use crate::types::any::PyAnyMethods; use crate::types::{PyAny, PyList}; -use crate::{ffi, PyNativeType, Python, ToPyObject}; +#[cfg(feature = "gil-refs")] +use crate::PyNativeType; +use crate::{ffi, Python, ToPyObject}; /// Represents a Python `dict`. #[repr(transparent)] @@ -56,34 +58,11 @@ pyobject_native_type_core!( ); impl PyDict { - /// Deprecated form of [`new_bound`][PyDict::new_bound]. - #[cfg(feature = "gil-refs")] - #[deprecated( - since = "0.21.0", - note = "`PyDict::new` will be replaced by `PyDict::new_bound` in a future PyO3 version" - )] - #[inline] - pub fn new(py: Python<'_>) -> &PyDict { - Self::new_bound(py).into_gil_ref() - } - /// Creates a new empty dictionary. pub fn new_bound(py: Python<'_>) -> Bound<'_, PyDict> { unsafe { ffi::PyDict_New().assume_owned(py).downcast_into_unchecked() } } - /// Deprecated form of [`from_sequence_bound`][PyDict::from_sequence_bound]. - #[cfg(feature = "gil-refs")] - #[deprecated( - since = "0.21.0", - note = "`PyDict::from_sequence` will be replaced by `PyDict::from_sequence_bound` in a future PyO3 version" - )] - #[inline] - #[cfg(not(any(PyPy, GraalPy)))] - pub fn from_sequence(seq: &PyAny) -> PyResult<&PyDict> { - Self::from_sequence_bound(&seq.as_borrowed()).map(Bound::into_gil_ref) - } - /// Creates a new dictionary from the sequence given. /// /// The sequence must consist of `(PyObject, PyObject)`. This is @@ -100,6 +79,30 @@ impl PyDict { })?; Ok(dict) } +} + +#[cfg(feature = "gil-refs")] +impl PyDict { + /// Deprecated form of [`new_bound`][PyDict::new_bound]. + #[deprecated( + since = "0.21.0", + note = "`PyDict::new` will be replaced by `PyDict::new_bound` in a future PyO3 version" + )] + #[inline] + pub fn new(py: Python<'_>) -> &PyDict { + Self::new_bound(py).into_gil_ref() + } + + /// Deprecated form of [`from_sequence_bound`][PyDict::from_sequence_bound]. + #[deprecated( + since = "0.21.0", + note = "`PyDict::from_sequence` will be replaced by `PyDict::from_sequence_bound` in a future PyO3 version" + )] + #[inline] + #[cfg(not(any(PyPy, GraalPy)))] + pub fn from_sequence(seq: &PyAny) -> PyResult<&PyDict> { + Self::from_sequence_bound(&seq.as_borrowed()).map(Bound::into_gil_ref) + } /// Returns a new dictionary that contains the same key-value pairs as self. /// @@ -550,8 +553,10 @@ fn dict_len(dict: &Bound<'_, PyDict>) -> Py_ssize_t { } /// PyO3 implementation of an iterator for a Python `dict` object. +#[cfg(feature = "gil-refs")] pub struct PyDictIterator<'py>(BoundDictIterator<'py>); +#[cfg(feature = "gil-refs")] impl<'py> Iterator for PyDictIterator<'py> { type Item = (&'py PyAny, &'py PyAny); @@ -567,12 +572,14 @@ impl<'py> Iterator for PyDictIterator<'py> { } } +#[cfg(feature = "gil-refs")] impl<'py> ExactSizeIterator for PyDictIterator<'py> { fn len(&self) -> usize { self.0.len() } } +#[cfg(feature = "gil-refs")] impl<'a> IntoIterator for &'a PyDict { type Item = (&'a PyAny, &'a PyAny); type IntoIter = PyDictIterator<'a>; diff --git a/src/types/frozenset.rs b/src/types/frozenset.rs index 1fbbba44615..78cbf01df67 100644 --- a/src/types/frozenset.rs +++ b/src/types/frozenset.rs @@ -1,11 +1,13 @@ use crate::types::PyIterator; +#[cfg(feature = "gil-refs")] +use crate::PyNativeType; use crate::{ err::{self, PyErr, PyResult}, ffi, ffi_ptr_ext::FfiPtrExt, py_result_ext::PyResultExt, types::any::PyAnyMethods, - Bound, PyAny, PyNativeType, PyObject, Python, ToPyObject, + Bound, PyAny, PyObject, Python, ToPyObject, }; use std::ptr; @@ -73,10 +75,32 @@ pyobject_native_type_core!( #checkfunction=ffi::PyFrozenSet_Check ); +impl PyFrozenSet { + /// Creates a new frozenset. + /// + /// May panic when running out of memory. + #[inline] + pub fn new_bound<'a, 'p, T: ToPyObject + 'a>( + py: Python<'p>, + elements: impl IntoIterator, + ) -> PyResult> { + new_from_iter(py, elements) + } + + /// Creates a new empty frozen set + pub fn empty_bound(py: Python<'_>) -> PyResult> { + unsafe { + ffi::PyFrozenSet_New(ptr::null_mut()) + .assume_owned_or_err(py) + .downcast_into_unchecked() + } + } +} + +#[cfg(feature = "gil-refs")] impl PyFrozenSet { /// Deprecated form of [`PyFrozenSet::new_bound`]. #[inline] - #[cfg(feature = "gil-refs")] #[deprecated( since = "0.21.0", note = "`PyFrozenSet::new` will be replaced by `PyFrozenSet::new_bound` in a future PyO3 version" @@ -88,19 +112,7 @@ impl PyFrozenSet { Self::new_bound(py, elements).map(Bound::into_gil_ref) } - /// Creates a new frozenset. - /// - /// May panic when running out of memory. - #[inline] - pub fn new_bound<'a, 'p, T: ToPyObject + 'a>( - py: Python<'p>, - elements: impl IntoIterator, - ) -> PyResult> { - new_from_iter(py, elements) - } - /// Deprecated form of [`PyFrozenSet::empty_bound`]. - #[cfg(feature = "gil-refs")] #[deprecated( since = "0.21.0", note = "`PyFrozenSet::empty` will be replaced by `PyFrozenSet::empty_bound` in a future PyO3 version" @@ -109,15 +121,6 @@ impl PyFrozenSet { Self::empty_bound(py).map(Bound::into_gil_ref) } - /// Creates a new empty frozen set - pub fn empty_bound(py: Python<'_>) -> PyResult> { - unsafe { - ffi::PyFrozenSet_New(ptr::null_mut()) - .assume_owned_or_err(py) - .downcast_into_unchecked() - } - } - /// Return the number of items in the set. /// This is equivalent to len(p) on a set. #[inline] @@ -201,8 +204,10 @@ impl<'py> PyFrozenSetMethods<'py> for Bound<'py, PyFrozenSet> { } /// PyO3 implementation of an iterator for a Python `frozenset` object. +#[cfg(feature = "gil-refs")] pub struct PyFrozenSetIterator<'py>(BoundFrozenSetIterator<'py>); +#[cfg(feature = "gil-refs")] impl<'py> Iterator for PyFrozenSetIterator<'py> { type Item = &'py super::PyAny; @@ -217,6 +222,7 @@ impl<'py> Iterator for PyFrozenSetIterator<'py> { } } +#[cfg(feature = "gil-refs")] impl ExactSizeIterator for PyFrozenSetIterator<'_> { #[inline] fn len(&self) -> usize { @@ -224,6 +230,7 @@ impl ExactSizeIterator for PyFrozenSetIterator<'_> { } } +#[cfg(feature = "gil-refs")] impl<'py> IntoIterator for &'py PyFrozenSet { type Item = &'py PyAny; type IntoIter = PyFrozenSetIterator<'py>; diff --git a/src/types/iterator.rs b/src/types/iterator.rs index 53330705869..6131033af7d 100644 --- a/src/types/iterator.rs +++ b/src/types/iterator.rs @@ -1,9 +1,9 @@ use crate::ffi_ptr_ext::FfiPtrExt; use crate::instance::Borrowed; use crate::py_result_ext::PyResultExt; +use crate::{ffi, AsPyPointer, Bound, PyAny, PyErr, PyResult, PyTypeCheck}; #[cfg(feature = "gil-refs")] -use crate::PyDowncastError; -use crate::{ffi, AsPyPointer, Bound, PyAny, PyErr, PyNativeType, PyResult, PyTypeCheck}; +use crate::{PyDowncastError, PyNativeType}; /// A Python iterator object. /// @@ -54,6 +54,7 @@ impl PyIterator { } } +#[cfg(feature = "gil-refs")] impl<'p> Iterator for &'p PyIterator { type Item = PyResult<&'p PyAny>; diff --git a/src/types/list.rs b/src/types/list.rs index 56f21feb133..0d911e03199 100644 --- a/src/types/list.rs +++ b/src/types/list.rs @@ -6,7 +6,9 @@ use crate::ffi_ptr_ext::FfiPtrExt; use crate::instance::Borrowed; use crate::internal_tricks::get_ssize_index; use crate::types::{PySequence, PyTuple}; -use crate::{Bound, PyAny, PyNativeType, PyObject, Python, ToPyObject}; +#[cfg(feature = "gil-refs")] +use crate::PyNativeType; +use crate::{Bound, PyAny, PyObject, Python, ToPyObject}; use crate::types::any::PyAnyMethods; use crate::types::sequence::PySequenceMethods; @@ -55,26 +57,10 @@ pub(crate) fn new_from_iter<'py>( } impl PyList { - /// Deprecated form of [`PyList::new_bound`]. - #[inline] - #[track_caller] - #[cfg(feature = "gil-refs")] - #[deprecated( - since = "0.21.0", - note = "`PyList::new` will be replaced by `PyList::new_bound` in a future PyO3 version" - )] - pub fn new(py: Python<'_>, elements: impl IntoIterator) -> &PyList - where - T: ToPyObject, - U: ExactSizeIterator, - { - Self::new_bound(py, elements).into_gil_ref() - } - /// Constructs a new list with the given elements. /// /// If you want to create a [`PyList`] with elements of different or unknown types, or from an - /// iterable that doesn't implement [`ExactSizeIterator`], use [`PyList::append`]. + /// iterable that doesn't implement [`ExactSizeIterator`], use [`PyListMethods::append`]. /// /// # Examples /// @@ -109,9 +95,35 @@ impl PyList { new_from_iter(py, &mut iter) } + /// Constructs a new empty list. + pub fn empty_bound(py: Python<'_>) -> Bound<'_, PyList> { + unsafe { + ffi::PyList_New(0) + .assume_owned(py) + .downcast_into_unchecked() + } + } +} + +#[cfg(feature = "gil-refs")] +impl PyList { + /// Deprecated form of [`PyList::new_bound`]. + #[inline] + #[track_caller] + #[deprecated( + since = "0.21.0", + note = "`PyList::new` will be replaced by `PyList::new_bound` in a future PyO3 version" + )] + pub fn new(py: Python<'_>, elements: impl IntoIterator) -> &PyList + where + T: ToPyObject, + U: ExactSizeIterator, + { + Self::new_bound(py, elements).into_gil_ref() + } + /// Deprecated form of [`PyList::empty_bound`]. #[inline] - #[cfg(feature = "gil-refs")] #[deprecated( since = "0.21.0", note = "`PyList::empty` will be replaced by `PyList::empty_bound` in a future PyO3 version" @@ -120,15 +132,6 @@ impl PyList { Self::empty_bound(py).into_gil_ref() } - /// Constructs a new empty list. - pub fn empty_bound(py: Python<'_>) -> Bound<'_, PyList> { - unsafe { - ffi::PyList_New(0) - .assume_owned(py) - .downcast_into_unchecked() - } - } - /// Returns the length of the list. pub fn len(&self) -> usize { self.as_borrowed().len() @@ -273,6 +276,7 @@ impl PyList { } } +#[cfg(feature = "gil-refs")] index_impls!(PyList, "list", PyList::len, PyList::get_slice); /// Implementation of functionality for [`PyList`]. @@ -586,8 +590,10 @@ impl<'py> PyListMethods<'py> for Bound<'py, PyList> { } /// Used by `PyList::iter()`. +#[cfg(feature = "gil-refs")] pub struct PyListIterator<'a>(BoundListIterator<'a>); +#[cfg(feature = "gil-refs")] impl<'a> Iterator for PyListIterator<'a> { type Item = &'a PyAny; @@ -602,6 +608,7 @@ impl<'a> Iterator for PyListIterator<'a> { } } +#[cfg(feature = "gil-refs")] impl<'a> DoubleEndedIterator for PyListIterator<'a> { #[inline] fn next_back(&mut self) -> Option { @@ -609,14 +616,17 @@ impl<'a> DoubleEndedIterator for PyListIterator<'a> { } } +#[cfg(feature = "gil-refs")] impl<'a> ExactSizeIterator for PyListIterator<'a> { fn len(&self) -> usize { self.0.len() } } +#[cfg(feature = "gil-refs")] impl FusedIterator for PyListIterator<'_> {} +#[cfg(feature = "gil-refs")] impl<'a> IntoIterator for &'a PyList { type Item = &'a PyAny; type IntoIter = PyListIterator<'a>; diff --git a/src/types/mapping.rs b/src/types/mapping.rs index a91dad3679f..aea2b484c3b 100644 --- a/src/types/mapping.rs +++ b/src/types/mapping.rs @@ -1,4 +1,4 @@ -use crate::err::{PyDowncastError, PyResult}; +use crate::err::PyResult; use crate::ffi_ptr_ext::FfiPtrExt; use crate::instance::Bound; use crate::py_result_ext::PyResultExt; @@ -6,7 +6,9 @@ use crate::sync::GILOnceCell; use crate::type_object::PyTypeInfo; use crate::types::any::PyAnyMethods; use crate::types::{PyAny, PyDict, PySequence, PyType}; -use crate::{ffi, Py, PyNativeType, PyTypeCheck, Python, ToPyObject}; +#[cfg(feature = "gil-refs")] +use crate::{err::PyDowncastError, PyNativeType}; +use crate::{ffi, Py, PyTypeCheck, Python, ToPyObject}; /// Represents a reference to a Python object supporting the mapping protocol. #[repr(transparent)] @@ -14,6 +16,18 @@ pub struct PyMapping(PyAny); pyobject_native_type_named!(PyMapping); pyobject_native_type_extract!(PyMapping); +impl PyMapping { + /// Register a pyclass as a subclass of `collections.abc.Mapping` (from the Python standard + /// library). This is equivalent to `collections.abc.Mapping.register(T)` in Python. + /// This registration is required for a pyclass to be downcastable from `PyAny` to `PyMapping`. + pub fn register(py: Python<'_>) -> PyResult<()> { + let ty = T::type_object_bound(py); + get_mapping_abc(py)?.call_method1("register", (ty,))?; + Ok(()) + } +} + +#[cfg(feature = "gil-refs")] impl PyMapping { /// Returns the number of objects in the mapping. /// @@ -92,15 +106,6 @@ impl PyMapping { pub fn items(&self) -> PyResult<&PySequence> { self.as_borrowed().items().map(Bound::into_gil_ref) } - - /// Register a pyclass as a subclass of `collections.abc.Mapping` (from the Python standard - /// library). This is equvalent to `collections.abc.Mapping.register(T)` in Python. - /// This registration is required for a pyclass to be downcastable from `PyAny` to `PyMapping`. - pub fn register(py: Python<'_>) -> PyResult<()> { - let ty = T::type_object_bound(py); - get_mapping_abc(py)?.call_method1("register", (ty,))?; - Ok(()) - } } /// Implementation of functionality for [`PyMapping`]. @@ -255,6 +260,7 @@ impl PyTypeCheck for PyMapping { } } +#[cfg(feature = "gil-refs")] #[allow(deprecated)] impl<'v> crate::PyTryFrom<'v> for PyMapping { /// Downcasting to `PyMapping` requires the concrete class to be a subclass (or registered diff --git a/src/types/memoryview.rs b/src/types/memoryview.rs index ffc1c81efa0..31afb372d7f 100644 --- a/src/types/memoryview.rs +++ b/src/types/memoryview.rs @@ -1,7 +1,9 @@ use crate::err::PyResult; use crate::ffi_ptr_ext::FfiPtrExt; use crate::py_result_ext::PyResultExt; -use crate::{ffi, AsPyPointer, Bound, PyAny, PyNativeType}; +#[cfg(feature = "gil-refs")] +use crate::PyNativeType; +use crate::{ffi, AsPyPointer, Bound, PyAny}; /// Represents a Python `memoryview`. #[repr(transparent)] @@ -31,6 +33,7 @@ impl PyMemoryView { } } +#[cfg(feature = "gil-refs")] impl<'py> TryFrom<&'py PyAny> for &'py PyMemoryView { type Error = crate::PyErr; diff --git a/src/types/mod.rs b/src/types/mod.rs index d127cbbca20..38c9238961d 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -79,11 +79,17 @@ pub use self::typeobject::{PyType, PyTypeMethods}; /// the Limited API and PyPy, the underlying structures are opaque and that may not be possible. /// In these cases the iterators are implemented by forwarding to [`PyIterator`]. pub mod iter { - pub use super::dict::{BoundDictIterator, PyDictIterator}; - pub use super::frozenset::{BoundFrozenSetIterator, PyFrozenSetIterator}; - pub use super::list::{BoundListIterator, PyListIterator}; - pub use super::set::{BoundSetIterator, PySetIterator}; - pub use super::tuple::{BorrowedTupleIterator, BoundTupleIterator, PyTupleIterator}; + pub use super::dict::BoundDictIterator; + pub use super::frozenset::BoundFrozenSetIterator; + pub use super::list::BoundListIterator; + pub use super::set::BoundSetIterator; + pub use super::tuple::{BorrowedTupleIterator, BoundTupleIterator}; + + #[cfg(feature = "gil-refs")] + pub use super::{ + dict::PyDictIterator, frozenset::PyFrozenSetIterator, list::PyListIterator, + set::PySetIterator, tuple::PyTupleIterator, + }; } /// Python objects that have a base type. diff --git a/src/types/module.rs b/src/types/module.rs index c8b2cf04551..f0ae7385f23 100644 --- a/src/types/module.rs +++ b/src/types/module.rs @@ -6,11 +6,12 @@ use crate::pyclass::PyClass; use crate::types::{ any::PyAnyMethods, list::PyListMethods, PyAny, PyCFunction, PyDict, PyList, PyString, }; -use crate::{exceptions, ffi, Bound, IntoPy, Py, PyNativeType, PyObject, Python}; +use crate::{exceptions, ffi, Bound, IntoPy, Py, PyObject, Python}; use std::ffi::CString; use std::str; -use super::PyStringMethods; +#[cfg(feature = "gil-refs")] +use {super::PyStringMethods, crate::PyNativeType}; /// Represents a Python [`module`][1] object. /// @@ -25,17 +26,6 @@ pub struct PyModule(PyAny); pyobject_native_type_core!(PyModule, pyobject_native_static_type_object!(ffi::PyModule_Type), #checkfunction=ffi::PyModule_Check); impl PyModule { - /// Deprecated form of [`PyModule::new_bound`]. - #[inline] - #[cfg(feature = "gil-refs")] - #[deprecated( - since = "0.21.0", - note = "`PyModule::new` will be replaced by `PyModule::new_bound` in a future PyO3 version" - )] - pub fn new<'py>(py: Python<'py>, name: &str) -> PyResult<&'py PyModule> { - Self::new_bound(py, name).map(Bound::into_gil_ref) - } - /// Creates a new module object with the `__name__` attribute set to `name`. /// /// # Examples @@ -62,20 +52,6 @@ impl PyModule { } } - /// Deprecated form of [`PyModule::import_bound`]. - #[inline] - #[cfg(feature = "gil-refs")] - #[deprecated( - since = "0.21.0", - note = "`PyModule::import` will be replaced by `PyModule::import_bound` in a future PyO3 version" - )] - pub fn import(py: Python<'_>, name: N) -> PyResult<&PyModule> - where - N: IntoPy>, - { - Self::import_bound(py, name).map(Bound::into_gil_ref) - } - /// Imports the Python module with the specified name. /// /// # Examples @@ -106,22 +82,6 @@ impl PyModule { } } - /// Deprecated form of [`PyModule::from_code_bound`]. - #[inline] - #[cfg(feature = "gil-refs")] - #[deprecated( - since = "0.21.0", - note = "`PyModule::from_code` will be replaced by `PyModule::from_code_bound` in a future PyO3 version" - )] - pub fn from_code<'py>( - py: Python<'py>, - code: &str, - file_name: &str, - module_name: &str, - ) -> PyResult<&'py PyModule> { - Self::from_code_bound(py, code, file_name, module_name).map(Bound::into_gil_ref) - } - /// Creates and loads a module named `module_name`, /// containing the Python code passed to `code` /// and pretending to live at `file_name`. @@ -195,6 +155,47 @@ impl PyModule { .downcast_into() } } +} + +#[cfg(feature = "gil-refs")] +impl PyModule { + /// Deprecated form of [`PyModule::new_bound`]. + #[inline] + #[deprecated( + since = "0.21.0", + note = "`PyModule::new` will be replaced by `PyModule::new_bound` in a future PyO3 version" + )] + pub fn new<'py>(py: Python<'py>, name: &str) -> PyResult<&'py PyModule> { + Self::new_bound(py, name).map(Bound::into_gil_ref) + } + + /// Deprecated form of [`PyModule::import_bound`]. + #[inline] + #[deprecated( + since = "0.21.0", + note = "`PyModule::import` will be replaced by `PyModule::import_bound` in a future PyO3 version" + )] + pub fn import(py: Python<'_>, name: N) -> PyResult<&PyModule> + where + N: IntoPy>, + { + Self::import_bound(py, name).map(Bound::into_gil_ref) + } + + /// Deprecated form of [`PyModule::from_code_bound`]. + #[inline] + #[deprecated( + since = "0.21.0", + note = "`PyModule::from_code` will be replaced by `PyModule::from_code_bound` in a future PyO3 version" + )] + pub fn from_code<'py>( + py: Python<'py>, + code: &str, + file_name: &str, + module_name: &str, + ) -> PyResult<&'py PyModule> { + Self::from_code_bound(py, code, file_name, module_name).map(Bound::into_gil_ref) + } /// Returns the module's `__dict__` attribute, which contains the module's symbol table. pub fn dict(&self) -> &PyDict { @@ -433,8 +434,9 @@ pub trait PyModuleMethods<'py>: crate::sealed::Sealed { /// Adds an attribute to the module. /// - /// For adding classes, functions or modules, prefer to use [`PyModule::add_class`], - /// [`PyModule::add_function`] or [`PyModule::add_submodule`] instead, respectively. + /// For adding classes, functions or modules, prefer to use [`PyModuleMethods::add_class`], + /// [`PyModuleMethods::add_function`] or [`PyModuleMethods::add_submodule`] instead, + /// respectively. /// /// # Examples /// @@ -510,7 +512,8 @@ pub trait PyModuleMethods<'py>: crate::sealed::Sealed { /// Adds a function or a (sub)module to a module, using the functions name as name. /// - /// Prefer to use [`PyModule::add_function`] and/or [`PyModule::add_submodule`] instead. + /// Prefer to use [`PyModuleMethods::add_function`] and/or [`PyModuleMethods::add_submodule`] + /// instead. fn add_wrapped(&self, wrapper: &impl Fn(Python<'py>) -> T) -> PyResult<()> where T: IntoPyCallbackOutput; diff --git a/src/types/sequence.rs b/src/types/sequence.rs index afe4a595964..f75d851973d 100644 --- a/src/types/sequence.rs +++ b/src/types/sequence.rs @@ -1,4 +1,4 @@ -use crate::err::{self, DowncastError, PyDowncastError, PyErr, PyResult}; +use crate::err::{self, DowncastError, PyErr, PyResult}; use crate::exceptions::PyTypeError; use crate::ffi_ptr_ext::FfiPtrExt; #[cfg(feature = "experimental-inspect")] @@ -9,7 +9,9 @@ use crate::py_result_ext::PyResultExt; use crate::sync::GILOnceCell; use crate::type_object::PyTypeInfo; use crate::types::{any::PyAnyMethods, PyAny, PyList, PyString, PyTuple, PyType}; -use crate::{ffi, FromPyObject, Py, PyNativeType, PyTypeCheck, Python, ToPyObject}; +#[cfg(feature = "gil-refs")] +use crate::{err::PyDowncastError, PyNativeType}; +use crate::{ffi, FromPyObject, Py, PyTypeCheck, Python, ToPyObject}; /// Represents a reference to a Python object supporting the sequence protocol. #[repr(transparent)] @@ -17,6 +19,18 @@ pub struct PySequence(PyAny); pyobject_native_type_named!(PySequence); pyobject_native_type_extract!(PySequence); +impl PySequence { + /// Register a pyclass as a subclass of `collections.abc.Sequence` (from the Python standard + /// library). This is equivalent to `collections.abc.Sequence.register(T)` in Python. + /// This registration is required for a pyclass to be downcastable from `PyAny` to `PySequence`. + pub fn register(py: Python<'_>) -> PyResult<()> { + let ty = T::type_object_bound(py); + get_sequence_abc(py)?.call_method1("register", (ty,))?; + Ok(()) + } +} + +#[cfg(feature = "gil-refs")] impl PySequence { /// Returns the number of objects in sequence. /// @@ -175,15 +189,6 @@ impl PySequence { pub fn to_tuple(&self) -> PyResult<&PyTuple> { self.as_borrowed().to_tuple().map(Bound::into_gil_ref) } - - /// Register a pyclass as a subclass of `collections.abc.Sequence` (from the Python standard - /// library). This is equvalent to `collections.abc.Sequence.register(T)` in Python. - /// This registration is required for a pyclass to be downcastable from `PyAny` to `PySequence`. - pub fn register(py: Python<'_>) -> PyResult<()> { - let ty = T::type_object_bound(py); - get_sequence_abc(py)?.call_method1("register", (ty,))?; - Ok(()) - } } /// Implementation of functionality for [`PySequence`]. @@ -465,16 +470,19 @@ impl<'py> PySequenceMethods<'py> for Bound<'py, PySequence> { } #[inline] +#[cfg(feature = "gil-refs")] fn sequence_len(seq: &PySequence) -> usize { seq.len().expect("failed to get sequence length") } #[inline] +#[cfg(feature = "gil-refs")] fn sequence_slice(seq: &PySequence, start: usize, end: usize) -> &PySequence { seq.get_slice(start, end) .expect("sequence slice operation failed") } +#[cfg(feature = "gil-refs")] index_impls!(PySequence, "sequence", sequence_len, sequence_slice); impl<'py, T> FromPyObject<'py> for Vec @@ -539,6 +547,7 @@ impl PyTypeCheck for PySequence { } } +#[cfg(feature = "gil-refs")] #[allow(deprecated)] impl<'v> crate::PyTryFrom<'v> for PySequence { /// Downcasting to `PySequence` requires the concrete class to be a subclass (or registered diff --git a/src/types/set.rs b/src/types/set.rs index 83938f3bf42..1bc4c86be51 100644 --- a/src/types/set.rs +++ b/src/types/set.rs @@ -1,11 +1,12 @@ use crate::types::PyIterator; +#[cfg(feature = "gil-refs")] +use crate::PyNativeType; use crate::{ err::{self, PyErr, PyResult}, ffi_ptr_ext::FfiPtrExt, instance::Bound, py_result_ext::PyResultExt, types::any::PyAnyMethods, - PyNativeType, }; use crate::{ffi, PyAny, PyObject, Python, ToPyObject}; use std::ptr; @@ -29,9 +30,31 @@ pyobject_native_type_core!( #checkfunction=ffi::PySet_Check ); +impl PySet { + /// Creates a new set with elements from the given slice. + /// + /// Returns an error if some element is not hashable. + #[inline] + pub fn new_bound<'a, 'p, T: ToPyObject + 'a>( + py: Python<'p>, + elements: impl IntoIterator, + ) -> PyResult> { + new_from_iter(py, elements) + } + + /// Creates a new empty set. + pub fn empty_bound(py: Python<'_>) -> PyResult> { + unsafe { + ffi::PySet_New(ptr::null_mut()) + .assume_owned_or_err(py) + .downcast_into_unchecked() + } + } +} + +#[cfg(feature = "gil-refs")] impl PySet { /// Deprecated form of [`PySet::new_bound`]. - #[cfg(feature = "gil-refs")] #[deprecated( since = "0.21.0", note = "`PySet::new` will be replaced by `PySet::new_bound` in a future PyO3 version" @@ -44,19 +67,7 @@ impl PySet { Self::new_bound(py, elements).map(Bound::into_gil_ref) } - /// Creates a new set with elements from the given slice. - /// - /// Returns an error if some element is not hashable. - #[inline] - pub fn new_bound<'a, 'p, T: ToPyObject + 'a>( - py: Python<'p>, - elements: impl IntoIterator, - ) -> PyResult> { - new_from_iter(py, elements) - } - /// Deprecated form of [`PySet::empty_bound`]. - #[cfg(feature = "gil-refs")] #[deprecated( since = "0.21.2", note = "`PySet::empty` will be replaced by `PySet::empty_bound` in a future PyO3 version" @@ -65,15 +76,6 @@ impl PySet { Self::empty_bound(py).map(Bound::into_gil_ref) } - /// Creates a new empty set. - pub fn empty_bound(py: Python<'_>) -> PyResult> { - unsafe { - ffi::PySet_New(ptr::null_mut()) - .assume_owned_or_err(py) - .downcast_into_unchecked() - } - } - /// Removes all elements from the set. #[inline] pub fn clear(&self) { @@ -259,8 +261,10 @@ impl<'py> PySetMethods<'py> for Bound<'py, PySet> { } /// PyO3 implementation of an iterator for a Python `set` object. +#[cfg(feature = "gil-refs")] pub struct PySetIterator<'py>(BoundSetIterator<'py>); +#[cfg(feature = "gil-refs")] impl<'py> Iterator for PySetIterator<'py> { type Item = &'py super::PyAny; @@ -279,12 +283,14 @@ impl<'py> Iterator for PySetIterator<'py> { } } +#[cfg(feature = "gil-refs")] impl ExactSizeIterator for PySetIterator<'_> { fn len(&self) -> usize { self.0.len() } } +#[cfg(feature = "gil-refs")] impl<'py> IntoIterator for &'py PySet { type Item = &'py PyAny; type IntoIter = PySetIterator<'py>; diff --git a/src/types/tuple.rs b/src/types/tuple.rs index 563a81983fa..afe129879f9 100644 --- a/src/types/tuple.rs +++ b/src/types/tuple.rs @@ -7,9 +7,11 @@ use crate::inspect::types::TypeInfo; use crate::instance::Borrowed; use crate::internal_tricks::get_ssize_index; use crate::types::{any::PyAnyMethods, sequence::PySequenceMethods, PyList, PySequence}; +#[cfg(feature = "gil-refs")] +use crate::PyNativeType; use crate::{ - exceptions, Bound, FromPyObject, IntoPy, Py, PyAny, PyErr, PyNativeType, PyObject, PyResult, - Python, ToPyObject, + exceptions, Bound, FromPyObject, IntoPy, Py, PyAny, PyErr, PyObject, PyResult, Python, + ToPyObject, }; #[inline] @@ -57,24 +59,6 @@ pub struct PyTuple(PyAny); pyobject_native_type_core!(PyTuple, pyobject_native_static_type_object!(ffi::PyTuple_Type), #checkfunction=ffi::PyTuple_Check); impl PyTuple { - /// Deprecated form of `PyTuple::new_bound`. - #[track_caller] - #[cfg(feature = "gil-refs")] - #[deprecated( - since = "0.21.0", - note = "`PyTuple::new` will be replaced by `PyTuple::new_bound` in a future PyO3 version" - )] - pub fn new( - py: Python<'_>, - elements: impl IntoIterator, - ) -> &PyTuple - where - T: ToPyObject, - U: ExactSizeIterator, - { - Self::new_bound(py, elements).into_gil_ref() - } - /// Constructs a new tuple with the given elements. /// /// If you want to create a [`PyTuple`] with elements of different or unknown types, or from an @@ -114,16 +98,6 @@ impl PyTuple { new_from_iter(py, &mut elements) } - /// Deprecated form of `PyTuple::empty_bound`. - #[cfg(feature = "gil-refs")] - #[deprecated( - since = "0.21.0", - note = "`PyTuple::empty` will be replaced by `PyTuple::empty_bound` in a future PyO3 version" - )] - pub fn empty(py: Python<'_>) -> &PyTuple { - Self::empty_bound(py).into_gil_ref() - } - /// Constructs an empty tuple (on the Python side, a singleton object). pub fn empty_bound(py: Python<'_>) -> Bound<'_, PyTuple> { unsafe { @@ -132,6 +106,35 @@ impl PyTuple { .downcast_into_unchecked() } } +} + +#[cfg(feature = "gil-refs")] +impl PyTuple { + /// Deprecated form of `PyTuple::new_bound`. + #[track_caller] + #[deprecated( + since = "0.21.0", + note = "`PyTuple::new` will be replaced by `PyTuple::new_bound` in a future PyO3 version" + )] + pub fn new( + py: Python<'_>, + elements: impl IntoIterator, + ) -> &PyTuple + where + T: ToPyObject, + U: ExactSizeIterator, + { + Self::new_bound(py, elements).into_gil_ref() + } + + /// Deprecated form of `PyTuple::empty_bound`. + #[deprecated( + since = "0.21.0", + note = "`PyTuple::empty` will be replaced by `PyTuple::empty_bound` in a future PyO3 version" + )] + pub fn empty(py: Python<'_>) -> &PyTuple { + Self::empty_bound(py).into_gil_ref() + } /// Gets the length of the tuple. pub fn len(&self) -> usize { @@ -236,6 +239,7 @@ impl PyTuple { } } +#[cfg(feature = "gil-refs")] index_impls!(PyTuple, "tuple", PyTuple::len, PyTuple::get_slice); /// Implementation of functionality for [`PyTuple`]. @@ -443,8 +447,10 @@ impl<'a, 'py> Borrowed<'a, 'py, PyTuple> { } /// Used by `PyTuple::iter()`. +#[cfg(feature = "gil-refs")] pub struct PyTupleIterator<'a>(BorrowedTupleIterator<'a, 'a>); +#[cfg(feature = "gil-refs")] impl<'a> Iterator for PyTupleIterator<'a> { type Item = &'a PyAny; @@ -459,6 +465,7 @@ impl<'a> Iterator for PyTupleIterator<'a> { } } +#[cfg(feature = "gil-refs")] impl<'a> DoubleEndedIterator for PyTupleIterator<'a> { #[inline] fn next_back(&mut self) -> Option { @@ -466,14 +473,17 @@ impl<'a> DoubleEndedIterator for PyTupleIterator<'a> { } } +#[cfg(feature = "gil-refs")] impl<'a> ExactSizeIterator for PyTupleIterator<'a> { fn len(&self) -> usize { self.0.len() } } +#[cfg(feature = "gil-refs")] impl FusedIterator for PyTupleIterator<'_> {} +#[cfg(feature = "gil-refs")] impl<'a> IntoIterator for &'a PyTuple { type Item = &'a PyAny; type IntoIter = PyTupleIterator<'a>; diff --git a/tests/test_no_imports.rs b/tests/test_no_imports.rs index 022d61e084d..3509a11f4be 100644 --- a/tests/test_no_imports.rs +++ b/tests/test_no_imports.rs @@ -10,6 +10,7 @@ fn basic_function(py: pyo3::Python<'_>, x: Option) -> pyo3::PyOb x.unwrap_or_else(|| py.None()) } +#[cfg(feature = "gil-refs")] #[allow(deprecated)] #[pyo3::pymodule] fn basic_module(_py: pyo3::Python<'_>, m: &pyo3::types::PyModule) -> pyo3::PyResult<()> { @@ -108,7 +109,7 @@ impl BasicClass { #[test] fn test_basic() { pyo3::Python::with_gil(|py| { - let module = pyo3::wrap_pymodule!(basic_module)(py); + let module = pyo3::wrap_pymodule!(basic_module_bound)(py); let cls = py.get_type_bound::(); let d = pyo3::types::IntoPyDict::into_py_dict_bound( [ diff --git a/tests/ui/deprecations.rs b/tests/ui/deprecations.rs index 96f652d9679..ef0b06652e4 100644 --- a/tests/ui/deprecations.rs +++ b/tests/ui/deprecations.rs @@ -58,8 +58,8 @@ fn pyfunction_with_module<'py>(module: &Bound<'py, PyModule>) -> PyResult PyResult<&str> { - module.name() +fn pyfunction_with_module_gil_ref(_module: &PyModule) -> PyResult<&str> { + todo!() } #[pyfunction] @@ -68,14 +68,12 @@ fn double(x: usize) -> usize { } #[pymodule] -fn module_gil_ref(m: &PyModule) -> PyResult<()> { - m.add_function(wrap_pyfunction!(double, m)?)?; +fn module_gil_ref(_m: &PyModule) -> PyResult<()> { Ok(()) } #[pymodule] -fn module_gil_ref_with_explicit_py_arg(_py: Python<'_>, m: &PyModule) -> PyResult<()> { - m.add_function(wrap_pyfunction!(double, m)?)?; +fn module_gil_ref_with_explicit_py_arg(_py: Python<'_>, _m: &PyModule) -> PyResult<()> { Ok(()) } diff --git a/tests/ui/deprecations.stderr b/tests/ui/deprecations.stderr index 2b75ee23e10..9c61c26581e 100644 --- a/tests/ui/deprecations.stderr +++ b/tests/ui/deprecations.stderr @@ -53,69 +53,69 @@ error: use of deprecated method `pyo3::deprecations::GilRefs::::function_arg` | ^ error: use of deprecated method `pyo3::deprecations::GilRefs::::function_arg`: use `&Bound<'_, T>` instead for this function argument - --> tests/ui/deprecations.rs:61:43 + --> tests/ui/deprecations.rs:61:44 | -61 | fn pyfunction_with_module_gil_ref(module: &PyModule) -> PyResult<&str> { - | ^ +61 | fn pyfunction_with_module_gil_ref(_module: &PyModule) -> PyResult<&str> { + | ^ error: use of deprecated method `pyo3::deprecations::GilRefs::::function_arg`: use `&Bound<'_, T>` instead for this function argument --> tests/ui/deprecations.rs:71:19 | -71 | fn module_gil_ref(m: &PyModule) -> PyResult<()> { - | ^ +71 | fn module_gil_ref(_m: &PyModule) -> PyResult<()> { + | ^^ error: use of deprecated method `pyo3::deprecations::GilRefs::::function_arg`: use `&Bound<'_, T>` instead for this function argument - --> tests/ui/deprecations.rs:77:57 + --> tests/ui/deprecations.rs:76:57 | -77 | fn module_gil_ref_with_explicit_py_arg(_py: Python<'_>, m: &PyModule) -> PyResult<()> { - | ^ +76 | fn module_gil_ref_with_explicit_py_arg(_py: Python<'_>, _m: &PyModule) -> PyResult<()> { + | ^^ error: use of deprecated method `pyo3::deprecations::GilRefs::::from_py_with_arg`: use `&Bound<'_, PyAny>` as the argument for this `from_py_with` extractor - --> tests/ui/deprecations.rs:110:27 + --> tests/ui/deprecations.rs:108:27 | -110 | #[pyo3(from_py_with = "extract_gil_ref")] _gil_ref: i32, +108 | #[pyo3(from_py_with = "extract_gil_ref")] _gil_ref: i32, | ^^^^^^^^^^^^^^^^^ error: use of deprecated method `pyo3::deprecations::GilRefs::::function_arg`: use `&Bound<'_, T>` instead for this function argument - --> tests/ui/deprecations.rs:116:29 + --> tests/ui/deprecations.rs:114:29 | -116 | fn pyfunction_gil_ref(_any: &PyAny) {} +114 | fn pyfunction_gil_ref(_any: &PyAny) {} | ^ error: use of deprecated method `pyo3::deprecations::OptionGilRefs::>::function_arg`: use `Option<&Bound<'_, T>>` instead for this function argument - --> tests/ui/deprecations.rs:119:36 + --> tests/ui/deprecations.rs:117:36 | -119 | fn pyfunction_option_gil_ref(_any: Option<&PyAny>) {} +117 | fn pyfunction_option_gil_ref(_any: Option<&PyAny>) {} | ^^^^^^ error: use of deprecated method `pyo3::deprecations::GilRefs::::from_py_with_arg`: use `&Bound<'_, PyAny>` as the argument for this `from_py_with` extractor - --> tests/ui/deprecations.rs:126:27 + --> tests/ui/deprecations.rs:124:27 | -126 | #[pyo3(from_py_with = "PyAny::len", item("my_object"))] +124 | #[pyo3(from_py_with = "PyAny::len", item("my_object"))] | ^^^^^^^^^^^^ error: use of deprecated method `pyo3::deprecations::GilRefs::::from_py_with_arg`: use `&Bound<'_, PyAny>` as the argument for this `from_py_with` extractor - --> tests/ui/deprecations.rs:136:27 + --> tests/ui/deprecations.rs:134:27 | -136 | #[pyo3(from_py_with = "PyAny::len")] usize, +134 | #[pyo3(from_py_with = "PyAny::len")] usize, | ^^^^^^^^^^^^ error: use of deprecated method `pyo3::deprecations::GilRefs::::from_py_with_arg`: use `&Bound<'_, PyAny>` as the argument for this `from_py_with` extractor - --> tests/ui/deprecations.rs:142:31 + --> tests/ui/deprecations.rs:140:31 | -142 | Zip(#[pyo3(from_py_with = "extract_gil_ref")] i32), +140 | Zip(#[pyo3(from_py_with = "extract_gil_ref")] i32), | ^^^^^^^^^^^^^^^^^ error: use of deprecated method `pyo3::deprecations::GilRefs::::from_py_with_arg`: use `&Bound<'_, PyAny>` as the argument for this `from_py_with` extractor - --> tests/ui/deprecations.rs:149:27 + --> tests/ui/deprecations.rs:147:27 | -149 | #[pyo3(from_py_with = "extract_gil_ref")] +147 | #[pyo3(from_py_with = "extract_gil_ref")] | ^^^^^^^^^^^^^^^^^ error: use of deprecated method `pyo3::deprecations::GilRefs::>::is_python`: use `wrap_pyfunction_bound!` instead - --> tests/ui/deprecations.rs:162:13 + --> tests/ui/deprecations.rs:160:13 | -162 | let _ = wrap_pyfunction!(double, py); +160 | let _ = wrap_pyfunction!(double, py); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | = note: this error originates in the macro `wrap_pyfunction` (in Nightly builds, run with -Z macro-backtrace for more info) From 2d19b7e2a7e29e2f445a8b415a764f40b4242c5c Mon Sep 17 00:00:00 2001 From: David Matos Date: Thu, 9 May 2024 17:37:53 +0200 Subject: [PATCH 063/495] Add `num-rational` support for Python's `fractions.Fraction` type (#4148) * Add `num-rational` support for Python's `fractions.Fraction` type * Add newsfragment * Use Bound instead * Handle objs which atts are incorrect * Add extra test * Add tests for wasm32 arch * add type for wasm32 clipppy --- Cargo.toml | 2 + guide/src/conversions/tables.md | 3 + guide/src/features.md | 4 + newsfragments/4148.added.md | 1 + src/conversions/mod.rs | 1 + src/conversions/num_rational.rs | 277 ++++++++++++++++++++++++++++++++ src/lib.rs | 3 + 7 files changed, 291 insertions(+) create mode 100644 newsfragments/4148.added.md create mode 100644 src/conversions/num_rational.rs diff --git a/Cargo.toml b/Cargo.toml index 9202c69ef92..4c5a083060d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,6 +40,7 @@ hashbrown = { version = ">= 0.9, < 0.15", optional = true } indexmap = { version = ">= 1.6, < 3", optional = true } num-bigint = { version = "0.4", optional = true } num-complex = { version = ">= 0.2, < 0.5", optional = true } +num-rational = {version = "0.4.1", optional = true } rust_decimal = { version = "1.0.0", default-features = false, optional = true } serde = { version = "1.0", optional = true } smallvec = { version = "1.0", optional = true } @@ -127,6 +128,7 @@ full = [ "indexmap", "num-bigint", "num-complex", + "num-rational", "rust_decimal", "serde", "smallvec", diff --git a/guide/src/conversions/tables.md b/guide/src/conversions/tables.md index eb33b17acf7..208e61671ec 100644 --- a/guide/src/conversions/tables.md +++ b/guide/src/conversions/tables.md @@ -19,6 +19,7 @@ The table below contains the Python type and the corresponding function argument | `int` | `i8`, `u8`, `i16`, `u16`, `i32`, `u32`, `i64`, `u64`, `i128`, `u128`, `isize`, `usize`, `num_bigint::BigInt`[^1], `num_bigint::BigUint`[^1] | `PyLong` | | `float` | `f32`, `f64` | `PyFloat` | | `complex` | `num_complex::Complex`[^2] | `PyComplex` | +| `fractions.Fraction`| `num_rational::Ratio`[^8] | - | | `list[T]` | `Vec` | `PyList` | | `dict[K, V]` | `HashMap`, `BTreeMap`, `hashbrown::HashMap`[^3], `indexmap::IndexMap`[^4] | `PyDict` | | `tuple[T, U]` | `(T, U)`, `Vec` | `PyTuple` | @@ -113,3 +114,5 @@ Finally, the following Rust types are also able to convert to Python as return v [^6]: Requires the `chrono-tz` optional feature. [^7]: Requires the `rust_decimal` optional feature. + +[^8]: Requires the `num-rational` optional feature. diff --git a/guide/src/features.md b/guide/src/features.md index 0816770a781..07085a9e89c 100644 --- a/guide/src/features.md +++ b/guide/src/features.md @@ -157,6 +157,10 @@ Adds a dependency on [num-bigint](https://docs.rs/num-bigint) and enables conver Adds a dependency on [num-complex](https://docs.rs/num-complex) and enables conversions into its [`Complex`](https://docs.rs/num-complex/latest/num_complex/struct.Complex.html) type. +### `num-rational` + +Adds a dependency on [num-rational](https://docs.rs/num-rational) and enables conversions into its [`Ratio`](https://docs.rs/num-rational/latest/num_rational/struct.Ratio.html) type. + ### `rust_decimal` Adds a dependency on [rust_decimal](https://docs.rs/rust_decimal) and enables conversions into its [`Decimal`](https://docs.rs/rust_decimal/latest/rust_decimal/struct.Decimal.html) type. diff --git a/newsfragments/4148.added.md b/newsfragments/4148.added.md new file mode 100644 index 00000000000..16da3d2db37 --- /dev/null +++ b/newsfragments/4148.added.md @@ -0,0 +1 @@ +Conversion between [num-rational](https://github.com/rust-num/num-rational) and Python's fractions.Fraction. diff --git a/src/conversions/mod.rs b/src/conversions/mod.rs index 3d785c02381..53ecf849c07 100644 --- a/src/conversions/mod.rs +++ b/src/conversions/mod.rs @@ -9,6 +9,7 @@ pub mod hashbrown; pub mod indexmap; pub mod num_bigint; pub mod num_complex; +pub mod num_rational; pub mod rust_decimal; pub mod serde; pub mod smallvec; diff --git a/src/conversions/num_rational.rs b/src/conversions/num_rational.rs new file mode 100644 index 00000000000..31eb7ca1c7b --- /dev/null +++ b/src/conversions/num_rational.rs @@ -0,0 +1,277 @@ +#![cfg(feature = "num-rational")] +//! Conversions to and from [num-rational](https://docs.rs/num-rational) types. +//! +//! This is useful for converting between Python's [fractions.Fraction](https://docs.python.org/3/library/fractions.html) into and from a native Rust +//! type. +//! +//! +//! To use this feature, add to your **`Cargo.toml`**: +//! +//! ```toml +//! [dependencies] +#![doc = concat!("pyo3 = { version = \"", env!("CARGO_PKG_VERSION"), "\", features = [\"num-rational\"] }")] +//! num-rational = "0.4.1" +//! ``` +//! +//! # Example +//! +//! Rust code to create a function that adds five to a fraction: +//! +//! ```rust +//! use num_rational::Ratio; +//! use pyo3::prelude::*; +//! +//! #[pyfunction] +//! fn add_five_to_fraction(fraction: Ratio) -> Ratio { +//! fraction + Ratio::new(5, 1) +//! } +//! +//! #[pymodule] +//! fn my_module(m: &Bound<'_, PyModule>) -> PyResult<()> { +//! m.add_function(wrap_pyfunction!(add_five_to_fraction, m)?)?; +//! Ok(()) +//! } +//! ``` +//! +//! Python code that validates the functionality: +//! ```python +//! from my_module import add_five_to_fraction +//! from fractions import Fraction +//! +//! fraction = Fraction(2,1) +//! fraction_plus_five = add_five_to_fraction(f) +//! assert fraction + 5 == fraction_plus_five +//! ``` + +use crate::ffi; +use crate::sync::GILOnceCell; +use crate::types::any::PyAnyMethods; +use crate::types::PyType; +use crate::{Bound, FromPyObject, IntoPy, Py, PyAny, PyObject, PyResult, Python, ToPyObject}; +use std::os::raw::c_char; + +#[cfg(feature = "num-bigint")] +use num_bigint::BigInt; +use num_rational::Ratio; + +static FRACTION_CLS: GILOnceCell> = GILOnceCell::new(); + +fn get_fraction_cls(py: Python<'_>) -> PyResult<&Bound<'_, PyType>> { + FRACTION_CLS.get_or_try_init_type_ref(py, "fractions", "Fraction") +} + +macro_rules! rational_conversion { + ($int: ty) => { + impl<'py> FromPyObject<'py> for Ratio<$int> { + fn extract_bound(obj: &Bound<'py, PyAny>) -> PyResult { + let py = obj.py(); + let py_numerator_obj = unsafe { + Bound::from_owned_ptr_or_err( + py, + ffi::PyObject_GetAttrString( + obj.as_ptr(), + "numerator\0".as_ptr() as *const c_char, + ), + ) + }; + let py_denominator_obj = unsafe { + Bound::from_owned_ptr_or_err( + py, + ffi::PyObject_GetAttrString( + obj.as_ptr(), + "denominator\0".as_ptr() as *const c_char, + ), + ) + }; + let numerator_owned = unsafe { + Bound::from_owned_ptr_or_err( + py, + ffi::PyNumber_Long(py_numerator_obj?.as_ptr()), + )? + }; + let denominator_owned = unsafe { + Bound::from_owned_ptr_or_err( + py, + ffi::PyNumber_Long(py_denominator_obj?.as_ptr()), + )? + }; + let rs_numerator: $int = numerator_owned.extract()?; + let rs_denominator: $int = denominator_owned.extract()?; + Ok(Ratio::new(rs_numerator, rs_denominator)) + } + } + + impl ToPyObject for Ratio<$int> { + fn to_object(&self, py: Python<'_>) -> PyObject { + let fraction_cls = get_fraction_cls(py).expect("failed to load fractions.Fraction"); + let ret = fraction_cls + .call1((self.numer().clone(), self.denom().clone())) + .expect("failed to call fractions.Fraction(value)"); + ret.to_object(py) + } + } + impl IntoPy for Ratio<$int> { + fn into_py(self, py: Python<'_>) -> PyObject { + self.to_object(py) + } + } + }; +} +rational_conversion!(i8); +rational_conversion!(i16); +rational_conversion!(i32); +rational_conversion!(isize); +rational_conversion!(i64); +#[cfg(feature = "num-bigint")] +rational_conversion!(BigInt); +#[cfg(test)] +mod tests { + use super::*; + use crate::types::dict::PyDictMethods; + use crate::types::PyDict; + + #[cfg(not(target_arch = "wasm32"))] + use proptest::prelude::*; + #[test] + fn test_negative_fraction() { + Python::with_gil(|py| { + let locals = PyDict::new_bound(py); + py.run_bound( + "import fractions\npy_frac = fractions.Fraction(-0.125)", + None, + Some(&locals), + ) + .unwrap(); + let py_frac = locals.get_item("py_frac").unwrap().unwrap(); + let roundtripped: Ratio = py_frac.extract().unwrap(); + let rs_frac = Ratio::new(-1, 8); + assert_eq!(roundtripped, rs_frac); + }) + } + #[test] + fn test_obj_with_incorrect_atts() { + Python::with_gil(|py| { + let locals = PyDict::new_bound(py); + py.run_bound( + "not_fraction = \"contains_incorrect_atts\"", + None, + Some(&locals), + ) + .unwrap(); + let py_frac = locals.get_item("not_fraction").unwrap().unwrap(); + assert!(py_frac.extract::>().is_err()); + }) + } + + #[test] + fn test_fraction_with_fraction_type() { + Python::with_gil(|py| { + let locals = PyDict::new_bound(py); + py.run_bound( + "import fractions\npy_frac = fractions.Fraction(fractions.Fraction(10))", + None, + Some(&locals), + ) + .unwrap(); + let py_frac = locals.get_item("py_frac").unwrap().unwrap(); + let roundtripped: Ratio = py_frac.extract().unwrap(); + let rs_frac = Ratio::new(10, 1); + assert_eq!(roundtripped, rs_frac); + }) + } + + #[test] + fn test_fraction_with_decimal() { + Python::with_gil(|py| { + let locals = PyDict::new_bound(py); + py.run_bound( + "import fractions\n\nfrom decimal import Decimal\npy_frac = fractions.Fraction(Decimal(\"1.1\"))", + None, + Some(&locals), + ) + .unwrap(); + let py_frac = locals.get_item("py_frac").unwrap().unwrap(); + let roundtripped: Ratio = py_frac.extract().unwrap(); + let rs_frac = Ratio::new(11, 10); + assert_eq!(roundtripped, rs_frac); + }) + } + + #[test] + fn test_fraction_with_num_den() { + Python::with_gil(|py| { + let locals = PyDict::new_bound(py); + py.run_bound( + "import fractions\npy_frac = fractions.Fraction(10,5)", + None, + Some(&locals), + ) + .unwrap(); + let py_frac = locals.get_item("py_frac").unwrap().unwrap(); + let roundtripped: Ratio = py_frac.extract().unwrap(); + let rs_frac = Ratio::new(10, 5); + assert_eq!(roundtripped, rs_frac); + }) + } + + #[cfg(target_arch = "wasm32")] + #[test] + fn test_int_roundtrip() { + Python::with_gil(|py| { + let rs_frac = Ratio::new(1, 2); + let py_frac: PyObject = rs_frac.into_py(py); + let roundtripped: Ratio = py_frac.extract(py).unwrap(); + assert_eq!(rs_frac, roundtripped); + // float conversion + }) + } + + #[cfg(target_arch = "wasm32")] + #[test] + fn test_big_int_roundtrip() { + Python::with_gil(|py| { + let rs_frac = Ratio::from_float(5.5).unwrap(); + let py_frac: PyObject = rs_frac.clone().into_py(py); + let roundtripped: Ratio = py_frac.extract(py).unwrap(); + assert_eq!(rs_frac, roundtripped); + }) + } + + #[cfg(not(target_arch = "wasm32"))] + proptest! { + #[test] + fn test_int_roundtrip(num in any::(), den in any::()) { + Python::with_gil(|py| { + let rs_frac = Ratio::new(num, den); + let py_frac = rs_frac.into_py(py); + let roundtripped: Ratio = py_frac.extract(py).unwrap(); + assert_eq!(rs_frac, roundtripped); + }) + } + + #[test] + #[cfg(feature = "num-bigint")] + fn test_big_int_roundtrip(num in any::()) { + Python::with_gil(|py| { + let rs_frac = Ratio::from_float(num).unwrap(); + let py_frac = rs_frac.clone().into_py(py); + let roundtripped: Ratio = py_frac.extract(py).unwrap(); + assert_eq!(roundtripped, rs_frac); + }) + } + + } + + #[test] + fn test_infinity() { + Python::with_gil(|py| { + let locals = PyDict::new_bound(py); + let py_bound = py.run_bound( + "import fractions\npy_frac = fractions.Fraction(\"Infinity\")", + None, + Some(&locals), + ); + assert!(py_bound.is_err()); + }) + } +} diff --git a/src/lib.rs b/src/lib.rs index b400f143f5a..3923257f5f3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -108,6 +108,7 @@ //! [`BigUint`] types. //! - [`num-complex`]: Enables conversions between Python objects and [num-complex]'s [`Complex`] //! type. +//! - [`num-rational`]: Enables conversions between Python's fractions.Fraction and [num-rational]'s types //! - [`rust_decimal`]: Enables conversions between Python's decimal.Decimal and [rust_decimal]'s //! [`Decimal`] type. //! - [`serde`]: Allows implementing [serde]'s [`Serialize`] and [`Deserialize`] traits for @@ -288,6 +289,7 @@ //! [`maturin`]: https://github.com/PyO3/maturin "Build and publish crates with pyo3, rust-cpython and cffi bindings as well as rust binaries as python packages" //! [`num-bigint`]: ./num_bigint/index.html "Documentation about the `num-bigint` feature." //! [`num-complex`]: ./num_complex/index.html "Documentation about the `num-complex` feature." +//! [`num-rational`]: ./num_rational/index.html "Documentation about the `num-rational` feature." //! [`pyo3-build-config`]: https://docs.rs/pyo3-build-config //! [rust_decimal]: https://docs.rs/rust_decimal //! [`rust_decimal`]: ./rust_decimal/index.html "Documenation about the `rust_decimal` feature." @@ -303,6 +305,7 @@ //! [manual_builds]: https://pyo3.rs/latest/building-and-distribution.html#manual-builds "Manual builds - Building and Distribution - PyO3 user guide" //! [num-bigint]: https://docs.rs/num-bigint //! [num-complex]: https://docs.rs/num-complex +//! [num-rational]: https://docs.rs/num-rational //! [serde]: https://docs.rs/serde //! [setuptools-rust]: https://github.com/PyO3/setuptools-rust "Setuptools plugin for Rust extensions" //! [the guide]: https://pyo3.rs "PyO3 user guide" From 7beb64a8cac4873b151d87c8099c56c69c8f602e Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Thu, 9 May 2024 23:08:23 +0200 Subject: [PATCH 064/495] allow constructor customization of complex enum variants (#4158) * allow `#[pyo3(signature = ...)]` on complex enum variants to specify constructor signature * rename keyword to `constructor` * review feedback * add docs in guide * add newsfragment --- guide/pyclass-parameters.md | 2 + guide/src/class.md | 40 ++++++++++++ newsfragments/4158.added.md | 1 + pyo3-macros-backend/src/attributes.rs | 1 + pyo3-macros-backend/src/pyclass.rs | 62 ++++++++++++++----- pyo3-macros-backend/src/pyfunction.rs | 2 +- .../src/pyfunction/signature.rs | 10 +++ pytests/src/enums.rs | 27 ++++++-- pytests/tests/test_enums.py | 23 +++++++ tests/ui/invalid_pyclass_enum.rs | 7 +++ tests/ui/invalid_pyclass_enum.stderr | 6 ++ 11 files changed, 160 insertions(+), 21 deletions(-) create mode 100644 newsfragments/4158.added.md diff --git a/guide/pyclass-parameters.md b/guide/pyclass-parameters.md index 6951a5b5e15..9bd0534ea5d 100644 --- a/guide/pyclass-parameters.md +++ b/guide/pyclass-parameters.md @@ -2,6 +2,7 @@ | Parameter | Description | | :- | :- | +| `constructor` | This is currently only allowed on [variants of complex enums][params-constructor]. It allows customization of the generated class constructor for each variant. It uses the same syntax and supports the same options as the `signature` attribute of functions and methods. | | `crate = "some::path"` | Path to import the `pyo3` crate, if it's not accessible at `::pyo3`. | | `dict` | Gives instances of this class an empty `__dict__` to store custom attributes. | | `extends = BaseType` | Use a custom baseclass. Defaults to [`PyAny`][params-1] | @@ -39,5 +40,6 @@ struct MyClass {} [params-4]: https://doc.rust-lang.org/std/rc/struct.Rc.html [params-5]: https://doc.rust-lang.org/std/sync/struct.Arc.html [params-6]: https://docs.python.org/3/library/weakref.html +[params-constructor]: https://pyo3.rs/latest/class.html#complex-enums [params-mapping]: https://pyo3.rs/latest/class/protocols.html#mapping--sequence-types [params-sequence]: https://pyo3.rs/latest/class/protocols.html#mapping--sequence-types diff --git a/guide/src/class.md b/guide/src/class.md index b5ef95cb2f7..3fcfaca4bdc 100644 --- a/guide/src/class.md +++ b/guide/src/class.md @@ -1243,6 +1243,46 @@ Python::with_gil(|py| { }) ``` +The constructor of each generated class can be customized using the `#[pyo3(constructor = (...))]` attribute. This uses the same syntax as the [`#[pyo3(signature = (...))]`](function/signature.md) +attribute on function and methods and supports the same options. To apply this attribute simply place it on top of a variant in a `#[pyclass]` complex enum as shown below: + +```rust +# use pyo3::prelude::*; +#[pyclass] +enum Shape { + #[pyo3(constructor = (radius=1.0))] + Circle { radius: f64 }, + #[pyo3(constructor = (*, width, height))] + Rectangle { width: f64, height: f64 }, + #[pyo3(constructor = (side_count, radius=1.0))] + RegularPolygon { side_count: u32, radius: f64 }, + Nothing { }, +} + +# #[cfg(Py_3_10)] +Python::with_gil(|py| { + let cls = py.get_type_bound::(); + pyo3::py_run!(py, cls, r#" + circle = cls.Circle() + assert isinstance(circle, cls) + assert isinstance(circle, cls.Circle) + assert circle.radius == 1.0 + + square = cls.Rectangle(width = 1, height = 1) + assert isinstance(square, cls) + assert isinstance(square, cls.Rectangle) + assert square.width == 1 + assert square.height == 1 + + hexagon = cls.RegularPolygon(6) + assert isinstance(hexagon, cls) + assert isinstance(hexagon, cls.RegularPolygon) + assert hexagon.side_count == 6 + assert hexagon.radius == 1 + "#) +}) +``` + ## Implementation details The `#[pyclass]` macros rely on a lot of conditional code generation: each `#[pyclass]` can optionally have a `#[pymethods]` block. diff --git a/newsfragments/4158.added.md b/newsfragments/4158.added.md new file mode 100644 index 00000000000..42e6d3ff4b4 --- /dev/null +++ b/newsfragments/4158.added.md @@ -0,0 +1 @@ +Added `#[pyo3(constructor = (...))]` to customize the generated constructors for complex enum variants diff --git a/pyo3-macros-backend/src/attributes.rs b/pyo3-macros-backend/src/attributes.rs index e91b3b8d9a2..d9c805aa3fa 100644 --- a/pyo3-macros-backend/src/attributes.rs +++ b/pyo3-macros-backend/src/attributes.rs @@ -12,6 +12,7 @@ pub mod kw { syn::custom_keyword!(annotation); syn::custom_keyword!(attribute); syn::custom_keyword!(cancel_handle); + syn::custom_keyword!(constructor); syn::custom_keyword!(dict); syn::custom_keyword!(extends); syn::custom_keyword!(freelist); diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index f8bfa164d7d..3023f897645 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -8,6 +8,7 @@ use crate::attributes::{ use crate::deprecations::Deprecations; use crate::konst::{ConstAttributes, ConstSpec}; use crate::method::{FnArg, FnSpec, PyArg, RegularArg}; +use crate::pyfunction::ConstructorAttribute; use crate::pyimpl::{gen_py_const, PyClassMethodsType}; use crate::pymethod::{ impl_py_getter_def, impl_py_setter_def, MethodAndMethodDef, MethodAndSlotDef, PropertyType, @@ -620,12 +621,15 @@ struct PyClassEnumVariantNamedField<'a> { } /// `#[pyo3()]` options for pyclass enum variants +#[derive(Default)] struct EnumVariantPyO3Options { name: Option, + constructor: Option, } enum EnumVariantPyO3Option { Name(NameAttribute), + Constructor(ConstructorAttribute), } impl Parse for EnumVariantPyO3Option { @@ -633,6 +637,8 @@ impl Parse for EnumVariantPyO3Option { let lookahead = input.lookahead1(); if lookahead.peek(attributes::kw::name) { input.parse().map(EnumVariantPyO3Option::Name) + } else if lookahead.peek(attributes::kw::constructor) { + input.parse().map(EnumVariantPyO3Option::Constructor) } else { Err(lookahead.error()) } @@ -641,21 +647,33 @@ impl Parse for EnumVariantPyO3Option { impl EnumVariantPyO3Options { fn take_pyo3_options(attrs: &mut Vec) -> Result { - let mut options = EnumVariantPyO3Options { name: None }; + let mut options = EnumVariantPyO3Options::default(); - for option in take_pyo3_options(attrs)? { - match option { - EnumVariantPyO3Option::Name(name) => { + take_pyo3_options(attrs)? + .into_iter() + .try_for_each(|option| options.set_option(option))?; + + Ok(options) + } + + fn set_option(&mut self, option: EnumVariantPyO3Option) -> syn::Result<()> { + macro_rules! set_option { + ($key:ident) => { + { ensure_spanned!( - options.name.is_none(), - name.span() => "`name` may only be specified once" + self.$key.is_none(), + $key.span() => concat!("`", stringify!($key), "` may only be specified once") ); - options.name = Some(name); + self.$key = Some($key); } - } + }; } - Ok(options) + match option { + EnumVariantPyO3Option::Constructor(constructor) => set_option!(constructor), + EnumVariantPyO3Option::Name(name) => set_option!(name), + } + Ok(()) } } @@ -689,6 +707,10 @@ fn impl_simple_enum( let variants = simple_enum.variants; let pytypeinfo = impl_pytypeinfo(cls, args, None, ctx); + for variant in &variants { + ensure_spanned!(variant.options.constructor.is_none(), variant.options.constructor.span() => "`constructor` can't be used on a simple enum variant"); + } + let (default_repr, default_repr_slot) = { let variants_repr = variants.iter().map(|variant| { let variant_name = variant.ident; @@ -889,7 +911,7 @@ fn impl_complex_enum( let mut variant_cls_pytypeinfos = vec![]; let mut variant_cls_pyclass_impls = vec![]; let mut variant_cls_impls = vec![]; - for variant in &variants { + for variant in variants { let variant_cls = gen_complex_enum_variant_class_ident(cls, variant.get_ident()); let variant_cls_zst = quote! { @@ -908,11 +930,11 @@ fn impl_complex_enum( let variant_cls_pytypeinfo = impl_pytypeinfo(&variant_cls, &variant_args, None, ctx); variant_cls_pytypeinfos.push(variant_cls_pytypeinfo); - let variant_new = complex_enum_variant_new(cls, variant, ctx)?; - - let (variant_cls_impl, field_getters) = impl_complex_enum_variant_cls(cls, variant, ctx)?; + let (variant_cls_impl, field_getters) = impl_complex_enum_variant_cls(cls, &variant, ctx)?; variant_cls_impls.push(variant_cls_impl); + let variant_new = complex_enum_variant_new(cls, variant, ctx)?; + let pyclass_impl = PyClassImplsBuilder::new( &variant_cls, &variant_args, @@ -1120,7 +1142,7 @@ pub fn gen_complex_enum_variant_attr( fn complex_enum_variant_new<'a>( cls: &'a syn::Ident, - variant: &'a PyClassEnumVariant<'a>, + variant: PyClassEnumVariant<'a>, ctx: &Ctx, ) -> Result { match variant { @@ -1132,7 +1154,7 @@ fn complex_enum_variant_new<'a>( fn complex_enum_struct_variant_new<'a>( cls: &'a syn::Ident, - variant: &'a PyClassEnumStructVariant<'a>, + variant: PyClassEnumStructVariant<'a>, ctx: &Ctx, ) -> Result { let Ctx { pyo3_path } = ctx; @@ -1162,7 +1184,15 @@ fn complex_enum_struct_variant_new<'a>( } args }; - let signature = crate::pyfunction::FunctionSignature::from_arguments(args)?; + + let signature = if let Some(constructor) = variant.options.constructor { + crate::pyfunction::FunctionSignature::from_arguments_and_attribute( + args, + constructor.into_signature(), + )? + } else { + crate::pyfunction::FunctionSignature::from_arguments(args)? + }; let spec = FnSpec { tp: crate::method::FnType::FnNew, diff --git a/pyo3-macros-backend/src/pyfunction.rs b/pyo3-macros-backend/src/pyfunction.rs index 7c355533b83..e259f0e2c1e 100644 --- a/pyo3-macros-backend/src/pyfunction.rs +++ b/pyo3-macros-backend/src/pyfunction.rs @@ -18,7 +18,7 @@ use syn::{ mod signature; -pub use self::signature::{FunctionSignature, SignatureAttribute}; +pub use self::signature::{ConstructorAttribute, FunctionSignature, SignatureAttribute}; #[derive(Clone, Debug)] pub struct PyFunctionArgPyO3Attributes { diff --git a/pyo3-macros-backend/src/pyfunction/signature.rs b/pyo3-macros-backend/src/pyfunction/signature.rs index 3daa79c89f5..b73b96a3d59 100644 --- a/pyo3-macros-backend/src/pyfunction/signature.rs +++ b/pyo3-macros-backend/src/pyfunction/signature.rs @@ -195,6 +195,16 @@ impl ToTokens for SignatureItemPosargsSep { } pub type SignatureAttribute = KeywordAttribute; +pub type ConstructorAttribute = KeywordAttribute; + +impl ConstructorAttribute { + pub fn into_signature(self) -> SignatureAttribute { + SignatureAttribute { + kw: kw::signature(self.kw.span), + value: self.value, + } + } +} #[derive(Default)] pub struct PythonSignature { diff --git a/pytests/src/enums.rs b/pytests/src/enums.rs index 0a1bc49bb63..68a5fc93dfe 100644 --- a/pytests/src/enums.rs +++ b/pytests/src/enums.rs @@ -39,11 +39,26 @@ pub fn do_simple_stuff(thing: &SimpleEnum) -> SimpleEnum { #[pyclass] pub enum ComplexEnum { - Int { i: i32 }, - Float { f: f64 }, - Str { s: String }, + Int { + i: i32, + }, + Float { + f: f64, + }, + Str { + s: String, + }, EmptyStruct {}, - MultiFieldStruct { a: i32, b: f64, c: bool }, + MultiFieldStruct { + a: i32, + b: f64, + c: bool, + }, + #[pyo3(constructor = (a = 42, b = None))] + VariantWithDefault { + a: i32, + b: Option, + }, } #[pyfunction] @@ -58,5 +73,9 @@ pub fn do_complex_stuff(thing: &ComplexEnum) -> ComplexEnum { b: *b, c: *c, }, + ComplexEnum::VariantWithDefault { a, b } => ComplexEnum::VariantWithDefault { + a: 2 * a, + b: b.as_ref().map(|s| s.to_uppercase()), + }, } } diff --git a/pytests/tests/test_enums.py b/pytests/tests/test_enums.py index 04b0cdca431..cd1d7aedaf8 100644 --- a/pytests/tests/test_enums.py +++ b/pytests/tests/test_enums.py @@ -18,6 +18,12 @@ def test_complex_enum_variant_constructors(): multi_field_struct_variant = enums.ComplexEnum.MultiFieldStruct(42, 3.14, True) assert isinstance(multi_field_struct_variant, enums.ComplexEnum.MultiFieldStruct) + variant_with_default_1 = enums.ComplexEnum.VariantWithDefault() + assert isinstance(variant_with_default_1, enums.ComplexEnum.VariantWithDefault) + + variant_with_default_2 = enums.ComplexEnum.VariantWithDefault(25, "Hello") + assert isinstance(variant_with_default_2, enums.ComplexEnum.VariantWithDefault) + @pytest.mark.parametrize( "variant", @@ -27,6 +33,7 @@ def test_complex_enum_variant_constructors(): enums.ComplexEnum.Str("hello"), enums.ComplexEnum.EmptyStruct(), enums.ComplexEnum.MultiFieldStruct(42, 3.14, True), + enums.ComplexEnum.VariantWithDefault(), ], ) def test_complex_enum_variant_subclasses(variant: enums.ComplexEnum): @@ -48,6 +55,10 @@ def test_complex_enum_field_getters(): assert multi_field_struct_variant.b == 3.14 assert multi_field_struct_variant.c is True + variant_with_default = enums.ComplexEnum.VariantWithDefault() + assert variant_with_default.a == 42 + assert variant_with_default.b is None + @pytest.mark.parametrize( "variant", @@ -57,6 +68,7 @@ def test_complex_enum_field_getters(): enums.ComplexEnum.Str("hello"), enums.ComplexEnum.EmptyStruct(), enums.ComplexEnum.MultiFieldStruct(42, 3.14, True), + enums.ComplexEnum.VariantWithDefault(), ], ) def test_complex_enum_desugared_match(variant: enums.ComplexEnum): @@ -78,6 +90,11 @@ def test_complex_enum_desugared_match(variant: enums.ComplexEnum): assert x == 42 assert y == 3.14 assert z is True + elif isinstance(variant, enums.ComplexEnum.VariantWithDefault): + x = variant.a + y = variant.b + assert x == 42 + assert y is None else: assert False @@ -90,6 +107,7 @@ def test_complex_enum_desugared_match(variant: enums.ComplexEnum): enums.ComplexEnum.Str("hello"), enums.ComplexEnum.EmptyStruct(), enums.ComplexEnum.MultiFieldStruct(42, 3.14, True), + enums.ComplexEnum.VariantWithDefault(b="hello"), ], ) def test_complex_enum_pyfunction_in_out_desugared_match(variant: enums.ComplexEnum): @@ -112,5 +130,10 @@ def test_complex_enum_pyfunction_in_out_desugared_match(variant: enums.ComplexEn assert x == 42 assert y == 3.14 assert z is True + elif isinstance(variant, enums.ComplexEnum.VariantWithDefault): + x = variant.a + y = variant.b + assert x == 84 + assert y == "HELLO" else: assert False diff --git a/tests/ui/invalid_pyclass_enum.rs b/tests/ui/invalid_pyclass_enum.rs index 95879c2fbd1..116b8968da8 100644 --- a/tests/ui/invalid_pyclass_enum.rs +++ b/tests/ui/invalid_pyclass_enum.rs @@ -27,4 +27,11 @@ enum NoTupleVariants { TupleVariant(i32), } +#[pyclass] +enum SimpleNoSignature { + #[pyo3(constructor = (a, b))] + A, + B, +} + fn main() {} diff --git a/tests/ui/invalid_pyclass_enum.stderr b/tests/ui/invalid_pyclass_enum.stderr index a03a0ae2814..e9ba9806da8 100644 --- a/tests/ui/invalid_pyclass_enum.stderr +++ b/tests/ui/invalid_pyclass_enum.stderr @@ -31,3 +31,9 @@ error: Tuple variant `TupleVariant` is not yet supported in a complex enum | 27 | TupleVariant(i32), | ^^^^^^^^^^^^ + +error: `constructor` can't be used on a simple enum variant + --> tests/ui/invalid_pyclass_enum.rs:32:12 + | +32 | #[pyo3(constructor = (a, b))] + | ^^^^^^^^^^^ From 21c02484d06354c389c9a3405083a3d4a03e1126 Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Fri, 10 May 2024 00:21:48 +0200 Subject: [PATCH 065/495] feature gate APIs using `into_gil_ref` (Part 2) (#4166) --- guide/src/conversions/traits.md | 6 ++--- guide/src/exception.md | 4 +-- guide/src/memory.md | 8 ++++++ guide/src/migration.md | 2 +- guide/src/types.md | 2 ++ pytests/src/pyclasses.rs | 26 +----------------- pytests/src/sequence.rs | 2 +- pytests/tests/test_pyclasses.py | 11 -------- src/conversion.rs | 1 + src/conversions/chrono.rs | 2 +- src/conversions/std/osstr.rs | 5 ++-- src/conversions/std/path.rs | 5 ++-- src/derive_utils.rs | 2 +- src/err/mod.rs | 1 + src/exceptions.rs | 6 ++++- src/impl_/deprecations.rs | 32 +++++++--------------- src/impl_/extract_argument.rs | 12 ++++----- src/instance.rs | 4 +-- src/pycell.rs | 4 +-- src/tests/hygiene/pyfunction.rs | 1 + src/types/any.rs | 15 ++++++----- src/types/bytearray.rs | 4 ++- src/types/float.rs | 2 +- src/types/iterator.rs | 4 +-- src/types/memoryview.rs | 4 +-- src/types/mod.rs | 14 +++++++--- src/types/num.rs | 2 +- tests/test_class_basics.rs | 15 +---------- tests/test_compile_error.rs | 10 +++++-- tests/test_methods.rs | 7 ----- tests/test_module.rs | 33 ----------------------- tests/test_proto_methods.rs | 4 +-- tests/ui/deprecations.stderr | 6 ----- tests/ui/invalid_result_conversion.stderr | 2 +- 34 files changed, 93 insertions(+), 165 deletions(-) diff --git a/guide/src/conversions/traits.md b/guide/src/conversions/traits.md index 65a5d150e79..95d16faaaa6 100644 --- a/guide/src/conversions/traits.md +++ b/guide/src/conversions/traits.md @@ -265,7 +265,7 @@ use pyo3::prelude::*; #[derive(FromPyObject)] # #[derive(Debug)] -enum RustyEnum<'a> { +enum RustyEnum<'py> { Int(usize), // input is a positive int String(String), // input is a string IntTuple(usize, usize), // input is a 2-tuple with positive ints @@ -284,7 +284,7 @@ enum RustyEnum<'a> { b: usize, }, #[pyo3(transparent)] - CatchAll(&'a PyAny), // This extraction never fails + CatchAll(Bound<'py, PyAny>), // This extraction never fails } # # use pyo3::types::{PyBytes, PyString}; @@ -394,7 +394,7 @@ enum RustyEnum<'a> { # assert_eq!( # b"text", # match rust_thing { -# RustyEnum::CatchAll(i) => i.downcast::()?.as_bytes(), +# RustyEnum::CatchAll(ref i) => i.downcast::()?.as_bytes(), # other => unreachable!("Error extracting: {:?}", other), # } # ); diff --git a/guide/src/exception.md b/guide/src/exception.md index 3e2f5034897..1a68e24086f 100644 --- a/guide/src/exception.md +++ b/guide/src/exception.md @@ -128,5 +128,5 @@ defines exceptions for several standard library modules. [`PyErr`]: {{#PYO3_DOCS_URL}}/pyo3/struct.PyErr.html [`PyResult`]: {{#PYO3_DOCS_URL}}/pyo3/type.PyResult.html [`PyErr::from_value`]: {{#PYO3_DOCS_URL}}/pyo3/struct.PyErr.html#method.from_value -[`PyAny::is_instance`]: {{#PYO3_DOCS_URL}}/pyo3/types/struct.PyAny.html#method.is_instance -[`PyAny::is_instance_of`]: {{#PYO3_DOCS_URL}}/pyo3/types/struct.PyAny.html#method.is_instance_of +[`PyAny::is_instance`]: {{#PYO3_DOCS_URL}}/pyo3/types/trait.PyAnyMethods.html#tymethod.is_instance +[`PyAny::is_instance_of`]: {{#PYO3_DOCS_URL}}/pyo3/types/trait.PyAnyMethods.html#tymethod.is_instance_of diff --git a/guide/src/memory.md b/guide/src/memory.md index a6640e65cf3..67a78d3be68 100644 --- a/guide/src/memory.md +++ b/guide/src/memory.md @@ -34,9 +34,11 @@ held. (If PyO3 could not assume this, every PyO3 API would need to take a very simple and easy-to-understand programs like this: ```rust +# #![allow(unused_imports)] # use pyo3::prelude::*; # use pyo3::types::PyString; # fn main() -> PyResult<()> { +# #[cfg(feature = "gil-refs")] Python::with_gil(|py| -> PyResult<()> { #[allow(deprecated)] // py.eval() is part of the GIL Refs API let hello = py @@ -57,9 +59,11 @@ it owns are decreased, releasing them to the Python garbage collector. Most of the time we don't have to think about this, but consider the following: ```rust +# #![allow(unused_imports)] # use pyo3::prelude::*; # use pyo3::types::PyString; # fn main() -> PyResult<()> { +# #[cfg(feature = "gil-refs")] Python::with_gil(|py| -> PyResult<()> { for _ in 0..10 { #[allow(deprecated)] // py.eval() is part of the GIL Refs API @@ -96,9 +100,11 @@ In general we don't want unbounded memory growth during loops! One workaround is to acquire and release the GIL with each iteration of the loop. ```rust +# #![allow(unused_imports)] # use pyo3::prelude::*; # use pyo3::types::PyString; # fn main() -> PyResult<()> { +# #[cfg(feature = "gil-refs")] for _ in 0..10 { Python::with_gil(|py| -> PyResult<()> { #[allow(deprecated)] // py.eval() is part of the GIL Refs API @@ -118,9 +124,11 @@ times. Another workaround is to work with the `GILPool` object directly, but this is unsafe. ```rust +# #![allow(unused_imports)] # use pyo3::prelude::*; # use pyo3::types::PyString; # fn main() -> PyResult<()> { +# #[cfg(feature = "gil-refs")] Python::with_gil(|py| -> PyResult<()> { for _ in 0..10 { #[allow(deprecated)] // `new_pool` is not needed in code not using the GIL Refs API diff --git a/guide/src/migration.md b/guide/src/migration.md index 0a048bf02bc..2317f85185e 100644 --- a/guide/src/migration.md +++ b/guide/src/migration.md @@ -54,7 +54,7 @@ pyo3 = { version = "0.21", features = ["gil-refs"] } The `PyTryFrom` trait has aged poorly, its `try_from` method now conflicts with `TryFrom::try_from` in the 2021 edition prelude. A lot of its functionality was also duplicated with `PyTypeInfo`. -To tighten up the PyO3 traits as part of the deprecation of the GIL Refs API the `PyTypeInfo` trait has had a simpler companion `PyTypeCheck`. The methods [`PyAny::downcast`]({{#PYO3_DOCS_URL}}/pyo3/types/struct.PyAny.html#method.downcast) and [`PyAny::downcast_exact`]({{#PYO3_DOCS_URL}}/pyo3/types/struct.PyAny.html#method.downcast_exact) no longer use `PyTryFrom` as a bound, instead using `PyTypeCheck` and `PyTypeInfo` respectively. +To tighten up the PyO3 traits as part of the deprecation of the GIL Refs API the `PyTypeInfo` trait has had a simpler companion `PyTypeCheck`. The methods `PyAny::downcast` and `PyAny::downcast_exact` no longer use `PyTryFrom` as a bound, instead using `PyTypeCheck` and `PyTypeInfo` respectively. To migrate, switch all type casts to use `obj.downcast()` instead of `try_from(obj)` (and similar for `downcast_exact`). diff --git a/guide/src/types.md b/guide/src/types.md index d28fb7e15d6..20e5e76a330 100644 --- a/guide/src/types.md +++ b/guide/src/types.md @@ -467,8 +467,10 @@ let _: &mut MyClass = &mut *py_ref_mut; `PyCell` was also accessed like a Python-native type. ```rust +#![allow(unused_imports)] # use pyo3::prelude::*; # #[pyclass] struct MyClass { } +# #[cfg(feature = "gil-refs")] # Python::with_gil(|py| -> PyResult<()> { #[allow(deprecated)] // &PyCell is part of the deprecate GIL Refs API let cell: &PyCell = PyCell::new(py, MyClass {})?; diff --git a/pytests/src/pyclasses.rs b/pytests/src/pyclasses.rs index ac817627cfe..6338596b481 100644 --- a/pytests/src/pyclasses.rs +++ b/pytests/src/pyclasses.rs @@ -63,30 +63,6 @@ impl AssertingBaseClass { } } -#[allow(deprecated)] -mod deprecated { - use super::*; - - #[pyclass(subclass)] - #[derive(Clone, Debug)] - pub struct AssertingBaseClassGilRef; - - #[pymethods] - impl AssertingBaseClassGilRef { - #[new] - #[classmethod] - fn new(cls: &PyType, expected_type: &PyType) -> PyResult { - if !cls.is(expected_type) { - return Err(PyValueError::new_err(format!( - "{:?} != {:?}", - cls, expected_type - ))); - } - Ok(Self) - } - } -} - #[pyclass] struct ClassWithoutConstructor; @@ -95,7 +71,7 @@ pub fn pyclasses(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; - m.add_class::()?; m.add_class::()?; + Ok(()) } diff --git a/pytests/src/sequence.rs b/pytests/src/sequence.rs index 0e48a161bd3..f552b4048b8 100644 --- a/pytests/src/sequence.rs +++ b/pytests/src/sequence.rs @@ -12,7 +12,7 @@ fn array_to_array_i32(arr: [i32; 3]) -> [i32; 3] { } #[pyfunction] -fn vec_to_vec_pystring(vec: Vec<&PyString>) -> Vec<&PyString> { +fn vec_to_vec_pystring(vec: Vec>) -> Vec> { vec } diff --git a/pytests/tests/test_pyclasses.py b/pytests/tests/test_pyclasses.py index 74f883e3808..efef178d489 100644 --- a/pytests/tests/test_pyclasses.py +++ b/pytests/tests/test_pyclasses.py @@ -65,17 +65,6 @@ def test_new_classmethod(): _ = AssertingSubClass(expected_type=str) -def test_new_classmethod_gil_ref(): - class AssertingSubClass(pyclasses.AssertingBaseClassGilRef): - pass - - # The `AssertingBaseClass` constructor errors if it is not passed the - # relevant subclass. - _ = AssertingSubClass(expected_type=AssertingSubClass) - with pytest.raises(ValueError): - _ = AssertingSubClass(expected_type=str) - - class ClassWithoutConstructorPy: def __new__(cls): raise TypeError("No constructor defined") diff --git a/src/conversion.rs b/src/conversion.rs index 8644db84289..6e116af7303 100644 --- a/src/conversion.rs +++ b/src/conversion.rs @@ -345,6 +345,7 @@ where } #[allow(deprecated)] +#[cfg(feature = "gil-refs")] impl<'py, T> FromPyObject<'py> for &'py crate::PyCell where T: PyClass, diff --git a/src/conversions/chrono.rs b/src/conversions/chrono.rs index 544d1cf2663..2e220681951 100644 --- a/src/conversions/chrono.rs +++ b/src/conversions/chrono.rs @@ -347,7 +347,7 @@ impl FromPyObject<'_> for FixedOffset { /// does not supports microseconds. fn extract_bound(ob: &Bound<'_, PyAny>) -> PyResult { #[cfg(not(Py_LIMITED_API))] - let ob: &PyTzInfo = ob.extract()?; + let ob = ob.downcast::()?; #[cfg(Py_LIMITED_API)] check_type(ob, &DatetimeTypes::get(ob.py()).tzinfo, "PyTzInfo")?; diff --git a/src/conversions/std/osstr.rs b/src/conversions/std/osstr.rs index b9382688589..4565c3fbd94 100644 --- a/src/conversions/std/osstr.rs +++ b/src/conversions/std/osstr.rs @@ -147,6 +147,7 @@ impl<'a> IntoPy for &'a OsString { #[cfg(test)] mod tests { + use crate::types::{PyAnyMethods, PyStringMethods}; use crate::{types::PyString, IntoPy, PyObject, Python, ToPyObject}; use std::fmt::Debug; use std::{ @@ -179,7 +180,7 @@ mod tests { Python::with_gil(|py| { fn test_roundtrip + Debug>(py: Python<'_>, obj: T) { let pyobject = obj.to_object(py); - let pystring: &PyString = pyobject.extract(py).unwrap(); + let pystring = pyobject.downcast_bound::(py).unwrap(); assert_eq!(pystring.to_string_lossy(), obj.as_ref().to_string_lossy()); let roundtripped_obj: OsString = pystring.extract().unwrap(); assert_eq!(obj.as_ref(), roundtripped_obj.as_os_str()); @@ -200,7 +201,7 @@ mod tests { obj: T, ) { let pyobject = obj.clone().into_py(py); - let pystring: &PyString = pyobject.extract(py).unwrap(); + let pystring = pyobject.downcast_bound::(py).unwrap(); assert_eq!(pystring.to_string_lossy(), obj.as_ref().to_string_lossy()); let roundtripped_obj: OsString = pystring.extract().unwrap(); assert!(obj.as_ref() == roundtripped_obj.as_os_str()); diff --git a/src/conversions/std/path.rs b/src/conversions/std/path.rs index 5d832e89575..d7f3121ea10 100644 --- a/src/conversions/std/path.rs +++ b/src/conversions/std/path.rs @@ -64,6 +64,7 @@ impl<'a> IntoPy for &'a PathBuf { #[cfg(test)] mod tests { + use crate::types::{PyAnyMethods, PyStringMethods}; use crate::{types::PyString, IntoPy, PyObject, Python, ToPyObject}; use std::borrow::Cow; use std::fmt::Debug; @@ -95,7 +96,7 @@ mod tests { Python::with_gil(|py| { fn test_roundtrip + Debug>(py: Python<'_>, obj: T) { let pyobject = obj.to_object(py); - let pystring: &PyString = pyobject.extract(py).unwrap(); + let pystring = pyobject.downcast_bound::(py).unwrap(); assert_eq!(pystring.to_string_lossy(), obj.as_ref().to_string_lossy()); let roundtripped_obj: PathBuf = pystring.extract().unwrap(); assert_eq!(obj.as_ref(), roundtripped_obj.as_path()); @@ -116,7 +117,7 @@ mod tests { obj: T, ) { let pyobject = obj.clone().into_py(py); - let pystring: &PyString = pyobject.extract(py).unwrap(); + let pystring = pyobject.downcast_bound::(py).unwrap(); assert_eq!(pystring.to_string_lossy(), obj.as_ref().to_string_lossy()); let roundtripped_obj: PathBuf = pystring.extract().unwrap(); assert_eq!(obj.as_ref(), roundtripped_obj.as_path()); diff --git a/src/derive_utils.rs b/src/derive_utils.rs index 4ccb38f901b..a47f489ceb8 100644 --- a/src/derive_utils.rs +++ b/src/derive_utils.rs @@ -13,7 +13,7 @@ impl<'a> PyFunctionArguments<'a> { match self { PyFunctionArguments::Python(py) => (py, None), PyFunctionArguments::PyModule(module) => { - let py = module.py(); + let py = crate::PyNativeType::py(module); (py, Some(module)) } } diff --git a/src/err/mod.rs b/src/err/mod.rs index d923761af1d..200c180e9b0 100644 --- a/src/err/mod.rs +++ b/src/err/mod.rs @@ -1000,6 +1000,7 @@ where } /// Convert `PyDowncastError` to Python `TypeError`. +#[cfg(feature = "gil-refs")] impl<'a> std::convert::From> for PyErr { fn from(err: PyDowncastError<'_>) -> PyErr { let args = PyDowncastErrorArguments { diff --git a/src/exceptions.rs b/src/exceptions.rs index 367022927c5..b44a5c5a3fe 100644 --- a/src/exceptions.rs +++ b/src/exceptions.rs @@ -32,6 +32,7 @@ macro_rules! impl_exception_boilerplate { $crate::impl_exception_boilerplate_bound!($name); + #[cfg(feature = "gil-refs")] impl ::std::error::Error for $name { fn source(&self) -> ::std::option::Option<&(dyn ::std::error::Error + 'static)> { unsafe { @@ -58,6 +59,7 @@ macro_rules! impl_exception_boilerplate_bound { /// /// [`PyErr`]: https://docs.rs/pyo3/latest/pyo3/struct.PyErr.html "PyErr in pyo3" #[inline] + #[allow(dead_code)] pub fn new_err(args: A) -> $crate::PyErr where A: $crate::PyErrArguments + ::std::marker::Send + ::std::marker::Sync + 'static, @@ -881,7 +883,9 @@ mod tests { use super::*; use crate::types::any::PyAnyMethods; use crate::types::{IntoPyDict, PyDict}; - use crate::{PyErr, PyNativeType}; + use crate::PyErr; + #[cfg(feature = "gil-refs")] + use crate::PyNativeType; import_exception_bound!(socket, gaierror); import_exception_bound!(email.errors, MessageError); diff --git a/src/impl_/deprecations.rs b/src/impl_/deprecations.rs index 9eb1da05c92..650e01ce729 100644 --- a/src/impl_/deprecations.rs +++ b/src/impl_/deprecations.rs @@ -29,39 +29,27 @@ impl GilRefs { } impl GilRefs> { - #[cfg_attr( - not(feature = "gil-refs"), - deprecated(since = "0.21.0", note = "use `wrap_pyfunction_bound!` instead") - )] + #[deprecated(since = "0.21.0", note = "use `wrap_pyfunction_bound!` instead")] pub fn is_python(&self) {} } impl GilRefs { - #[cfg_attr( - not(feature = "gil-refs"), - deprecated( - since = "0.21.0", - note = "use `&Bound<'_, T>` instead for this function argument" - ) + #[deprecated( + since = "0.21.0", + note = "use `&Bound<'_, T>` instead for this function argument" )] pub fn function_arg(&self) {} - #[cfg_attr( - not(feature = "gil-refs"), - deprecated( - since = "0.21.0", - note = "use `&Bound<'_, PyAny>` as the argument for this `from_py_with` extractor" - ) + #[deprecated( + since = "0.21.0", + note = "use `&Bound<'_, PyAny>` as the argument for this `from_py_with` extractor" )] pub fn from_py_with_arg(&self) {} } impl OptionGilRefs> { - #[cfg_attr( - not(feature = "gil-refs"), - deprecated( - since = "0.21.0", - note = "use `Option<&Bound<'_, T>>` instead for this function argument" - ) + #[deprecated( + since = "0.21.0", + note = "use `Option<&Bound<'_, T>>` instead for this function argument" )] pub fn function_arg(&self) {} } diff --git a/src/impl_/extract_argument.rs b/src/impl_/extract_argument.rs index 485b8645086..5f652d75122 100644 --- a/src/impl_/extract_argument.rs +++ b/src/impl_/extract_argument.rs @@ -790,10 +790,8 @@ fn push_parameter_list(msg: &mut String, parameter_names: &[&str]) { #[cfg(test)] mod tests { - use crate::{ - types::{IntoPyDict, PyTuple}, - PyAny, Python, - }; + use crate::types::{IntoPyDict, PyTuple}; + use crate::Python; use super::{push_parameter_list, FunctionDescription, NoVarargs, NoVarkeywords}; @@ -809,7 +807,7 @@ mod tests { }; Python::with_gil(|py| { - let args = PyTuple::new_bound(py, Vec::<&PyAny>::new()); + let args = PyTuple::empty_bound(py); let kwargs = [("foo", 0u8)].into_py_dict_bound(py); let err = unsafe { function_description @@ -840,7 +838,7 @@ mod tests { }; Python::with_gil(|py| { - let args = PyTuple::new_bound(py, Vec::<&PyAny>::new()); + let args = PyTuple::empty_bound(py); let kwargs = [(1u8, 1u8)].into_py_dict_bound(py); let err = unsafe { function_description @@ -871,7 +869,7 @@ mod tests { }; Python::with_gil(|py| { - let args = PyTuple::new_bound(py, Vec::<&PyAny>::new()); + let args = PyTuple::empty_bound(py); let mut output = [None, None]; let err = unsafe { function_description.extract_arguments_tuple_dict::( diff --git a/src/instance.rs b/src/instance.rs index 81ceaa95546..e160de3a314 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -1283,7 +1283,7 @@ impl Py { } /// Returns whether `self` and `other` point to the same object. To compare - /// the equality of two objects (the `==` operator), use [`eq`](PyAny::eq). + /// the equality of two objects (the `==` operator), use [`eq`](PyAnyMethods::eq). /// /// This is equivalent to the Python expression `self is other`. #[inline] @@ -2142,7 +2142,7 @@ a = A() fn test_is_ellipsis() { Python::with_gil(|py| { let v = py - .eval("...", None, None) + .eval_bound("...", None, None) .map_err(|e| e.display(py)) .unwrap() .to_object(py); diff --git a/src/pycell.rs b/src/pycell.rs index 80ccff0a030..e0088d9b523 100644 --- a/src/pycell.rs +++ b/src/pycell.rs @@ -41,7 +41,7 @@ //! The [`#[pymethods]`](crate::pymethods) proc macro will generate this wrapper function (and more), //! using [`PyCell`] under the hood: //! -//! ```rust +//! ```rust,ignore //! # use pyo3::prelude::*; //! # #[pyclass] //! # struct Number { @@ -148,7 +148,7 @@ //! ``` //! //! It is better to write that function like this: -//! ```rust +//! ```rust,ignore //! # #![allow(deprecated)] //! # use pyo3::prelude::*; //! # #[pyclass] diff --git a/src/tests/hygiene/pyfunction.rs b/src/tests/hygiene/pyfunction.rs index edc8b6e35d3..c1bca213933 100644 --- a/src/tests/hygiene/pyfunction.rs +++ b/src/tests/hygiene/pyfunction.rs @@ -8,6 +8,7 @@ fn do_something(x: i32) -> crate::PyResult { } #[test] +#[cfg(feature = "gil-refs")] fn invoke_wrap_pyfunction() { crate::Python::with_gil(|py| { #[allow(deprecated)] diff --git a/src/types/any.rs b/src/types/any.rs index 1854308ae7f..17837835be1 100644 --- a/src/types/any.rs +++ b/src/types/any.rs @@ -1,15 +1,17 @@ use crate::class::basic::CompareOp; use crate::conversion::{AsPyPointer, FromPyObjectBound, IntoPy, ToPyObject}; -use crate::err::{DowncastError, DowncastIntoError, PyDowncastError, PyErr, PyResult}; +use crate::err::{DowncastError, DowncastIntoError, PyErr, PyResult}; use crate::exceptions::{PyAttributeError, PyTypeError}; use crate::ffi_ptr_ext::FfiPtrExt; use crate::instance::Bound; use crate::py_result_ext::PyResultExt; -use crate::type_object::{HasPyGilRef, PyTypeCheck, PyTypeInfo}; +use crate::type_object::{PyTypeCheck, PyTypeInfo}; #[cfg(not(any(PyPy, GraalPy)))] use crate::types::PySuper; use crate::types::{PyDict, PyIterator, PyList, PyString, PyTuple, PyType}; -use crate::{err, ffi, Py, PyNativeType, Python}; +use crate::{err, ffi, Py, Python}; +#[cfg(feature = "gil-refs")] +use crate::{err::PyDowncastError, type_object::HasPyGilRef, PyNativeType}; use std::cell::UnsafeCell; use std::cmp::Ordering; use std::os::raw::c_int; @@ -66,6 +68,7 @@ pyobject_native_type_extract!(PyAny); pyobject_native_type_sized!(PyAny, ffi::PyObject); +#[cfg(feature = "gil-refs")] impl PyAny { /// Returns whether `self` and `other` point to the same object. To compare /// the equality of two objects (the `==` operator), use [`eq`](PyAny::eq). @@ -942,7 +945,7 @@ impl PyAny { #[doc(alias = "PyAny")] pub trait PyAnyMethods<'py>: crate::sealed::Sealed { /// Returns whether `self` and `other` point to the same object. To compare - /// the equality of two objects (the `==` operator), use [`eq`](PyAny::eq). + /// the equality of two objects (the `==` operator), use [`eq`](PyAnyMethods::eq). /// /// This is equivalent to the Python expression `self is other`. fn is(&self, other: &T) -> bool; @@ -1589,10 +1592,10 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed { /// Downcast this `PyAny` to a concrete Python type or pyclass (but not a subclass of it). /// - /// It is almost always better to use [`PyAny::downcast`] because it accounts for Python + /// It is almost always better to use [`PyAnyMethods::downcast`] because it accounts for Python /// subtyping. Use this method only when you do not want to allow subtypes. /// - /// The advantage of this method over [`PyAny::downcast`] is that it is faster. The implementation + /// The advantage of this method over [`PyAnyMethods::downcast`] is that it is faster. The implementation /// of `downcast_exact` uses the equivalent of the Python expression `type(self) is T`, whereas /// `downcast` uses `isinstance(self, T)`. /// diff --git a/src/types/bytearray.rs b/src/types/bytearray.rs index bdf677c9019..ef20509e629 100644 --- a/src/types/bytearray.rs +++ b/src/types/bytearray.rs @@ -3,7 +3,9 @@ use crate::ffi_ptr_ext::FfiPtrExt; use crate::instance::{Borrowed, Bound}; use crate::py_result_ext::PyResultExt; use crate::types::any::PyAnyMethods; -use crate::{ffi, AsPyPointer, PyAny, PyNativeType, Python}; +#[cfg(feature = "gil-refs")] +use crate::AsPyPointer; +use crate::{ffi, PyAny, PyNativeType, Python}; use std::os::raw::c_char; use std::slice; diff --git a/src/types/float.rs b/src/types/float.rs index 3a64694a624..2ed3d2921b9 100644 --- a/src/types/float.rs +++ b/src/types/float.rs @@ -11,7 +11,7 @@ use super::any::PyAnyMethods; /// Represents a Python `float` object. /// /// You can usually avoid directly working with this type -/// by using [`ToPyObject`] and [`extract`](PyAny::extract) +/// by using [`ToPyObject`] and [`extract`](PyAnyMethods::extract) /// with `f32`/`f64`. #[repr(transparent)] pub struct PyFloat(PyAny); diff --git a/src/types/iterator.rs b/src/types/iterator.rs index 6131033af7d..4562efde3f8 100644 --- a/src/types/iterator.rs +++ b/src/types/iterator.rs @@ -1,9 +1,9 @@ use crate::ffi_ptr_ext::FfiPtrExt; use crate::instance::Borrowed; use crate::py_result_ext::PyResultExt; -use crate::{ffi, AsPyPointer, Bound, PyAny, PyErr, PyResult, PyTypeCheck}; +use crate::{ffi, Bound, PyAny, PyErr, PyResult, PyTypeCheck}; #[cfg(feature = "gil-refs")] -use crate::{PyDowncastError, PyNativeType}; +use crate::{AsPyPointer, PyDowncastError, PyNativeType}; /// A Python iterator object. /// diff --git a/src/types/memoryview.rs b/src/types/memoryview.rs index 31afb372d7f..320b3f9f70b 100644 --- a/src/types/memoryview.rs +++ b/src/types/memoryview.rs @@ -1,9 +1,9 @@ use crate::err::PyResult; use crate::ffi_ptr_ext::FfiPtrExt; use crate::py_result_ext::PyResultExt; +use crate::{ffi, Bound, PyAny}; #[cfg(feature = "gil-refs")] -use crate::PyNativeType; -use crate::{ffi, AsPyPointer, Bound, PyAny}; +use crate::{AsPyPointer, PyNativeType}; /// Represents a Python `memoryview`. #[repr(transparent)] diff --git a/src/types/mod.rs b/src/types/mod.rs index 38c9238961d..2203ccdf2dc 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -130,7 +130,8 @@ macro_rules! pyobject_native_type_base( fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::result::Result<(), ::std::fmt::Error> { - let s = self.repr().or(::std::result::Result::Err(::std::fmt::Error))?; + use $crate::{PyNativeType, types::{PyAnyMethods, PyStringMethods}}; + let s = self.as_borrowed().repr().or(::std::result::Result::Err(::std::fmt::Error))?; f.write_str(&s.to_string_lossy()) } } @@ -139,19 +140,20 @@ macro_rules! pyobject_native_type_base( fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::result::Result<(), ::std::fmt::Error> { - use $crate::PyNativeType; - match self.str() { + use $crate::{PyNativeType, types::{PyAnyMethods, PyStringMethods, PyTypeMethods}}; + match self.as_borrowed().str() { ::std::result::Result::Ok(s) => return f.write_str(&s.to_string_lossy()), ::std::result::Result::Err(err) => err.write_unraisable_bound(self.py(), ::std::option::Option::Some(&self.as_borrowed())), } - match self.get_type().name() { + match self.as_borrowed().get_type().name() { ::std::result::Result::Ok(name) => ::std::write!(f, "", name), ::std::result::Result::Err(_err) => f.write_str(""), } } } + #[cfg(feature = "gil-refs")] impl<$($generics,)*> $crate::ToPyObject for $name { #[inline] @@ -196,6 +198,7 @@ macro_rules! pyobject_native_type_named ( // FIXME https://github.com/PyO3/pyo3/issues/3903 #[allow(unknown_lints, non_local_definitions)] + #[cfg(feature = "gil-refs")] impl<$($generics,)*> $crate::IntoPy<$crate::Py<$name>> for &'_ $name { #[inline] fn into_py(self, py: $crate::Python<'_>) -> $crate::Py<$name> { @@ -205,6 +208,7 @@ macro_rules! pyobject_native_type_named ( // FIXME https://github.com/PyO3/pyo3/issues/3903 #[allow(unknown_lints, non_local_definitions)] + #[cfg(feature = "gil-refs")] impl<$($generics,)*> ::std::convert::From<&'_ $name> for $crate::Py<$name> { #[inline] fn from(other: &$name) -> Self { @@ -215,6 +219,7 @@ macro_rules! pyobject_native_type_named ( // FIXME https://github.com/PyO3/pyo3/issues/3903 #[allow(unknown_lints, non_local_definitions)] + #[cfg(feature = "gil-refs")] impl<'a, $($generics,)*> ::std::convert::From<&'a $name> for &'a $crate::PyAny { fn from(ob: &'a $name) -> Self { unsafe{&*(ob as *const $name as *const $crate::PyAny)} @@ -271,6 +276,7 @@ macro_rules! pyobject_native_type_extract { ($name:ty $(;$generics:ident)*) => { // FIXME https://github.com/PyO3/pyo3/issues/3903 #[allow(unknown_lints, non_local_definitions)] + #[cfg(feature = "gil-refs")] impl<'py, $($generics,)*> $crate::FromPyObject<'py> for &'py $name { #[inline] fn extract_bound(obj: &$crate::Bound<'py, $crate::PyAny>) -> $crate::PyResult { diff --git a/src/types/num.rs b/src/types/num.rs index 26748f7d1c7..924d4b2c593 100644 --- a/src/types/num.rs +++ b/src/types/num.rs @@ -4,7 +4,7 @@ use crate::{ffi, PyAny}; /// /// You can usually avoid directly working with this type /// by using [`ToPyObject`](crate::conversion::ToPyObject) -/// and [`extract`](PyAny::extract) +/// and [`extract`](super::PyAnyMethods::extract) /// with the primitive Rust integer types. #[repr(transparent)] pub struct PyLong(PyAny); diff --git a/tests/test_class_basics.rs b/tests/test_class_basics.rs index 8ff61bd2d6b..b7bee2638ca 100644 --- a/tests/test_class_basics.rs +++ b/tests/test_class_basics.rs @@ -310,15 +310,6 @@ impl ClassWithFromPyWithMethods { argument } - #[classmethod] - #[cfg(feature = "gil-refs")] - fn classmethod_gil_ref( - _cls: &PyType, - #[pyo3(from_py_with = "PyAny::len")] argument: usize, - ) -> usize { - argument - } - #[staticmethod] fn staticmethod(#[pyo3(from_py_with = "get_length")] argument: usize) -> usize { argument @@ -333,19 +324,15 @@ impl ClassWithFromPyWithMethods { fn test_pymethods_from_py_with() { Python::with_gil(|py| { let instance = Py::new(py, ClassWithFromPyWithMethods {}).unwrap(); - let has_gil_refs = cfg!(feature = "gil-refs"); py_run!( py, - instance - has_gil_refs, + instance, r#" arg = {1: 1, 2: 3} assert instance.instance_method(arg) == 2 assert instance.classmethod(arg) == 2 - if has_gil_refs: - assert instance.classmethod_gil_ref(arg) == 2 assert instance.staticmethod(arg) == 2 assert 42 in instance diff --git a/tests/test_compile_error.rs b/tests/test_compile_error.rs index 30e77888cfd..975d26009a5 100644 --- a/tests/test_compile_error.rs +++ b/tests/test_compile_error.rs @@ -20,7 +20,7 @@ fn test_compile_errors() { t.compile_fail("tests/ui/invalid_pymethod_names.rs"); t.compile_fail("tests/ui/invalid_pymodule_args.rs"); t.compile_fail("tests/ui/reject_generics.rs"); - #[cfg(not(feature = "gil-refs"))] + #[cfg(feature = "gil-refs")] t.compile_fail("tests/ui/deprecations.rs"); t.compile_fail("tests/ui/invalid_closure.rs"); t.compile_fail("tests/ui/pyclass_send.rs"); @@ -38,7 +38,13 @@ fn test_compile_errors() { t.compile_fail("tests/ui/invalid_pymethod_receiver.rs"); t.compile_fail("tests/ui/missing_intopy.rs"); // adding extra error conversion impls changes the output - #[cfg(not(any(windows, feature = "eyre", feature = "anyhow", Py_LIMITED_API)))] + #[cfg(not(any( + windows, + feature = "eyre", + feature = "anyhow", + feature = "gil-refs", + Py_LIMITED_API + )))] t.compile_fail("tests/ui/invalid_result_conversion.rs"); t.compile_fail("tests/ui/not_send.rs"); t.compile_fail("tests/ui/not_send2.rs"); diff --git a/tests/test_methods.rs b/tests/test_methods.rs index 2b5396e9ee4..c1610be3dd6 100644 --- a/tests/test_methods.rs +++ b/tests/test_methods.rs @@ -77,13 +77,6 @@ impl ClassMethod { Ok(format!("{}.method()!", cls.qualname()?)) } - #[classmethod] - /// Test class method. - #[cfg(feature = "gil-refs")] - fn method_gil_ref(cls: &PyType) -> PyResult { - Ok(format!("{}.method()!", cls.qualname()?)) - } - #[classmethod] fn method_owned(cls: Py) -> PyResult { let qualname = Python::with_gil(|gil| cls.bind(gil).qualname())?; diff --git a/tests/test_module.rs b/tests/test_module.rs index 5760c3ebaf3..b2487cfd8b3 100644 --- a/tests/test_module.rs +++ b/tests/test_module.rs @@ -371,13 +371,6 @@ fn pyfunction_with_module<'py>(module: &Bound<'py, PyModule>) -> PyResult PyResult<&str> { - module.name() -} - #[pyfunction] #[pyo3(pass_module)] fn pyfunction_with_module_owned( @@ -426,28 +419,14 @@ fn pyfunction_with_module_and_args_kwargs<'py>( .map(|s| (s, args.len(), kwargs.map(|d| d.len()))) } -#[pyfunction] -#[pyo3(pass_module)] -#[cfg(feature = "gil-refs")] -fn pyfunction_with_pass_module_in_attribute(module: &PyModule) -> PyResult<&str> { - module.name() -} - #[pymodule] fn module_with_functions_with_module(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(pyfunction_with_module, m)?)?; - #[cfg(feature = "gil-refs")] - m.add_function(wrap_pyfunction!(pyfunction_with_module_gil_ref, m)?)?; m.add_function(wrap_pyfunction!(pyfunction_with_module_owned, m)?)?; m.add_function(wrap_pyfunction!(pyfunction_with_module_and_py, m)?)?; m.add_function(wrap_pyfunction!(pyfunction_with_module_and_arg, m)?)?; m.add_function(wrap_pyfunction!(pyfunction_with_module_and_default_arg, m)?)?; m.add_function(wrap_pyfunction!(pyfunction_with_module_and_args_kwargs, m)?)?; - #[cfg(feature = "gil-refs")] - m.add_function(wrap_pyfunction!( - pyfunction_with_pass_module_in_attribute, - m - )?)?; m.add_function(wrap_pyfunction!(pyfunction_with_module, m)?)?; Ok(()) } @@ -461,12 +440,6 @@ fn test_module_functions_with_module() { m, "m.pyfunction_with_module() == 'module_with_functions_with_module'" ); - #[cfg(feature = "gil-refs")] - py_assert!( - py, - m, - "m.pyfunction_with_module_gil_ref() == 'module_with_functions_with_module'" - ); py_assert!( py, m, @@ -489,12 +462,6 @@ fn test_module_functions_with_module() { "m.pyfunction_with_module_and_args_kwargs(1, x=1, y=2) \ == ('module_with_functions_with_module', 1, 2)" ); - #[cfg(feature = "gil-refs")] - py_assert!( - py, - m, - "m.pyfunction_with_pass_module_in_attribute() == 'module_with_functions_with_module'" - ); }); } diff --git a/tests/test_proto_methods.rs b/tests/test_proto_methods.rs index c5d7306086d..5f0fa105e1f 100644 --- a/tests/test_proto_methods.rs +++ b/tests/test_proto_methods.rs @@ -247,9 +247,9 @@ fn mapping() { } #[derive(FromPyObject)] -enum SequenceIndex<'a> { +enum SequenceIndex<'py> { Integer(isize), - Slice(&'a PySlice), + Slice(Bound<'py, PySlice>), } #[pyclass] diff --git a/tests/ui/deprecations.stderr b/tests/ui/deprecations.stderr index 9c61c26581e..dc21b595743 100644 --- a/tests/ui/deprecations.stderr +++ b/tests/ui/deprecations.stderr @@ -10,12 +10,6 @@ note: the lint level is defined here 1 | #![deny(deprecated)] | ^^^^^^^^^^ -error: use of deprecated struct `pyo3::PyCell`: `PyCell` was merged into `Bound`, use that instead; see the migration guide for more info - --> tests/ui/deprecations.rs:23:30 - | -23 | fn method_gil_ref(_slf: &PyCell) {} - | ^^^^^^ - error: use of deprecated method `pyo3::deprecations::GilRefs::::from_py_with_arg`: use `&Bound<'_, PyAny>` as the argument for this `from_py_with` extractor --> tests/ui/deprecations.rs:42:44 | diff --git a/tests/ui/invalid_result_conversion.stderr b/tests/ui/invalid_result_conversion.stderr index 9695c5e6a19..8da8f49fac3 100644 --- a/tests/ui/invalid_result_conversion.stderr +++ b/tests/ui/invalid_result_conversion.stderr @@ -9,10 +9,10 @@ error[E0277]: the trait bound `PyErr: From` is not satisfied > > > - >> >> >> > + > and $N others = note: required for `MyError` to implement `Into` = note: this error originates in the attribute macro `pyfunction` (in Nightly builds, run with -Z macro-backtrace for more info) From f3c7b90deff8abf0c3bc2dcfd8c08fa7e0e05a91 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Thu, 9 May 2024 23:22:17 +0100 Subject: [PATCH 066/495] remove function pointer wrappers no longer needed for MSRV (#4167) --- pyo3-macros-backend/src/method.rs | 12 +++---- pyo3-macros-backend/src/pyclass.rs | 2 +- pyo3-macros-backend/src/pyimpl.rs | 2 +- pyo3-macros-backend/src/pymethod.rs | 6 ++-- src/impl_/pyclass/lazy_type_object.rs | 2 +- src/impl_/pymethods.rs | 52 +++++++++++---------------- src/pyclass/create_type_object.rs | 4 +-- src/types/function.rs | 22 +++--------- 8 files changed, 39 insertions(+), 63 deletions(-) diff --git a/pyo3-macros-backend/src/method.rs b/pyo3-macros-backend/src/method.rs index cb86e8ec606..e1a025b819e 100644 --- a/pyo3-macros-backend/src/method.rs +++ b/pyo3-macros-backend/src/method.rs @@ -828,7 +828,7 @@ impl<'a> FnSpec<'a> { CallingConvention::Noargs => quote! { #pyo3_path::impl_::pymethods::PyMethodDef::noargs( #python_name, - #pyo3_path::impl_::pymethods::PyCFunction({ + { unsafe extern "C" fn trampoline( _slf: *mut #pyo3_path::ffi::PyObject, _args: *mut #pyo3_path::ffi::PyObject, @@ -841,14 +841,14 @@ impl<'a> FnSpec<'a> { ) } trampoline - }), + }, #doc, ) }, CallingConvention::Fastcall => quote! { #pyo3_path::impl_::pymethods::PyMethodDef::fastcall_cfunction_with_keywords( #python_name, - #pyo3_path::impl_::pymethods::PyCFunctionFastWithKeywords({ + { unsafe extern "C" fn trampoline( _slf: *mut #pyo3_path::ffi::PyObject, _args: *const *mut #pyo3_path::ffi::PyObject, @@ -865,14 +865,14 @@ impl<'a> FnSpec<'a> { ) } trampoline - }), + }, #doc, ) }, CallingConvention::Varargs => quote! { #pyo3_path::impl_::pymethods::PyMethodDef::cfunction_with_keywords( #python_name, - #pyo3_path::impl_::pymethods::PyCFunctionWithKeywords({ + { unsafe extern "C" fn trampoline( _slf: *mut #pyo3_path::ffi::PyObject, _args: *mut #pyo3_path::ffi::PyObject, @@ -887,7 +887,7 @@ impl<'a> FnSpec<'a> { ) } trampoline - }), + }, #doc, ) }, diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index 3023f897645..179fe71bb9e 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -1129,7 +1129,7 @@ pub fn gen_complex_enum_variant_attr( #pyo3_path::class::PyMethodDefType::ClassAttribute({ #pyo3_path::class::PyClassAttributeDef::new( #python_name, - #pyo3_path::impl_::pymethods::PyClassAttributeFactory(#cls_type::#wrapper_ident) + #cls_type::#wrapper_ident ) }) }; diff --git a/pyo3-macros-backend/src/pyimpl.rs b/pyo3-macros-backend/src/pyimpl.rs index cf27cf37066..0cb7631a4df 100644 --- a/pyo3-macros-backend/src/pyimpl.rs +++ b/pyo3-macros-backend/src/pyimpl.rs @@ -200,7 +200,7 @@ pub fn gen_py_const(cls: &syn::Type, spec: &ConstSpec<'_>, ctx: &Ctx) -> MethodA #pyo3_path::class::PyMethodDefType::ClassAttribute({ #pyo3_path::class::PyClassAttributeDef::new( #python_name, - #pyo3_path::impl_::pymethods::PyClassAttributeFactory(#cls::#wrapper_ident) + #cls::#wrapper_ident ) }) }; diff --git a/pyo3-macros-backend/src/pymethod.rs b/pyo3-macros-backend/src/pymethod.rs index 1ef137cfcc8..3e8c2980700 100644 --- a/pyo3-macros-backend/src/pymethod.rs +++ b/pyo3-macros-backend/src/pymethod.rs @@ -512,7 +512,7 @@ fn impl_py_class_attribute( #pyo3_path::class::PyMethodDefType::ClassAttribute({ #pyo3_path::class::PyClassAttributeDef::new( #python_name, - #pyo3_path::impl_::pymethods::PyClassAttributeFactory(#cls::#wrapper_ident) + #cls::#wrapper_ident ) }) }; @@ -699,7 +699,7 @@ pub fn impl_py_setter_def( #pyo3_path::class::PyMethodDefType::Setter( #pyo3_path::class::PySetterDef::new( #python_name, - #pyo3_path::impl_::pymethods::PySetter(#cls::#wrapper_ident), + #cls::#wrapper_ident, #doc ) ) @@ -831,7 +831,7 @@ pub fn impl_py_getter_def( #pyo3_path::class::PyMethodDefType::Getter( #pyo3_path::class::PyGetterDef::new( #python_name, - #pyo3_path::impl_::pymethods::PyGetter(#cls::#wrapper_ident), + #cls::#wrapper_ident, #doc ) ) diff --git a/src/impl_/pyclass/lazy_type_object.rs b/src/impl_/pyclass/lazy_type_object.rs index efb6ecf37e6..f83fa4c5186 100644 --- a/src/impl_/pyclass/lazy_type_object.rs +++ b/src/impl_/pyclass/lazy_type_object.rs @@ -153,7 +153,7 @@ impl LazyTypeObjectInner { if let PyMethodDefType::ClassAttribute(attr) = def { let key = attr.attribute_c_string().unwrap(); - match (attr.meth.0)(py) { + match (attr.meth)(py) { Ok(val) => items.push((key, val)), Err(err) => { return Err(wrap_in_runtime_error( diff --git a/src/impl_/pymethods.rs b/src/impl_/pymethods.rs index df89dba7dbd..2e9dd0ac520 100644 --- a/src/impl_/pymethods.rs +++ b/src/impl_/pymethods.rs @@ -69,27 +69,13 @@ pub enum PyMethodDefType { #[derive(Copy, Clone, Debug)] pub enum PyMethodType { - PyCFunction(PyCFunction), - PyCFunctionWithKeywords(PyCFunctionWithKeywords), + PyCFunction(ffi::PyCFunction), + PyCFunctionWithKeywords(ffi::PyCFunctionWithKeywords), #[cfg(not(Py_LIMITED_API))] - PyCFunctionFastWithKeywords(PyCFunctionFastWithKeywords), -} - -// These newtype structs serve no purpose other than wrapping which are function pointers - because -// function pointers aren't allowed in const fn, but types wrapping them are! -#[derive(Clone, Copy, Debug)] -pub struct PyCFunction(pub ffi::PyCFunction); -#[derive(Clone, Copy, Debug)] -pub struct PyCFunctionWithKeywords(pub ffi::PyCFunctionWithKeywords); -#[cfg(not(Py_LIMITED_API))] -#[derive(Clone, Copy, Debug)] -pub struct PyCFunctionFastWithKeywords(pub ffi::_PyCFunctionFastWithKeywords); -#[derive(Clone, Copy)] -pub struct PyGetter(pub Getter); -#[derive(Clone, Copy)] -pub struct PySetter(pub Setter); -#[derive(Clone, Copy)] -pub struct PyClassAttributeFactory(pub for<'p> fn(Python<'p>) -> PyResult); + PyCFunctionFastWithKeywords(ffi::_PyCFunctionFastWithKeywords), +} + +pub type PyClassAttributeFactory = for<'p> fn(Python<'p>) -> PyResult; // TODO: it would be nice to use CStr in these types, but then the constructors can't be const fn // until `CStr::from_bytes_with_nul_unchecked` is const fn. @@ -117,14 +103,14 @@ impl PyClassAttributeDef { #[derive(Clone)] pub struct PyGetterDef { pub(crate) name: &'static str, - pub(crate) meth: PyGetter, + pub(crate) meth: Getter, pub(crate) doc: &'static str, } #[derive(Clone)] pub struct PySetterDef { pub(crate) name: &'static str, - pub(crate) meth: PySetter, + pub(crate) meth: Setter, pub(crate) doc: &'static str, } @@ -136,7 +122,11 @@ unsafe impl Sync for PySetterDef {} impl PyMethodDef { /// Define a function with no `*args` and `**kwargs`. - pub const fn noargs(name: &'static str, cfunction: PyCFunction, doc: &'static str) -> Self { + pub const fn noargs( + name: &'static str, + cfunction: ffi::PyCFunction, + doc: &'static str, + ) -> Self { Self { ml_name: name, ml_meth: PyMethodType::PyCFunction(cfunction), @@ -148,7 +138,7 @@ impl PyMethodDef { /// Define a function that can take `*args` and `**kwargs`. pub const fn cfunction_with_keywords( name: &'static str, - cfunction: PyCFunctionWithKeywords, + cfunction: ffi::PyCFunctionWithKeywords, doc: &'static str, ) -> Self { Self { @@ -163,7 +153,7 @@ impl PyMethodDef { #[cfg(not(Py_LIMITED_API))] pub const fn fastcall_cfunction_with_keywords( name: &'static str, - cfunction: PyCFunctionFastWithKeywords, + cfunction: ffi::_PyCFunctionFastWithKeywords, doc: &'static str, ) -> Self { Self { @@ -182,15 +172,13 @@ impl PyMethodDef { /// Convert `PyMethodDef` to Python method definition struct `ffi::PyMethodDef` pub(crate) fn as_method_def(&self) -> PyResult<(ffi::PyMethodDef, PyMethodDefDestructor)> { let meth = match self.ml_meth { - PyMethodType::PyCFunction(meth) => ffi::PyMethodDefPointer { - PyCFunction: meth.0, - }, + PyMethodType::PyCFunction(meth) => ffi::PyMethodDefPointer { PyCFunction: meth }, PyMethodType::PyCFunctionWithKeywords(meth) => ffi::PyMethodDefPointer { - PyCFunctionWithKeywords: meth.0, + PyCFunctionWithKeywords: meth, }, #[cfg(not(Py_LIMITED_API))] PyMethodType::PyCFunctionFastWithKeywords(meth) => ffi::PyMethodDefPointer { - _PyCFunctionFastWithKeywords: meth.0, + _PyCFunctionFastWithKeywords: meth, }, }; @@ -232,7 +220,7 @@ pub(crate) type Setter = impl PyGetterDef { /// Define a getter. - pub const fn new(name: &'static str, getter: PyGetter, doc: &'static str) -> Self { + pub const fn new(name: &'static str, getter: Getter, doc: &'static str) -> Self { Self { name, meth: getter, @@ -243,7 +231,7 @@ impl PyGetterDef { impl PySetterDef { /// Define a setter. - pub const fn new(name: &'static str, setter: PySetter, doc: &'static str) -> Self { + pub const fn new(name: &'static str, setter: Setter, doc: &'static str) -> Self { Self { name, meth: setter, diff --git a/src/pyclass/create_type_object.rs b/src/pyclass/create_type_object.rs index e90c5736e5c..1b3a9fb1296 100644 --- a/src/pyclass/create_type_object.rs +++ b/src/pyclass/create_type_object.rs @@ -508,7 +508,7 @@ impl GetSetDefBuilder { self.doc = Some(getter.doc); } // TODO: return an error if getter already defined? - self.getter = Some(getter.meth.0) + self.getter = Some(getter.meth) } fn add_setter(&mut self, setter: &PySetterDef) { @@ -517,7 +517,7 @@ impl GetSetDefBuilder { self.doc = Some(setter.doc); } // TODO: return an error if setter already defined? - self.setter = Some(setter.meth.0) + self.setter = Some(setter.meth) } fn as_get_set_def( diff --git a/src/types/function.rs b/src/types/function.rs index 09c5004b77b..a127b4e0574 100644 --- a/src/types/function.rs +++ b/src/types/function.rs @@ -37,11 +37,7 @@ impl PyCFunction { let (py, module) = py_or_module.into_py_and_maybe_module(); Self::internal_new( py, - &PyMethodDef::cfunction_with_keywords( - name, - pymethods::PyCFunctionWithKeywords(fun), - doc, - ), + &PyMethodDef::cfunction_with_keywords(name, fun, doc), module.map(PyNativeType::as_borrowed).as_deref(), ) .map(Bound::into_gil_ref) @@ -57,11 +53,7 @@ impl PyCFunction { ) -> PyResult> { Self::internal_new( py, - &PyMethodDef::cfunction_with_keywords( - name, - pymethods::PyCFunctionWithKeywords(fun), - doc, - ), + &PyMethodDef::cfunction_with_keywords(name, fun, doc), module, ) } @@ -81,7 +73,7 @@ impl PyCFunction { let (py, module) = py_or_module.into_py_and_maybe_module(); Self::internal_new( py, - &PyMethodDef::noargs(name, pymethods::PyCFunction(fun), doc), + &PyMethodDef::noargs(name, fun, doc), module.map(PyNativeType::as_borrowed).as_deref(), ) .map(Bound::into_gil_ref) @@ -95,11 +87,7 @@ impl PyCFunction { doc: &'static str, module: Option<&Bound<'py, PyModule>>, ) -> PyResult> { - Self::internal_new( - py, - &PyMethodDef::noargs(name, pymethods::PyCFunction(fun), doc), - module, - ) + Self::internal_new(py, &PyMethodDef::noargs(name, fun, doc), module) } /// Deprecated form of [`PyCFunction::new_closure`] @@ -153,7 +141,7 @@ impl PyCFunction { { let method_def = pymethods::PyMethodDef::cfunction_with_keywords( name.unwrap_or("pyo3-closure\0"), - pymethods::PyCFunctionWithKeywords(run_closure::), + run_closure::, doc.unwrap_or("\0"), ); let (def, def_destructor) = method_def.as_method_def()?; From 104328ce14a786a290537c6b2d542419a9e9f514 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Fri, 10 May 2024 01:54:08 -0400 Subject: [PATCH 067/495] feature gate deprecated more APIs for `Py` (#4169) --- .../python-from-rust/calling-existing-code.md | 10 ++-- src/err/mod.rs | 2 +- src/macros.rs | 4 +- src/marker.rs | 50 ++++++++----------- 4 files changed, 30 insertions(+), 36 deletions(-) diff --git a/guide/src/python-from-rust/calling-existing-code.md b/guide/src/python-from-rust/calling-existing-code.md index 572f4b4414f..9c0f592451f 100644 --- a/guide/src/python-from-rust/calling-existing-code.md +++ b/guide/src/python-from-rust/calling-existing-code.md @@ -24,9 +24,9 @@ fn main() -> PyResult<()> { } ``` -## Want to run just an expression? Then use `eval`. +## Want to run just an expression? Then use `eval_bound`. -[`Python::eval`]({{#PYO3_DOCS_URL}}/pyo3/marker/struct.Python.html#method.eval) is +[`Python::eval_bound`]({{#PYO3_DOCS_URL}}/pyo3/marker/struct.Python.html#method.eval_bound) is a method to execute a [Python expression](https://docs.python.org/3.7/reference/expressions.html) and return the evaluated value as a `Bound<'py, PyAny>` object. @@ -47,14 +47,14 @@ Python::with_gil(|py| { # } ``` -## Want to run statements? Then use `run`. +## Want to run statements? Then use `run_bound`. -[`Python::run`] is a method to execute one or more +[`Python::run_bound`] is a method to execute one or more [Python statements](https://docs.python.org/3.7/reference/simple_stmts.html). This method returns nothing (like any Python statement), but you can get access to manipulated objects via the `locals` dict. -You can also use the [`py_run!`] macro, which is a shorthand for [`Python::run`]. +You can also use the [`py_run!`] macro, which is a shorthand for [`Python::run_bound`]. Since [`py_run!`] panics on exceptions, we recommend you use this macro only for quickly testing your Python extensions. diff --git a/src/err/mod.rs b/src/err/mod.rs index 200c180e9b0..52e4b6c616a 100644 --- a/src/err/mod.rs +++ b/src/err/mod.rs @@ -724,7 +724,7 @@ impl PyErr { /// /// The `category` should be one of the `Warning` classes available in /// [`pyo3::exceptions`](crate::exceptions), or a subclass. The Python - /// object can be retrieved using [`Python::get_type()`]. + /// object can be retrieved using [`Python::get_type_bound()`]. /// /// Example: /// ```rust diff --git a/src/macros.rs b/src/macros.rs index 8bd79408a56..d6f25c37308 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -2,10 +2,10 @@ /// /// # Panics /// -/// This macro internally calls [`Python::run`](crate::Python::run) and panics +/// This macro internally calls [`Python::run_bound`](crate::Python::run_bound) and panics /// if it returns `Err`, after printing the error to stdout. /// -/// If you need to handle failures, please use [`Python::run`](crate::marker::Python::run) instead. +/// If you need to handle failures, please use [`Python::run_bound`](crate::marker::Python::run_bound) instead. /// /// # Examples /// ``` diff --git a/src/marker.rs b/src/marker.rs index 2230d776236..ea794856d66 100644 --- a/src/marker.rs +++ b/src/marker.rs @@ -126,7 +126,9 @@ use crate::types::{ PyAny, PyDict, PyEllipsis, PyModule, PyNone, PyNotImplemented, PyString, PyType, }; use crate::version::PythonVersionInfo; -use crate::{ffi, Bound, IntoPy, Py, PyNativeType, PyObject, PyTypeInfo}; +#[cfg(feature = "gil-refs")] +use crate::PyNativeType; +use crate::{ffi, Bound, IntoPy, Py, PyObject, PyTypeInfo}; #[allow(deprecated)] use crate::{gil::GILPool, FromPyPointer}; use std::ffi::{CStr, CString}; @@ -305,7 +307,7 @@ pub use nightly::Ungil; /// A marker token that represents holding the GIL. /// /// It serves three main purposes: -/// - It provides a global API for the Python interpreter, such as [`Python::eval`]. +/// - It provides a global API for the Python interpreter, such as [`Python::eval_bound`]. /// - It can be passed to functions that require a proof of holding the GIL, such as /// [`Py::clone_ref`]. /// - Its lifetime represents the scope of holding the GIL which can be used to create Rust @@ -321,7 +323,7 @@ pub use nightly::Ungil; /// - In a function or method annotated with [`#[pyfunction]`](crate::pyfunction) or [`#[pymethods]`](crate::pymethods) you can declare it /// as a parameter, and PyO3 will pass in the token when Python code calls it. /// - If you already have something with a lifetime bound to the GIL, such as `&`[`PyAny`], you can -/// use its [`.py()`][PyAny::py] method to get a token. +/// use its `.py()` method to get a token. /// - When you need to acquire the GIL yourself, such as when calling Python code from Rust, you /// should call [`Python::with_gil`] to do that and pass your code as a closure to it. /// @@ -352,7 +354,7 @@ pub use nightly::Ungil; /// # Releasing and freeing memory /// /// The [`Python`] type can be used to create references to variables owned by the Python -/// interpreter, using functions such as [`Python::eval`] and `PyModule::import`. These +/// interpreter, using functions such as `Python::eval` and `PyModule::import`. These /// references are tied to a [`GILPool`] whose references are not cleared until it is dropped. /// This can cause apparent "memory leaks" if it is kept around for a long time. /// @@ -552,12 +554,10 @@ impl<'py> Python<'py> { } /// Deprecated version of [`Python::eval_bound`] - #[cfg_attr( - not(feature = "gil-refs"), - deprecated( - since = "0.21.0", - note = "`Python::eval` will be replaced by `Python::eval_bound` in a future PyO3 version" - ) + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`Python::eval` will be replaced by `Python::eval_bound` in a future PyO3 version" )] pub fn eval( self, @@ -601,12 +601,10 @@ impl<'py> Python<'py> { } /// Deprecated version of [`Python::run_bound`] - #[cfg_attr( - not(feature = "gil-refs"), - deprecated( - since = "0.21.0", - note = "`Python::run` will be replaced by `Python::run_bound` in a future PyO3 version" - ) + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`Python::run` will be replaced by `Python::run_bound` in a future PyO3 version" )] pub fn run( self, @@ -728,12 +726,10 @@ impl<'py> Python<'py> { } /// Gets the Python type object for type `T`. - #[cfg_attr( - not(feature = "gil-refs"), - deprecated( - since = "0.21.0", - note = "`Python::get_type` will be replaced by `Python::get_type_bound` in a future PyO3 version" - ) + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`Python::get_type` will be replaced by `Python::get_type_bound` in a future PyO3 version" )] #[inline] pub fn get_type(self) -> &'py PyType @@ -753,12 +749,10 @@ impl<'py> Python<'py> { } /// Deprecated form of [`Python::import_bound`] - #[cfg_attr( - not(feature = "gil-refs"), - deprecated( - since = "0.21.0", - note = "`Python::import` will be replaced by `Python::import_bound` in a future PyO3 version" - ) + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "`Python::import` will be replaced by `Python::import_bound` in a future PyO3 version" )] pub fn import(self, name: N) -> PyResult<&'py PyModule> where From aef0a05719db45caba0ab90315d1b250a690e35b Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Fri, 10 May 2024 12:34:58 +0200 Subject: [PATCH 068/495] deprecate implicit default for trailing optional arguments (#4078) * deprecate "trailing optional arguments" implicit default behaviour * add newsfragment * generate individual deprecation messages per function * add migration guide entry --- guide/src/async-await.md | 1 + guide/src/function/signature.md | 13 +++++ guide/src/migration.md | 35 ++++++++++++ newsfragments/4078.changed.md | 1 + pyo3-macros-backend/src/deprecations.rs | 53 +++++++++++++++++- pyo3-macros-backend/src/method.rs | 7 +++ pyo3-macros-backend/src/pymethod.rs | 4 ++ pytests/src/datetime.rs | 3 ++ src/tests/hygiene/pymethods.rs | 1 + tests/test_arithmetics.rs | 1 + tests/test_mapping.rs | 2 + tests/test_methods.rs | 11 ++++ tests/test_pyfunction.rs | 3 ++ tests/test_sequence.rs | 1 + tests/test_text_signature.rs | 1 + tests/ui/deprecations.rs | 26 +++++++++ tests/ui/deprecations.stderr | 72 ++++++++++++++++--------- 17 files changed, 210 insertions(+), 25 deletions(-) create mode 100644 newsfragments/4078.changed.md diff --git a/guide/src/async-await.md b/guide/src/async-await.md index 06fa1580ad7..27574181804 100644 --- a/guide/src/async-await.md +++ b/guide/src/async-await.md @@ -12,6 +12,7 @@ use futures::channel::oneshot; use pyo3::prelude::*; #[pyfunction] +#[pyo3(signature=(seconds, result=None))] async fn sleep(seconds: f64, result: Option) -> Option { let (tx, rx) = oneshot::channel(); thread::spawn(move || { diff --git a/guide/src/function/signature.md b/guide/src/function/signature.md index b276fc457fb..69949220be6 100644 --- a/guide/src/function/signature.md +++ b/guide/src/function/signature.md @@ -121,9 +121,22 @@ num=-1 ## Trailing optional arguments +
+ +⚠️ Warning: This behaviour is being phased out 🛠️ + +The special casing of trailing optional arguments is deprecated. In a future `pyo3` version, arguments of type `Option<..>` will share the same behaviour as other arguments, they are required unless a default is set using `#[pyo3(signature = (...))]`. + +This is done to better align the Python and Rust definition of such functions and make it more intuitive to rewrite them from Python in Rust. Specifically `def some_fn(a: int, b: Optional[int]): ...` will not automatically default `b` to `none`, but requires an explicit default if desired, where as in current `pyo3` it is handled the other way around. + +During the migration window a `#[pyo3(signature = (...))]` will be required to silence the deprecation warning. After support for trailing optional arguments is fully removed, the signature attribute can be removed if all arguments should be required. +
+ + As a convenience, functions without a `#[pyo3(signature = (...))]` option will treat trailing `Option` arguments as having a default of `None`. In the example below, PyO3 will create `increment` with a signature of `increment(x, amount=None)`. ```rust +#![allow(deprecated)] use pyo3::prelude::*; /// Returns a copy of `x` increased by `amount`. diff --git a/guide/src/migration.md b/guide/src/migration.md index 2317f85185e..875407317bf 100644 --- a/guide/src/migration.md +++ b/guide/src/migration.md @@ -3,6 +3,41 @@ This guide can help you upgrade code through breaking changes from one PyO3 version to the next. For a detailed list of all changes, see the [CHANGELOG](changelog.md). +## from 0.21.* to 0.22 + +### Deprecation of implicit default for trailing optional arguments +
+Click to expand + +With `pyo3` 0.22 the implicit `None` default for trailing `Option` type argument is deprecated. To migrate, place a `#[pyo3(signature = (...))]` attribute on affected functions or methods and specify the desired behavior. +The migration warning specifies the corresponding signature to keep the current behavior. With 0.23 the signature will be required for any function containing `Option` type parameters to prevent accidental +and unnoticed changes in behavior. With 0.24 this restriction will be lifted again and `Option` type arguments will be treated as any other argument _without_ special handling. + +Before: + +```rust +# #![allow(deprecated, dead_code)] +# use pyo3::prelude::*; +#[pyfunction] +fn increment(x: u64, amount: Option) -> u64 { + x + amount.unwrap_or(1) +} +``` + +After: + +```rust +# #![allow(dead_code)] +# use pyo3::prelude::*; +#[pyfunction] +#[pyo3(signature = (x, amount=None))] +fn increment(x: u64, amount: Option) -> u64 { + x + amount.unwrap_or(1) +} +``` + +
+ ## from 0.20.* to 0.21
Click to expand diff --git a/newsfragments/4078.changed.md b/newsfragments/4078.changed.md new file mode 100644 index 00000000000..45f160f5556 --- /dev/null +++ b/newsfragments/4078.changed.md @@ -0,0 +1 @@ +deprecate implicit default for trailing optional arguments diff --git a/pyo3-macros-backend/src/deprecations.rs b/pyo3-macros-backend/src/deprecations.rs index 3f1f34144f6..4db40cc86f7 100644 --- a/pyo3-macros-backend/src/deprecations.rs +++ b/pyo3-macros-backend/src/deprecations.rs @@ -1,4 +1,7 @@ -use crate::utils::Ctx; +use crate::{ + method::{FnArg, FnSpec}, + utils::Ctx, +}; use proc_macro2::{Span, TokenStream}; use quote::{quote_spanned, ToTokens}; @@ -45,3 +48,51 @@ impl<'ctx> ToTokens for Deprecations<'ctx> { } } } + +pub(crate) fn deprecate_trailing_option_default(spec: &FnSpec<'_>) -> TokenStream { + if spec.signature.attribute.is_none() + && spec.signature.arguments.iter().any(|arg| { + if let FnArg::Regular(arg) = arg { + arg.option_wrapped_type.is_some() + } else { + false + } + }) + { + use std::fmt::Write; + let mut deprecation_msg = String::from( + "This function has implicit defaults for the trailing `Option` arguments. \ + These implicit defaults are being phased out. Add `#[pyo3(signature = (", + ); + spec.signature.arguments.iter().for_each(|arg| { + match arg { + FnArg::Regular(arg) => { + if arg.option_wrapped_type.is_some() { + write!(deprecation_msg, "{}=None, ", arg.name) + } else { + write!(deprecation_msg, "{}, ", arg.name) + } + } + FnArg::VarArgs(arg) => write!(deprecation_msg, "{}, ", arg.name), + FnArg::KwArgs(arg) => write!(deprecation_msg, "{}, ", arg.name), + FnArg::Py(_) | FnArg::CancelHandle(_) => Ok(()), + } + .expect("writing to `String` should not fail"); + }); + + //remove trailing space and comma + deprecation_msg.pop(); + deprecation_msg.pop(); + + deprecation_msg + .push_str(")]` to this function to silence this warning and keep the current behavior"); + quote_spanned! { spec.name.span() => + #[deprecated(note = #deprecation_msg)] + #[allow(dead_code)] + const SIGNATURE: () = (); + const _: () = SIGNATURE; + } + } else { + TokenStream::new() + } +} diff --git a/pyo3-macros-backend/src/method.rs b/pyo3-macros-backend/src/method.rs index e1a025b819e..c0e38bf8416 100644 --- a/pyo3-macros-backend/src/method.rs +++ b/pyo3-macros-backend/src/method.rs @@ -5,6 +5,7 @@ use proc_macro2::{Span, TokenStream}; use quote::{format_ident, quote, quote_spanned, ToTokens}; use syn::{ext::IdentExt, spanned::Spanned, Ident, Result}; +use crate::deprecations::deprecate_trailing_option_default; use crate::utils::Ctx; use crate::{ attributes::{FromPyWithAttribute, TextSignatureAttribute, TextSignatureAttributeValue}, @@ -708,6 +709,8 @@ impl<'a> FnSpec<'a> { quote!(#func_name) }; + let deprecation = deprecate_trailing_option_default(self); + Ok(match self.convention { CallingConvention::Noargs => { let mut holders = Holders::new(); @@ -730,6 +733,7 @@ impl<'a> FnSpec<'a> { py: #pyo3_path::Python<'py>, _slf: *mut #pyo3_path::ffi::PyObject, ) -> #pyo3_path::PyResult<*mut #pyo3_path::ffi::PyObject> { + #deprecation let _slf_ref = &_slf; let function = #rust_name; // Shadow the function name to avoid #3017 #init_holders @@ -754,6 +758,7 @@ impl<'a> FnSpec<'a> { _nargs: #pyo3_path::ffi::Py_ssize_t, _kwnames: *mut #pyo3_path::ffi::PyObject ) -> #pyo3_path::PyResult<*mut #pyo3_path::ffi::PyObject> { + #deprecation let _slf_ref = &_slf; let function = #rust_name; // Shadow the function name to avoid #3017 #arg_convert @@ -778,6 +783,7 @@ impl<'a> FnSpec<'a> { _args: *mut #pyo3_path::ffi::PyObject, _kwargs: *mut #pyo3_path::ffi::PyObject ) -> #pyo3_path::PyResult<*mut #pyo3_path::ffi::PyObject> { + #deprecation let _slf_ref = &_slf; let function = #rust_name; // Shadow the function name to avoid #3017 #arg_convert @@ -805,6 +811,7 @@ impl<'a> FnSpec<'a> { _kwargs: *mut #pyo3_path::ffi::PyObject ) -> #pyo3_path::PyResult<*mut #pyo3_path::ffi::PyObject> { use #pyo3_path::callback::IntoPyCallbackOutput; + #deprecation let _slf_ref = &_slf; let function = #rust_name; // Shadow the function name to avoid #3017 #arg_convert diff --git a/pyo3-macros-backend/src/pymethod.rs b/pyo3-macros-backend/src/pymethod.rs index 3e8c2980700..208735f2619 100644 --- a/pyo3-macros-backend/src/pymethod.rs +++ b/pyo3-macros-backend/src/pymethod.rs @@ -1,6 +1,7 @@ use std::borrow::Cow; use crate::attributes::{NameAttribute, RenamingRule}; +use crate::deprecations::deprecate_trailing_option_default; use crate::method::{CallingConvention, ExtractErrorMode, PyArg}; use crate::params::{check_arg_for_gil_refs, impl_regular_arg_param, Holders}; use crate::utils::Ctx; @@ -637,7 +638,10 @@ pub fn impl_py_setter_def( ); let extract = check_arg_for_gil_refs(tokens, holders.push_gil_refs_checker(arg.ty.span()), ctx); + + let deprecation = deprecate_trailing_option_default(spec); quote! { + #deprecation #from_py_with let _val = #extract; } diff --git a/pytests/src/datetime.rs b/pytests/src/datetime.rs index d0de99ae406..e26782d04f7 100644 --- a/pytests/src/datetime.rs +++ b/pytests/src/datetime.rs @@ -25,6 +25,7 @@ fn date_from_timestamp(py: Python<'_>, timestamp: i64) -> PyResult( py: Python<'py>, hour: u8, @@ -101,6 +102,7 @@ fn get_delta_tuple<'py>(delta: &Bound<'py, PyDelta>) -> Bound<'py, PyTuple> { #[allow(clippy::too_many_arguments)] #[pyfunction] +#[pyo3(signature=(year, month, day, hour, minute, second, microsecond, tzinfo=None))] fn make_datetime<'py>( py: Python<'py>, year: i32, @@ -159,6 +161,7 @@ fn get_datetime_tuple_fold<'py>(dt: &Bound<'py, PyDateTime>) -> Bound<'py, PyTup } #[pyfunction] +#[pyo3(signature=(ts, tz=None))] fn datetime_from_timestamp<'py>( py: Python<'py>, ts: f64, diff --git a/src/tests/hygiene/pymethods.rs b/src/tests/hygiene/pymethods.rs index 020f983be31..95d670c63a6 100644 --- a/src/tests/hygiene/pymethods.rs +++ b/src/tests/hygiene/pymethods.rs @@ -309,6 +309,7 @@ impl Dummy { 0 } + #[pyo3(signature=(ndigits=::std::option::Option::None))] fn __round__(&self, ndigits: ::std::option::Option) -> u32 { 0 } diff --git a/tests/test_arithmetics.rs b/tests/test_arithmetics.rs index b1efcd0ac55..007f42a79e8 100644 --- a/tests/test_arithmetics.rs +++ b/tests/test_arithmetics.rs @@ -35,6 +35,7 @@ impl UnaryArithmetic { Self::new(self.inner.abs()) } + #[pyo3(signature=(_ndigits=None))] fn __round__(&self, _ndigits: Option) -> Self { Self::new(self.inner.round()) } diff --git a/tests/test_mapping.rs b/tests/test_mapping.rs index 675dd14d63f..784ab8845cd 100644 --- a/tests/test_mapping.rs +++ b/tests/test_mapping.rs @@ -21,6 +21,7 @@ struct Mapping { #[pymethods] impl Mapping { #[new] + #[pyo3(signature=(elements=None))] fn new(elements: Option<&Bound<'_, PyList>>) -> PyResult { if let Some(pylist) = elements { let mut elems = HashMap::with_capacity(pylist.len()); @@ -59,6 +60,7 @@ impl Mapping { } } + #[pyo3(signature=(key, default=None))] fn get(&self, py: Python<'_>, key: &str, default: Option) -> Option { self.index .get(key) diff --git a/tests/test_methods.rs b/tests/test_methods.rs index c1610be3dd6..37f3b2d8bd6 100644 --- a/tests/test_methods.rs +++ b/tests/test_methods.rs @@ -187,6 +187,7 @@ impl MethSignature { fn get_optional2(&self, test: Option) -> Option { test } + #[pyo3(signature=(_t1 = None, t2 = None, _t3 = None))] fn get_optional_positional( &self, _t1: Option, @@ -745,11 +746,13 @@ impl MethodWithPyClassArg { fn inplace_add_pyref(&self, mut other: PyRefMut<'_, MethodWithPyClassArg>) { other.value += self.value; } + #[pyo3(signature=(other = None))] fn optional_add(&self, other: Option<&MethodWithPyClassArg>) -> MethodWithPyClassArg { MethodWithPyClassArg { value: self.value + other.map(|o| o.value).unwrap_or(10), } } + #[pyo3(signature=(other = None))] fn optional_inplace_add(&self, other: Option<&mut MethodWithPyClassArg>) { if let Some(other) = other { other.value += self.value; @@ -851,6 +854,7 @@ struct FromSequence { #[pymethods] impl FromSequence { #[new] + #[pyo3(signature=(seq = None))] fn new(seq: Option<&Bound<'_, PySequence>>) -> PyResult { if let Some(seq) = seq { Ok(FromSequence { @@ -1026,6 +1030,7 @@ macro_rules! issue_1506 { issue_1506!( #[pymethods] impl Issue1506 { + #[pyo3(signature = (_arg, _args, _kwargs=None))] fn issue_1506( &self, _py: Python<'_>, @@ -1035,6 +1040,7 @@ issue_1506!( ) { } + #[pyo3(signature = (_arg, _args, _kwargs=None))] fn issue_1506_mut( &mut self, _py: Python<'_>, @@ -1044,6 +1050,7 @@ issue_1506!( ) { } + #[pyo3(signature = (_arg, _args, _kwargs=None))] fn issue_1506_custom_receiver( _slf: Py, _py: Python<'_>, @@ -1053,6 +1060,7 @@ issue_1506!( ) { } + #[pyo3(signature = (_arg, _args, _kwargs=None))] fn issue_1506_custom_receiver_explicit( _slf: Py, _py: Python<'_>, @@ -1063,6 +1071,7 @@ issue_1506!( } #[new] + #[pyo3(signature = (_arg, _args, _kwargs=None))] fn issue_1506_new( _py: Python<'_>, _arg: &Bound<'_, PyAny>, @@ -1081,6 +1090,7 @@ issue_1506!( fn issue_1506_setter(&self, _py: Python<'_>, _value: i32) {} #[staticmethod] + #[pyo3(signature = (_arg, _args, _kwargs=None))] fn issue_1506_static( _py: Python<'_>, _arg: &Bound<'_, PyAny>, @@ -1090,6 +1100,7 @@ issue_1506!( } #[classmethod] + #[pyo3(signature = (_arg, _args, _kwargs=None))] fn issue_1506_class( _cls: &Bound<'_, PyType>, _py: Python<'_>, diff --git a/tests/test_pyfunction.rs b/tests/test_pyfunction.rs index 8a57e2707c9..4a90f3f9d99 100644 --- a/tests/test_pyfunction.rs +++ b/tests/test_pyfunction.rs @@ -182,6 +182,7 @@ fn test_from_py_with_defaults() { // issue 2280 combination of from_py_with and Option did not compile #[pyfunction] + #[pyo3(signature = (int=None))] fn from_py_with_option(#[pyo3(from_py_with = "optional_int")] int: Option) -> i32 { int.unwrap_or(0) } @@ -216,6 +217,7 @@ struct ValueClass { } #[pyfunction] +#[pyo3(signature=(str_arg, int_arg, tuple_arg, option_arg = None, struct_arg = None))] fn conversion_error( str_arg: &str, int_arg: i64, @@ -542,6 +544,7 @@ fn test_some_wrap_arguments() { #[test] fn test_reference_to_bound_arguments() { #[pyfunction] + #[pyo3(signature = (x, y = None))] fn reference_args<'py>( x: &Bound<'py, PyAny>, y: Option<&Bound<'py, PyAny>>, diff --git a/tests/test_sequence.rs b/tests/test_sequence.rs index 3715affc05b..9627f06ca75 100644 --- a/tests/test_sequence.rs +++ b/tests/test_sequence.rs @@ -17,6 +17,7 @@ struct ByteSequence { #[pymethods] impl ByteSequence { #[new] + #[pyo3(signature=(elements = None))] fn new(elements: Option<&Bound<'_, PyList>>) -> PyResult { if let Some(pylist) = elements { let mut elems = Vec::with_capacity(pylist.len()); diff --git a/tests/test_text_signature.rs b/tests/test_text_signature.rs index a9f5a041596..3899878bd56 100644 --- a/tests/test_text_signature.rs +++ b/tests/test_text_signature.rs @@ -142,6 +142,7 @@ fn test_auto_test_signature_function() { } #[pyfunction] + #[pyo3(signature=(a, b=None, c=None))] fn my_function_6(a: i32, b: Option, c: Option) { let _ = (a, b, c); } diff --git a/tests/ui/deprecations.rs b/tests/ui/deprecations.rs index ef0b06652e4..fc9e8687cae 100644 --- a/tests/ui/deprecations.rs +++ b/tests/ui/deprecations.rs @@ -39,6 +39,9 @@ impl MyClass { #[setter] fn set_bar_bound(&self, _value: &Bound<'_, PyAny>) {} + #[setter] + fn set_option(&self, _value: Option) {} + fn __eq__(&self, #[pyo3(from_py_with = "extract_gil_ref")] _other: i32) -> bool { true } @@ -103,6 +106,10 @@ fn extract_bound(obj: &Bound<'_, PyAny>) -> PyResult { obj.extract() } +fn extract_options(obj: &Bound<'_, PyAny>) -> PyResult> { + obj.extract() +} + #[pyfunction] fn pyfunction_from_py_with( #[pyo3(from_py_with = "extract_gil_ref")] _gil_ref: i32, @@ -114,8 +121,27 @@ fn pyfunction_from_py_with( fn pyfunction_gil_ref(_any: &PyAny) {} #[pyfunction] +#[pyo3(signature = (_any))] fn pyfunction_option_gil_ref(_any: Option<&PyAny>) {} +#[pyfunction] +#[pyo3(signature = (_i, _any=None))] +fn pyfunction_option_1(_i: u32, _any: Option) {} + +#[pyfunction] +fn pyfunction_option_2(_i: u32, _any: Option) {} + +#[pyfunction] +fn pyfunction_option_3(_i: u32, _any: Option, _foo: Option) {} + +#[pyfunction] +fn pyfunction_option_4( + _i: u32, + #[pyo3(from_py_with = "extract_options")] _any: Option, + _foo: Option, +) { +} + #[derive(Debug, FromPyObject)] pub struct Zap { #[pyo3(item)] diff --git a/tests/ui/deprecations.stderr b/tests/ui/deprecations.stderr index dc21b595743..b11c0058ce2 100644 --- a/tests/ui/deprecations.stderr +++ b/tests/ui/deprecations.stderr @@ -10,10 +10,34 @@ note: the lint level is defined here 1 | #![deny(deprecated)] | ^^^^^^^^^^ +error: use of deprecated constant `MyClass::__pymethod_set_set_option__::SIGNATURE`: This function has implicit defaults for the trailing `Option` arguments. These implicit defaults are being phased out. Add `#[pyo3(signature = (_value=None)]` to this function to silence this warning and keep the current behavior + --> tests/ui/deprecations.rs:43:8 + | +43 | fn set_option(&self, _value: Option) {} + | ^^^^^^^^^^ + +error: use of deprecated constant `__pyfunction_pyfunction_option_2::SIGNATURE`: This function has implicit defaults for the trailing `Option` arguments. These implicit defaults are being phased out. Add `#[pyo3(signature = (_i, _any=None)]` to this function to silence this warning and keep the current behavior + --> tests/ui/deprecations.rs:132:4 + | +132 | fn pyfunction_option_2(_i: u32, _any: Option) {} + | ^^^^^^^^^^^^^^^^^^^ + +error: use of deprecated constant `__pyfunction_pyfunction_option_3::SIGNATURE`: This function has implicit defaults for the trailing `Option` arguments. These implicit defaults are being phased out. Add `#[pyo3(signature = (_i, _any=None, _foo=None)]` to this function to silence this warning and keep the current behavior + --> tests/ui/deprecations.rs:135:4 + | +135 | fn pyfunction_option_3(_i: u32, _any: Option, _foo: Option) {} + | ^^^^^^^^^^^^^^^^^^^ + +error: use of deprecated constant `__pyfunction_pyfunction_option_4::SIGNATURE`: This function has implicit defaults for the trailing `Option` arguments. These implicit defaults are being phased out. Add `#[pyo3(signature = (_i, _any=None, _foo=None)]` to this function to silence this warning and keep the current behavior + --> tests/ui/deprecations.rs:138:4 + | +138 | fn pyfunction_option_4( + | ^^^^^^^^^^^^^^^^^^^ + error: use of deprecated method `pyo3::deprecations::GilRefs::::from_py_with_arg`: use `&Bound<'_, PyAny>` as the argument for this `from_py_with` extractor - --> tests/ui/deprecations.rs:42:44 + --> tests/ui/deprecations.rs:45:44 | -42 | fn __eq__(&self, #[pyo3(from_py_with = "extract_gil_ref")] _other: i32) -> bool { +45 | fn __eq__(&self, #[pyo3(from_py_with = "extract_gil_ref")] _other: i32) -> bool { | ^^^^^^^^^^^^^^^^^ error: use of deprecated method `pyo3::deprecations::GilRefs::::function_arg`: use `&Bound<'_, T>` instead for this function argument @@ -47,69 +71,69 @@ error: use of deprecated method `pyo3::deprecations::GilRefs::::function_arg` | ^ error: use of deprecated method `pyo3::deprecations::GilRefs::::function_arg`: use `&Bound<'_, T>` instead for this function argument - --> tests/ui/deprecations.rs:61:44 + --> tests/ui/deprecations.rs:64:44 | -61 | fn pyfunction_with_module_gil_ref(_module: &PyModule) -> PyResult<&str> { +64 | fn pyfunction_with_module_gil_ref(_module: &PyModule) -> PyResult<&str> { | ^ error: use of deprecated method `pyo3::deprecations::GilRefs::::function_arg`: use `&Bound<'_, T>` instead for this function argument - --> tests/ui/deprecations.rs:71:19 + --> tests/ui/deprecations.rs:74:19 | -71 | fn module_gil_ref(_m: &PyModule) -> PyResult<()> { +74 | fn module_gil_ref(_m: &PyModule) -> PyResult<()> { | ^^ error: use of deprecated method `pyo3::deprecations::GilRefs::::function_arg`: use `&Bound<'_, T>` instead for this function argument - --> tests/ui/deprecations.rs:76:57 + --> tests/ui/deprecations.rs:79:57 | -76 | fn module_gil_ref_with_explicit_py_arg(_py: Python<'_>, _m: &PyModule) -> PyResult<()> { +79 | fn module_gil_ref_with_explicit_py_arg(_py: Python<'_>, _m: &PyModule) -> PyResult<()> { | ^^ error: use of deprecated method `pyo3::deprecations::GilRefs::::from_py_with_arg`: use `&Bound<'_, PyAny>` as the argument for this `from_py_with` extractor - --> tests/ui/deprecations.rs:108:27 + --> tests/ui/deprecations.rs:115:27 | -108 | #[pyo3(from_py_with = "extract_gil_ref")] _gil_ref: i32, +115 | #[pyo3(from_py_with = "extract_gil_ref")] _gil_ref: i32, | ^^^^^^^^^^^^^^^^^ error: use of deprecated method `pyo3::deprecations::GilRefs::::function_arg`: use `&Bound<'_, T>` instead for this function argument - --> tests/ui/deprecations.rs:114:29 + --> tests/ui/deprecations.rs:121:29 | -114 | fn pyfunction_gil_ref(_any: &PyAny) {} +121 | fn pyfunction_gil_ref(_any: &PyAny) {} | ^ error: use of deprecated method `pyo3::deprecations::OptionGilRefs::>::function_arg`: use `Option<&Bound<'_, T>>` instead for this function argument - --> tests/ui/deprecations.rs:117:36 + --> tests/ui/deprecations.rs:125:36 | -117 | fn pyfunction_option_gil_ref(_any: Option<&PyAny>) {} +125 | fn pyfunction_option_gil_ref(_any: Option<&PyAny>) {} | ^^^^^^ error: use of deprecated method `pyo3::deprecations::GilRefs::::from_py_with_arg`: use `&Bound<'_, PyAny>` as the argument for this `from_py_with` extractor - --> tests/ui/deprecations.rs:124:27 + --> tests/ui/deprecations.rs:150:27 | -124 | #[pyo3(from_py_with = "PyAny::len", item("my_object"))] +150 | #[pyo3(from_py_with = "PyAny::len", item("my_object"))] | ^^^^^^^^^^^^ error: use of deprecated method `pyo3::deprecations::GilRefs::::from_py_with_arg`: use `&Bound<'_, PyAny>` as the argument for this `from_py_with` extractor - --> tests/ui/deprecations.rs:134:27 + --> tests/ui/deprecations.rs:160:27 | -134 | #[pyo3(from_py_with = "PyAny::len")] usize, +160 | #[pyo3(from_py_with = "PyAny::len")] usize, | ^^^^^^^^^^^^ error: use of deprecated method `pyo3::deprecations::GilRefs::::from_py_with_arg`: use `&Bound<'_, PyAny>` as the argument for this `from_py_with` extractor - --> tests/ui/deprecations.rs:140:31 + --> tests/ui/deprecations.rs:166:31 | -140 | Zip(#[pyo3(from_py_with = "extract_gil_ref")] i32), +166 | Zip(#[pyo3(from_py_with = "extract_gil_ref")] i32), | ^^^^^^^^^^^^^^^^^ error: use of deprecated method `pyo3::deprecations::GilRefs::::from_py_with_arg`: use `&Bound<'_, PyAny>` as the argument for this `from_py_with` extractor - --> tests/ui/deprecations.rs:147:27 + --> tests/ui/deprecations.rs:173:27 | -147 | #[pyo3(from_py_with = "extract_gil_ref")] +173 | #[pyo3(from_py_with = "extract_gil_ref")] | ^^^^^^^^^^^^^^^^^ error: use of deprecated method `pyo3::deprecations::GilRefs::>::is_python`: use `wrap_pyfunction_bound!` instead - --> tests/ui/deprecations.rs:160:13 + --> tests/ui/deprecations.rs:186:13 | -160 | let _ = wrap_pyfunction!(double, py); +186 | let _ = wrap_pyfunction!(double, py); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | = note: this error originates in the macro `wrap_pyfunction` (in Nightly builds, run with -Z macro-backtrace for more info) From 1e8e09dce3fccadaf19295c2db98004a49cb0f32 Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Fri, 10 May 2024 19:03:57 +0200 Subject: [PATCH 069/495] feature gate `as/into_gil_ref` APIs (Part 3) (#4172) --- guide/src/migration.md | 2 +- guide/src/types.md | 2 + src/conversion.rs | 8 +- src/conversions/std/slice.rs | 4 +- src/conversions/std/string.rs | 4 +- src/impl_/frompyobject.rs | 3 + src/impl_/pyfunction.rs | 13 +- src/impl_/pymethods.rs | 11 +- src/instance.rs | 2 + src/pycell.rs | 214 ++++++++++++------------ src/tests/hygiene/misc.rs | 5 +- src/tests/hygiene/pymodule.rs | 12 +- src/types/string.rs | 64 +++---- tests/test_wrap_pyfunction_deduction.rs | 4 + 14 files changed, 196 insertions(+), 152 deletions(-) diff --git a/guide/src/migration.md b/guide/src/migration.md index 875407317bf..7e0420de22d 100644 --- a/guide/src/migration.md +++ b/guide/src/migration.md @@ -1649,7 +1649,7 @@ However, for `#[pyproto]` and some functions, you need to manually fix the code. In 0.8 object creation was done with `PyRef::new` and `PyRefMut::new`. In 0.9 these have both been removed. To upgrade code, please use -[`PyCell::new`]({{#PYO3_DOCS_URL}}/pyo3/pycell/struct.PyCell.html#method.new) instead. +`PyCell::new` instead. If you need [`PyRef`] or [`PyRefMut`], just call `.borrow()` or `.borrow_mut()` on the newly-created `PyCell`. diff --git a/guide/src/types.md b/guide/src/types.md index 20e5e76a330..2a13c241de1 100644 --- a/guide/src/types.md +++ b/guide/src/types.md @@ -446,8 +446,10 @@ Like PyO3's Python native types, the GIL Ref `&PyCell` implements `Deref` was used to access `&T` and `&mut T` via `PyRef` and `PyRefMut` respectively. ```rust +#![allow(unused_imports)] # use pyo3::prelude::*; # #[pyclass] struct MyClass { } +# #[cfg(feature = "gil-refs")] # Python::with_gil(|py| -> PyResult<()> { #[allow(deprecated)] // &PyCell is part of the deprecated GIL Refs API let cell: &PyCell = PyCell::new(py, MyClass {})?; diff --git a/src/conversion.rs b/src/conversion.rs index 6e116af7303..4df19730b43 100644 --- a/src/conversion.rs +++ b/src/conversion.rs @@ -222,9 +222,7 @@ pub trait FromPyObject<'py>: Sized { /// /// Implementors are encouraged to implement this method and leave `extract` defaulted, as /// this will be most compatible with PyO3's future API. - fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult { - Self::extract(ob.clone().into_gil_ref()) - } + fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult; /// Extracts the type hint information for this type when it appears as an argument. /// @@ -350,8 +348,8 @@ impl<'py, T> FromPyObject<'py> for &'py crate::PyCell where T: PyClass, { - fn extract(obj: &'py PyAny) -> PyResult { - obj.downcast().map_err(Into::into) + fn extract_bound(obj: &Bound<'py, PyAny>) -> PyResult { + obj.clone().into_gil_ref().downcast().map_err(Into::into) } } diff --git a/src/conversions/std/slice.rs b/src/conversions/std/slice.rs index b3932302ef3..9c9cde06fc7 100644 --- a/src/conversions/std/slice.rs +++ b/src/conversions/std/slice.rs @@ -20,8 +20,8 @@ impl<'a> IntoPy for &'a [u8] { #[cfg(feature = "gil-refs")] impl<'py> crate::FromPyObject<'py> for &'py [u8] { - fn extract(obj: &'py PyAny) -> PyResult { - Ok(obj.downcast::()?.as_bytes()) + fn extract_bound(obj: &crate::Bound<'py, PyAny>) -> PyResult { + Ok(obj.clone().into_gil_ref().downcast::()?.as_bytes()) } #[cfg(feature = "experimental-inspect")] diff --git a/src/conversions/std/string.rs b/src/conversions/std/string.rs index 9c276d1d3d9..5bc05c1a091 100644 --- a/src/conversions/std/string.rs +++ b/src/conversions/std/string.rs @@ -116,8 +116,8 @@ impl<'a> IntoPy for &'a String { /// Accepts Python `str` objects. #[cfg(feature = "gil-refs")] impl<'py> FromPyObject<'py> for &'py str { - fn extract(ob: &'py PyAny) -> PyResult { - ob.downcast::()?.to_str() + fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult { + ob.clone().into_gil_ref().downcast::()?.to_str() } #[cfg(feature = "experimental-inspect")] diff --git a/src/impl_/frompyobject.rs b/src/impl_/frompyobject.rs index e38ff3c76b2..1e46efaeae3 100644 --- a/src/impl_/frompyobject.rs +++ b/src/impl_/frompyobject.rs @@ -4,6 +4,7 @@ use crate::{exceptions::PyTypeError, FromPyObject, PyAny, PyErr, PyResult, Pytho pub enum Extractor<'a, 'py, T> { Bound(fn(&'a Bound<'py, PyAny>) -> PyResult), + #[cfg(feature = "gil-refs")] GilRef(fn(&'a PyAny) -> PyResult), } @@ -13,6 +14,7 @@ impl<'a, 'py, T> From) -> PyResult> for Extractor<'a } } +#[cfg(feature = "gil-refs")] impl<'a, T> From PyResult> for Extractor<'a, '_, T> { fn from(value: fn(&'a PyAny) -> PyResult) -> Self { Self::GilRef(value) @@ -23,6 +25,7 @@ impl<'a, 'py, T> Extractor<'a, 'py, T> { pub(crate) fn call(self, obj: &'a Bound<'py, PyAny>) -> PyResult { match self { Extractor::Bound(f) => f(obj), + #[cfg(feature = "gil-refs")] Extractor::GilRef(f) => f(obj.as_gil_ref()), } } diff --git a/src/impl_/pyfunction.rs b/src/impl_/pyfunction.rs index cb838fea6c2..0be5174487f 100644 --- a/src/impl_/pyfunction.rs +++ b/src/impl_/pyfunction.rs @@ -1,6 +1,6 @@ use crate::{ types::{PyCFunction, PyModule}, - Borrowed, Bound, PyNativeType, PyResult, Python, + Borrowed, Bound, PyResult, Python, }; pub use crate::impl_::pymethods::PyMethodDef; @@ -37,14 +37,24 @@ impl<'py> WrapPyFunctionArg<'py, Bound<'py, PyCFunction>> for &'_ Borrowed<'_, ' // For Python<'py>, only the GIL Ref form exists to avoid causing type inference to kick in. // The `wrap_pyfunction_bound!` macro is needed for the Bound form. +#[cfg(feature = "gil-refs")] impl<'py> WrapPyFunctionArg<'py, &'py PyCFunction> for Python<'py> { fn wrap_pyfunction(self, method_def: &PyMethodDef) -> PyResult<&'py PyCFunction> { PyCFunction::internal_new(self, method_def, None).map(Bound::into_gil_ref) } } +#[cfg(not(feature = "gil-refs"))] +impl<'py> WrapPyFunctionArg<'py, Bound<'py, PyCFunction>> for Python<'py> { + fn wrap_pyfunction(self, method_def: &PyMethodDef) -> PyResult> { + PyCFunction::internal_new(self, method_def, None) + } +} + +#[cfg(feature = "gil-refs")] impl<'py> WrapPyFunctionArg<'py, &'py PyCFunction> for &'py PyModule { fn wrap_pyfunction(self, method_def: &PyMethodDef) -> PyResult<&'py PyCFunction> { + use crate::PyNativeType; PyCFunction::internal_new(self.py(), method_def, Some(&self.as_borrowed())) .map(Bound::into_gil_ref) } @@ -62,6 +72,7 @@ where } } +#[cfg(feature = "gil-refs")] impl<'py> WrapPyFunctionArg<'py, Bound<'py, PyCFunction>> for OnlyBound> { fn wrap_pyfunction(self, method_def: &PyMethodDef) -> PyResult> { PyCFunction::internal_new(self.0, method_def, None) diff --git a/src/impl_/pymethods.rs b/src/impl_/pymethods.rs index 2e9dd0ac520..44b2af25650 100644 --- a/src/impl_/pymethods.rs +++ b/src/impl_/pymethods.rs @@ -5,7 +5,9 @@ use crate::impl_::panic::PanicTrap; use crate::internal_tricks::extract_c_string; use crate::pycell::{PyBorrowError, PyBorrowMutError}; use crate::pyclass::boolean_struct::False; -use crate::types::{any::PyAnyMethods, PyModule, PyType}; +use crate::types::any::PyAnyMethods; +#[cfg(feature = "gil-refs")] +use crate::types::{PyModule, PyType}; use crate::{ ffi, Borrowed, Bound, DowncastError, Py, PyAny, PyClass, PyClassInitializer, PyErr, PyObject, PyRef, PyRefMut, PyResult, PyTraverseError, PyTypeCheck, PyVisit, Python, @@ -492,6 +494,7 @@ impl<'a, 'py> BoundRef<'a, 'py, PyAny> { // GIL Ref implementations for &'a T ran into trouble with orphan rules, // so explicit implementations are used instead for the two relevant types. +#[cfg(feature = "gil-refs")] impl<'a> From> for &'a PyType { #[inline] fn from(bound: BoundRef<'a, 'a, PyType>) -> Self { @@ -499,6 +502,7 @@ impl<'a> From> for &'a PyType { } } +#[cfg(feature = "gil-refs")] impl<'a> From> for &'a PyModule { #[inline] fn from(bound: BoundRef<'a, 'a, PyModule>) -> Self { @@ -507,6 +511,7 @@ impl<'a> From> for &'a PyModule { } #[allow(deprecated)] +#[cfg(feature = "gil-refs")] impl<'a, 'py, T: PyClass> From> for &'a crate::PyCell { #[inline] fn from(bound: BoundRef<'a, 'py, T>) -> Self { @@ -518,7 +523,7 @@ impl<'a, 'py, T: PyClass> TryFrom> for PyRef<'py, T> { type Error = PyBorrowError; #[inline] fn try_from(value: BoundRef<'a, 'py, T>) -> Result { - value.0.clone().into_gil_ref().try_into() + value.0.try_borrow() } } @@ -526,7 +531,7 @@ impl<'a, 'py, T: PyClass> TryFrom> for PyRe type Error = PyBorrowMutError; #[inline] fn try_from(value: BoundRef<'a, 'py, T>) -> Result { - value.0.clone().into_gil_ref().try_into() + value.0.try_borrow_mut() } } diff --git a/src/instance.rs b/src/instance.rs index e160de3a314..1c510b90be5 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -492,6 +492,7 @@ impl<'py, T> Bound<'py, T> { /// /// This is a helper to be used for migration from the deprecated "GIL Refs" API. #[inline] + #[cfg(feature = "gil-refs")] pub fn as_gil_ref(&'py self) -> &'py T::AsRefTarget where T: HasPyGilRef, @@ -507,6 +508,7 @@ impl<'py, T> Bound<'py, T> { /// /// This is a helper to be used for migration from the deprecated "GIL Refs" API. #[inline] + #[cfg(feature = "gil-refs")] pub fn into_gil_ref(self) -> &'py T::AsRefTarget where T: HasPyGilRef, diff --git a/src/pycell.rs b/src/pycell.rs index e0088d9b523..215ed7bba66 100644 --- a/src/pycell.rs +++ b/src/pycell.rs @@ -201,11 +201,12 @@ use crate::pyclass::{ boolean_struct::{False, True}, PyClass, }; -use crate::pyclass_init::PyClassInitializer; use crate::type_object::{PyLayout, PySizedLayout}; use crate::types::any::PyAnyMethods; use crate::types::PyAny; -use crate::{ffi, Bound, IntoPy, PyErr, PyNativeType, PyObject, PyResult, PyTypeCheck, Python}; +use crate::{ffi, Bound, IntoPy, PyErr, PyNativeType, PyObject, PyTypeCheck, Python}; +#[cfg(feature = "gil-refs")] +use crate::{pyclass_init::PyClassInitializer, PyResult}; use std::fmt; use std::mem::ManuallyDrop; use std::ops::{Deref, DerefMut}; @@ -222,7 +223,7 @@ use self::impl_::{PyClassObject, PyClassObjectLayout}; /// # Examples /// /// This example demonstrates getting a mutable reference of the contained `PyClass`. -/// ```rust +/// ```rust,ignore /// use pyo3::prelude::*; /// /// #[pyclass] @@ -272,12 +273,10 @@ impl PyCell { /// /// In cases where the value in the cell does not need to be accessed immediately after /// creation, consider [`Py::new`](crate::Py::new) as a more efficient alternative. - #[cfg_attr( - not(feature = "gil-refs"), - deprecated( - since = "0.21.0", - note = "use `Bound::new(py, value)` or `Py::new(py, value)` instead of `PyCell::new(py, value)`" - ) + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "use `Bound::new(py, value)` or `Py::new(py, value)` instead of `PyCell::new(py, value)`" )] pub fn new(py: Python<'_>, value: impl Into>) -> PyResult<&Self> { Bound::new(py, value).map(Bound::into_gil_ref) @@ -317,7 +316,7 @@ impl PyCell { /// /// # Examples /// - /// ``` + /// ```ignore /// # use pyo3::prelude::*; /// #[pyclass] /// struct Class {} @@ -347,7 +346,7 @@ impl PyCell { /// /// # Examples /// - /// ``` + /// ```ignore /// # use pyo3::prelude::*; /// #[pyclass] /// struct Class {} @@ -380,7 +379,7 @@ impl PyCell { /// /// # Examples /// - /// ``` + /// ```ignore /// # use pyo3::prelude::*; /// #[pyclass] /// struct Class {} @@ -417,7 +416,7 @@ impl PyCell { /// /// # Examples /// - /// ``` + /// ```ignore /// use std::sync::atomic::{AtomicUsize, Ordering}; /// # use pyo3::prelude::*; /// @@ -561,14 +560,14 @@ impl fmt::Debug for PyCell { } } -/// A wrapper type for an immutably borrowed value from a [`PyCell`]``. +/// A wrapper type for an immutably borrowed value from a [`Bound<'py, T>`]. /// -/// See the [`PyCell`] documentation for more information. +/// See the [`Bound`] documentation for more information. /// /// # Examples /// -/// You can use `PyRef` as an alternative to a `&self` receiver when -/// - you need to access the pointer of the `PyCell`, or +/// You can use [`PyRef`] as an alternative to a `&self` receiver when +/// - you need to access the pointer of the [`Bound`], or /// - you want to get a super class. /// ``` /// # use pyo3::prelude::*; @@ -599,7 +598,7 @@ impl fmt::Debug for PyCell { /// } /// # Python::with_gil(|py| { /// # let sub = Py::new(py, Child::new()).unwrap(); -/// # pyo3::py_run!(py, sub, "assert sub.format() == 'Caterpillar(base: Butterfly, cnt: 5)', sub.format()"); +/// # pyo3::py_run!(py, sub, "assert sub.format() == 'Caterpillar(base: Butterfly, cnt: 4)', sub.format()"); /// # }); /// ``` /// @@ -1004,101 +1003,106 @@ mod tests { #[derive(Copy, Clone, PartialEq, Eq, Debug)] struct SomeClass(i32); - #[test] - fn pycell_replace() { - Python::with_gil(|py| { - #[allow(deprecated)] - let cell = PyCell::new(py, SomeClass(0)).unwrap(); - assert_eq!(*cell.borrow(), SomeClass(0)); - - let previous = cell.replace(SomeClass(123)); - assert_eq!(previous, SomeClass(0)); - assert_eq!(*cell.borrow(), SomeClass(123)); - }) - } - - #[test] - #[should_panic(expected = "Already borrowed: PyBorrowMutError")] - fn pycell_replace_panic() { - Python::with_gil(|py| { - #[allow(deprecated)] - let cell = PyCell::new(py, SomeClass(0)).unwrap(); - let _guard = cell.borrow(); - - cell.replace(SomeClass(123)); - }) - } + #[cfg(feature = "gil-refs")] + mod deprecated { + use super::*; + + #[test] + fn pycell_replace() { + Python::with_gil(|py| { + #[allow(deprecated)] + let cell = PyCell::new(py, SomeClass(0)).unwrap(); + assert_eq!(*cell.borrow(), SomeClass(0)); + + let previous = cell.replace(SomeClass(123)); + assert_eq!(previous, SomeClass(0)); + assert_eq!(*cell.borrow(), SomeClass(123)); + }) + } - #[test] - fn pycell_replace_with() { - Python::with_gil(|py| { - #[allow(deprecated)] - let cell = PyCell::new(py, SomeClass(0)).unwrap(); - assert_eq!(*cell.borrow(), SomeClass(0)); - - let previous = cell.replace_with(|value| { - *value = SomeClass(2); - SomeClass(123) - }); - assert_eq!(previous, SomeClass(2)); - assert_eq!(*cell.borrow(), SomeClass(123)); - }) - } + #[test] + #[should_panic(expected = "Already borrowed: PyBorrowMutError")] + fn pycell_replace_panic() { + Python::with_gil(|py| { + #[allow(deprecated)] + let cell = PyCell::new(py, SomeClass(0)).unwrap(); + let _guard = cell.borrow(); - #[test] - #[should_panic(expected = "Already borrowed: PyBorrowMutError")] - fn pycell_replace_with_panic() { - Python::with_gil(|py| { - #[allow(deprecated)] - let cell = PyCell::new(py, SomeClass(0)).unwrap(); - let _guard = cell.borrow(); + cell.replace(SomeClass(123)); + }) + } - cell.replace_with(|_| SomeClass(123)); - }) - } + #[test] + fn pycell_replace_with() { + Python::with_gil(|py| { + #[allow(deprecated)] + let cell = PyCell::new(py, SomeClass(0)).unwrap(); + assert_eq!(*cell.borrow(), SomeClass(0)); + + let previous = cell.replace_with(|value| { + *value = SomeClass(2); + SomeClass(123) + }); + assert_eq!(previous, SomeClass(2)); + assert_eq!(*cell.borrow(), SomeClass(123)); + }) + } - #[test] - fn pycell_swap() { - Python::with_gil(|py| { - #[allow(deprecated)] - let cell = PyCell::new(py, SomeClass(0)).unwrap(); - #[allow(deprecated)] - let cell2 = PyCell::new(py, SomeClass(123)).unwrap(); - assert_eq!(*cell.borrow(), SomeClass(0)); - assert_eq!(*cell2.borrow(), SomeClass(123)); - - cell.swap(cell2); - assert_eq!(*cell.borrow(), SomeClass(123)); - assert_eq!(*cell2.borrow(), SomeClass(0)); - }) - } + #[test] + #[should_panic(expected = "Already borrowed: PyBorrowMutError")] + fn pycell_replace_with_panic() { + Python::with_gil(|py| { + #[allow(deprecated)] + let cell = PyCell::new(py, SomeClass(0)).unwrap(); + let _guard = cell.borrow(); - #[test] - #[should_panic(expected = "Already borrowed: PyBorrowMutError")] - fn pycell_swap_panic() { - Python::with_gil(|py| { - #[allow(deprecated)] - let cell = PyCell::new(py, SomeClass(0)).unwrap(); - #[allow(deprecated)] - let cell2 = PyCell::new(py, SomeClass(123)).unwrap(); + cell.replace_with(|_| SomeClass(123)); + }) + } - let _guard = cell.borrow(); - cell.swap(cell2); - }) - } + #[test] + fn pycell_swap() { + Python::with_gil(|py| { + #[allow(deprecated)] + let cell = PyCell::new(py, SomeClass(0)).unwrap(); + #[allow(deprecated)] + let cell2 = PyCell::new(py, SomeClass(123)).unwrap(); + assert_eq!(*cell.borrow(), SomeClass(0)); + assert_eq!(*cell2.borrow(), SomeClass(123)); + + cell.swap(cell2); + assert_eq!(*cell.borrow(), SomeClass(123)); + assert_eq!(*cell2.borrow(), SomeClass(0)); + }) + } - #[test] - #[should_panic(expected = "Already borrowed: PyBorrowMutError")] - fn pycell_swap_panic_other_borrowed() { - Python::with_gil(|py| { - #[allow(deprecated)] - let cell = PyCell::new(py, SomeClass(0)).unwrap(); - #[allow(deprecated)] - let cell2 = PyCell::new(py, SomeClass(123)).unwrap(); + #[test] + #[should_panic(expected = "Already borrowed: PyBorrowMutError")] + fn pycell_swap_panic() { + Python::with_gil(|py| { + #[allow(deprecated)] + let cell = PyCell::new(py, SomeClass(0)).unwrap(); + #[allow(deprecated)] + let cell2 = PyCell::new(py, SomeClass(123)).unwrap(); + + let _guard = cell.borrow(); + cell.swap(cell2); + }) + } - let _guard = cell2.borrow(); - cell.swap(cell2); - }) + #[test] + #[should_panic(expected = "Already borrowed: PyBorrowMutError")] + fn pycell_swap_panic_other_borrowed() { + Python::with_gil(|py| { + #[allow(deprecated)] + let cell = PyCell::new(py, SomeClass(0)).unwrap(); + #[allow(deprecated)] + let cell2 = PyCell::new(py, SomeClass(123)).unwrap(); + + let _guard = cell2.borrow(); + cell.swap(cell2); + }) + } } #[test] diff --git a/src/tests/hygiene/misc.rs b/src/tests/hygiene/misc.rs index 24dad7ec196..7a2f58818a1 100644 --- a/src/tests/hygiene/misc.rs +++ b/src/tests/hygiene/misc.rs @@ -41,7 +41,10 @@ fn append_to_inittab() { #[crate::pymodule] #[pyo3(crate = "crate")] #[allow(clippy::unnecessary_wraps)] - fn module_for_inittab(_: crate::Python<'_>, _: &crate::types::PyModule) -> crate::PyResult<()> { + fn module_for_inittab( + _: crate::Python<'_>, + _: &crate::Bound<'_, crate::types::PyModule>, + ) -> crate::PyResult<()> { ::std::result::Result::Ok(()) } crate::append_to_inittab!(module_for_inittab); diff --git a/src/tests/hygiene/pymodule.rs b/src/tests/hygiene/pymodule.rs index 32b3632be22..91f9808bcc4 100644 --- a/src/tests/hygiene/pymodule.rs +++ b/src/tests/hygiene/pymodule.rs @@ -7,6 +7,7 @@ fn do_something(x: i32) -> crate::PyResult { ::std::result::Result::Ok(x) } +#[cfg(feature = "gil-refs")] #[allow(deprecated)] #[crate::pymodule] #[pyo3(crate = "crate")] @@ -14,6 +15,15 @@ fn foo(_py: crate::Python<'_>, _m: &crate::types::PyModule) -> crate::PyResult<( ::std::result::Result::Ok(()) } +#[crate::pymodule] +#[pyo3(crate = "crate")] +fn foo_bound( + _py: crate::Python<'_>, + _m: &crate::Bound<'_, crate::types::PyModule>, +) -> crate::PyResult<()> { + ::std::result::Result::Ok(()) +} + #[cfg(feature = "gil-refs")] #[allow(deprecated)] #[crate::pymodule] @@ -34,7 +44,7 @@ fn my_module_bound(m: &crate::Bound<'_, crate::types::PyModule>) -> crate::PyRes )?; as crate::types::PyModuleMethods>::add_wrapped( m, - crate::wrap_pymodule!(foo), + crate::wrap_pymodule!(foo_bound), )?; ::std::result::Result::Ok(()) diff --git a/src/types/string.rs b/src/types/string.rs index 09c5903547c..4f0025acfe8 100644 --- a/src/types/string.rs +++ b/src/types/string.rs @@ -6,7 +6,9 @@ use crate::py_result_ext::PyResultExt; use crate::types::any::PyAnyMethods; use crate::types::bytes::PyBytesMethods; use crate::types::PyBytes; -use crate::{ffi, Bound, IntoPy, Py, PyAny, PyNativeType, PyResult, Python}; +#[cfg(feature = "gil-refs")] +use crate::PyNativeType; +use crate::{ffi, Bound, IntoPy, Py, PyAny, PyResult, Python}; use std::borrow::Cow; use std::os::raw::c_char; use std::str; @@ -135,16 +137,6 @@ pub struct PyString(PyAny); pyobject_native_type_core!(PyString, pyobject_native_static_type_object!(ffi::PyUnicode_Type), #checkfunction=ffi::PyUnicode_Check); impl PyString { - /// Deprecated form of [`PyString::new_bound`]. - #[cfg(feature = "gil-refs")] - #[deprecated( - since = "0.21.0", - note = "`PyString::new` will be replaced by `PyString::new_bound` in a future PyO3 version" - )] - pub fn new<'py>(py: Python<'py>, s: &str) -> &'py Self { - Self::new_bound(py, s).into_gil_ref() - } - /// Creates a new Python string object. /// /// Panics if out of memory. @@ -158,16 +150,6 @@ impl PyString { } } - /// Deprecated form of [`PyString::intern_bound`]. - #[cfg(feature = "gil-refs")] - #[deprecated( - since = "0.21.0", - note = "`PyString::intern` will be replaced by `PyString::intern_bound` in a future PyO3 version" - )] - pub fn intern<'py>(py: Python<'py>, s: &str) -> &'py Self { - Self::intern_bound(py, s).into_gil_ref() - } - /// Intern the given string /// /// This will return a reference to the same Python string object if called repeatedly with the same string. @@ -188,16 +170,6 @@ impl PyString { } } - /// Deprecated form of [`PyString::from_object_bound`]. - #[cfg(feature = "gil-refs")] - #[deprecated( - since = "0.21.0", - note = "`PyString::from_object` will be replaced by `PyString::from_object_bound` in a future PyO3 version" - )] - pub fn from_object<'py>(src: &'py PyAny, encoding: &str, errors: &str) -> PyResult<&'py Self> { - Self::from_object_bound(&src.as_borrowed(), encoding, errors).map(Bound::into_gil_ref) - } - /// Attempts to create a Python string from a Python [bytes-like object]. /// /// [bytes-like object]: (https://docs.python.org/3/glossary.html#term-bytes-like-object). @@ -216,6 +188,36 @@ impl PyString { .downcast_into_unchecked() } } +} + +#[cfg(feature = "gil-refs")] +impl PyString { + /// Deprecated form of [`PyString::new_bound`]. + #[deprecated( + since = "0.21.0", + note = "`PyString::new` will be replaced by `PyString::new_bound` in a future PyO3 version" + )] + pub fn new<'py>(py: Python<'py>, s: &str) -> &'py Self { + Self::new_bound(py, s).into_gil_ref() + } + + /// Deprecated form of [`PyString::intern_bound`]. + #[deprecated( + since = "0.21.0", + note = "`PyString::intern` will be replaced by `PyString::intern_bound` in a future PyO3 version" + )] + pub fn intern<'py>(py: Python<'py>, s: &str) -> &'py Self { + Self::intern_bound(py, s).into_gil_ref() + } + + /// Deprecated form of [`PyString::from_object_bound`]. + #[deprecated( + since = "0.21.0", + note = "`PyString::from_object` will be replaced by `PyString::from_object_bound` in a future PyO3 version" + )] + pub fn from_object<'py>(src: &'py PyAny, encoding: &str, errors: &str) -> PyResult<&'py Self> { + Self::from_object_bound(&src.as_borrowed(), encoding, errors).map(Bound::into_gil_ref) + } /// Gets the Python string as a Rust UTF-8 string slice. /// diff --git a/tests/test_wrap_pyfunction_deduction.rs b/tests/test_wrap_pyfunction_deduction.rs index 845cf2a39d7..e205003113e 100644 --- a/tests/test_wrap_pyfunction_deduction.rs +++ b/tests/test_wrap_pyfunction_deduction.rs @@ -5,6 +5,7 @@ use pyo3::{prelude::*, types::PyCFunction}; #[pyfunction] fn f() {} +#[cfg(feature = "gil-refs")] pub fn add_wrapped(wrapper: &impl Fn(Python<'_>) -> PyResult<&PyCFunction>) { let _ = wrapper; } @@ -12,7 +13,10 @@ pub fn add_wrapped(wrapper: &impl Fn(Python<'_>) -> PyResult<&PyCFunction>) { #[test] fn wrap_pyfunction_deduction() { #[allow(deprecated)] + #[cfg(feature = "gil-refs")] add_wrapped(wrap_pyfunction!(f)); + #[cfg(not(feature = "gil-refs"))] + add_wrapped_bound(wrap_pyfunction!(f)); } pub fn add_wrapped_bound(wrapper: &impl Fn(Python<'_>) -> PyResult>) { From 444be3bafaec65c8293e76b6f9086d85ea3e793c Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Fri, 10 May 2024 20:28:30 +0200 Subject: [PATCH 070/495] feature gate deprecated APIs for `Python` (#4173) --- guide/src/memory.md | 3 +- src/conversion.rs | 89 +++++++++++++++-------------------- src/gil.rs | 15 ++++-- src/lib.rs | 1 + src/marker.rs | 112 ++++++++++++++------------------------------ src/prelude.rs | 1 + src/pycell.rs | 2 + 7 files changed, 89 insertions(+), 134 deletions(-) diff --git a/guide/src/memory.md b/guide/src/memory.md index 67a78d3be68..b37d83563e5 100644 --- a/guide/src/memory.md +++ b/guide/src/memory.md @@ -154,8 +154,7 @@ at the end of each loop iteration, before the `with_gil()` closure ends. When doing this, you must be very careful to ensure that once the `GILPool` is dropped you do not retain access to any owned references created after the -`GILPool` was created. Read the -[documentation for `Python::new_pool()`]({{#PYO3_DOCS_URL}}/pyo3/marker/struct.Python.html#method.new_pool) +`GILPool` was created. Read the documentation for `Python::new_pool()` for more information on safety. This memory management can also be applicable when writing extension modules. diff --git a/src/conversion.rs b/src/conversion.rs index 4df19730b43..95931d30d2e 100644 --- a/src/conversion.rs +++ b/src/conversion.rs @@ -1,14 +1,21 @@ //! Defines conversions between Rust and Python types. -use crate::err::{self, PyDowncastError, PyResult}; +use crate::err::PyResult; #[cfg(feature = "experimental-inspect")] use crate::inspect::types::TypeInfo; use crate::pyclass::boolean_struct::False; use crate::types::any::PyAnyMethods; use crate::types::PyTuple; use crate::{ - ffi, gil, Borrowed, Bound, Py, PyAny, PyClass, PyNativeType, PyObject, PyRef, PyRefMut, Python, + ffi, Borrowed, Bound, Py, PyAny, PyClass, PyNativeType, PyObject, PyRef, PyRefMut, Python, +}; +#[cfg(feature = "gil-refs")] +use { + crate::{ + err::{self, PyDowncastError}, + gil, + }, + std::ptr::NonNull, }; -use std::ptr::NonNull; /// Returns a borrowed pointer to a Python object. /// @@ -385,6 +392,7 @@ where /// If `T` implements `PyTryFrom`, we can convert `&PyAny` to `&T`. /// /// This trait is similar to `std::convert::TryFrom` +#[cfg(feature = "gil-refs")] #[deprecated(since = "0.21.0")] pub trait PyTryFrom<'v>: Sized + PyNativeType { /// Cast from a concrete Python object type to PyObject. @@ -416,6 +424,7 @@ pub trait PyTryFrom<'v>: Sized + PyNativeType { /// Trait implemented by Python object types that allow a checked downcast. /// This trait is similar to `std::convert::TryInto` +#[cfg(feature = "gil-refs")] #[deprecated(since = "0.21.0")] pub trait PyTryInto: Sized { /// Cast from PyObject to a concrete Python object type. @@ -506,6 +515,7 @@ impl IntoPy> for () { /// # Safety /// /// See safety notes on individual functions. +#[cfg(feature = "gil-refs")] #[deprecated(since = "0.21.0")] pub unsafe trait FromPyPointer<'p>: Sized { /// Convert from an arbitrary `PyObject`. @@ -515,12 +525,9 @@ pub unsafe trait FromPyPointer<'p>: Sized { /// Implementations must ensure the object does not get freed during `'p` /// and ensure that `ptr` is of the correct type. /// Note that it must be safe to decrement the reference count of `ptr`. - #[cfg_attr( - not(feature = "gil-refs"), - deprecated( - since = "0.21.0", - note = "use `Py::from_owned_ptr_or_opt(py, ptr)` or `Bound::from_owned_ptr_or_opt(py, ptr)` instead" - ) + #[deprecated( + since = "0.21.0", + note = "use `Py::from_owned_ptr_or_opt(py, ptr)` or `Bound::from_owned_ptr_or_opt(py, ptr)` instead" )] unsafe fn from_owned_ptr_or_opt(py: Python<'p>, ptr: *mut ffi::PyObject) -> Option<&'p Self>; /// Convert from an arbitrary `PyObject` or panic. @@ -528,12 +535,9 @@ pub unsafe trait FromPyPointer<'p>: Sized { /// # Safety /// /// Relies on [`from_owned_ptr_or_opt`](#method.from_owned_ptr_or_opt). - #[cfg_attr( - not(feature = "gil-refs"), - deprecated( - since = "0.21.0", - note = "use `Py::from_owned_ptr(py, ptr)` or `Bound::from_owned_ptr(py, ptr)` instead" - ) + #[deprecated( + since = "0.21.0", + note = "use `Py::from_owned_ptr(py, ptr)` or `Bound::from_owned_ptr(py, ptr)` instead" )] unsafe fn from_owned_ptr_or_panic(py: Python<'p>, ptr: *mut ffi::PyObject) -> &'p Self { #[allow(deprecated)] @@ -544,12 +548,9 @@ pub unsafe trait FromPyPointer<'p>: Sized { /// # Safety /// /// Relies on [`from_owned_ptr_or_opt`](#method.from_owned_ptr_or_opt). - #[cfg_attr( - not(feature = "gil-refs"), - deprecated( - since = "0.21.0", - note = "use `Py::from_owned_ptr(py, ptr)` or `Bound::from_owned_ptr(py, ptr)` instead" - ) + #[deprecated( + since = "0.21.0", + note = "use `Py::from_owned_ptr(py, ptr)` or `Bound::from_owned_ptr(py, ptr)` instead" )] unsafe fn from_owned_ptr(py: Python<'p>, ptr: *mut ffi::PyObject) -> &'p Self { #[allow(deprecated)] @@ -560,12 +561,9 @@ pub unsafe trait FromPyPointer<'p>: Sized { /// # Safety /// /// Relies on [`from_owned_ptr_or_opt`](#method.from_owned_ptr_or_opt). - #[cfg_attr( - not(feature = "gil-refs"), - deprecated( - since = "0.21.0", - note = "use `Py::from_owned_ptr_or_err(py, ptr)` or `Bound::from_owned_ptr_or_err(py, ptr)` instead" - ) + #[deprecated( + since = "0.21.0", + note = "use `Py::from_owned_ptr_or_err(py, ptr)` or `Bound::from_owned_ptr_or_err(py, ptr)` instead" )] unsafe fn from_owned_ptr_or_err(py: Python<'p>, ptr: *mut ffi::PyObject) -> PyResult<&'p Self> { #[allow(deprecated)] @@ -576,12 +574,9 @@ pub unsafe trait FromPyPointer<'p>: Sized { /// # Safety /// /// Implementations must ensure the object does not get freed during `'p` and avoid type confusion. - #[cfg_attr( - not(feature = "gil-refs"), - deprecated( - since = "0.21.0", - note = "use `Py::from_borrowed_ptr_or_opt(py, ptr)` or `Bound::from_borrowed_ptr_or_opt(py, ptr)` instead" - ) + #[deprecated( + since = "0.21.0", + note = "use `Py::from_borrowed_ptr_or_opt(py, ptr)` or `Bound::from_borrowed_ptr_or_opt(py, ptr)` instead" )] unsafe fn from_borrowed_ptr_or_opt(py: Python<'p>, ptr: *mut ffi::PyObject) -> Option<&'p Self>; @@ -590,12 +585,9 @@ pub unsafe trait FromPyPointer<'p>: Sized { /// # Safety /// /// Relies on unsafe fn [`from_borrowed_ptr_or_opt`](#method.from_borrowed_ptr_or_opt). - #[cfg_attr( - not(feature = "gil-refs"), - deprecated( - since = "0.21.0", - note = "use `Py::from_borrowed_ptr(py, ptr)` or `Bound::from_borrowed_ptr(py, ptr)` instead" - ) + #[deprecated( + since = "0.21.0", + note = "use `Py::from_borrowed_ptr(py, ptr)` or `Bound::from_borrowed_ptr(py, ptr)` instead" )] unsafe fn from_borrowed_ptr_or_panic(py: Python<'p>, ptr: *mut ffi::PyObject) -> &'p Self { #[allow(deprecated)] @@ -606,12 +598,9 @@ pub unsafe trait FromPyPointer<'p>: Sized { /// # Safety /// /// Relies on unsafe fn [`from_borrowed_ptr_or_opt`](#method.from_borrowed_ptr_or_opt). - #[cfg_attr( - not(feature = "gil-refs"), - deprecated( - since = "0.21.0", - note = "use `Py::from_borrowed_ptr(py, ptr)` or `Bound::from_borrowed_ptr(py, ptr)` instead" - ) + #[deprecated( + since = "0.21.0", + note = "use `Py::from_borrowed_ptr(py, ptr)` or `Bound::from_borrowed_ptr(py, ptr)` instead" )] unsafe fn from_borrowed_ptr(py: Python<'p>, ptr: *mut ffi::PyObject) -> &'p Self { #[allow(deprecated)] @@ -622,12 +611,9 @@ pub unsafe trait FromPyPointer<'p>: Sized { /// # Safety /// /// Relies on unsafe fn [`from_borrowed_ptr_or_opt`](#method.from_borrowed_ptr_or_opt). - #[cfg_attr( - not(feature = "gil-refs"), - deprecated( - since = "0.21.0", - note = "use `Py::from_borrowed_ptr_or_err(py, ptr)` or `Bound::from_borrowed_ptr_or_err(py, ptr)` instead" - ) + #[deprecated( + since = "0.21.0", + note = "use `Py::from_borrowed_ptr_or_err(py, ptr)` or `Bound::from_borrowed_ptr_or_err(py, ptr)` instead" )] unsafe fn from_borrowed_ptr_or_err( py: Python<'p>, @@ -638,6 +624,7 @@ pub unsafe trait FromPyPointer<'p>: Sized { } } +#[cfg(feature = "gil-refs")] #[allow(deprecated)] unsafe impl<'p, T> FromPyPointer<'p> for T where diff --git a/src/gil.rs b/src/gil.rs index 0bcb8c086c0..29c4ffbe389 100644 --- a/src/gil.rs +++ b/src/gil.rs @@ -366,12 +366,12 @@ pub struct GILPool { impl GILPool { /// Creates a new [`GILPool`]. This function should only ever be called with the GIL held. /// - /// It is recommended not to use this API directly, but instead to use [`Python::new_pool`], as + /// It is recommended not to use this API directly, but instead to use `Python::new_pool`, as /// that guarantees the GIL is held. /// /// # Safety /// - /// As well as requiring the GIL, see the safety notes on [`Python::new_pool`]. + /// As well as requiring the GIL, see the safety notes on `Python::new_pool`. #[inline] pub unsafe fn new() -> GILPool { increment_gil_count(); @@ -462,6 +462,7 @@ pub unsafe fn register_decref(obj: NonNull) { /// /// # Safety /// The object must be an owned Python reference. +#[cfg(feature = "gil-refs")] pub unsafe fn register_owned(_py: Python<'_>, obj: NonNull) { debug_assert!(gil_is_acquired()); // Ignores the error in case this function called from `atexit`. @@ -507,9 +508,12 @@ fn decrement_gil_count() { mod tests { #[allow(deprecated)] use super::GILPool; - use super::{gil_is_acquired, GIL_COUNT, OWNED_OBJECTS, POOL}; + use super::{gil_is_acquired, GIL_COUNT, POOL}; use crate::types::any::PyAnyMethods; - use crate::{ffi, gil, PyObject, Python}; + use crate::{ffi, PyObject, Python}; + #[cfg(feature = "gil-refs")] + use {super::OWNED_OBJECTS, crate::gil}; + use std::ptr::NonNull; #[cfg(not(target_arch = "wasm32"))] use std::sync; @@ -518,6 +522,7 @@ mod tests { py.eval_bound("object()", None, None).unwrap().unbind() } + #[cfg(feature = "gil-refs")] fn owned_object_count() -> usize { #[cfg(debug_assertions)] let len = OWNED_OBJECTS.with(|owned_objects| owned_objects.borrow().len()); @@ -554,6 +559,7 @@ mod tests { } #[test] + #[cfg(feature = "gil-refs")] #[allow(deprecated)] fn test_owned() { Python::with_gil(|py| { @@ -580,6 +586,7 @@ mod tests { } #[test] + #[cfg(feature = "gil-refs")] #[allow(deprecated)] fn test_owned_nested() { Python::with_gil(|py| { diff --git a/src/lib.rs b/src/lib.rs index 3923257f5f3..2a40445222e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -317,6 +317,7 @@ //! [`Ungil`]: crate::marker::Ungil pub use crate::class::*; pub use crate::conversion::{AsPyPointer, FromPyObject, IntoPy, ToPyObject}; +#[cfg(feature = "gil-refs")] #[allow(deprecated)] pub use crate::conversion::{FromPyPointer, PyTryFrom, PyTryInto}; pub use crate::err::{ diff --git a/src/marker.rs b/src/marker.rs index ea794856d66..b6821deb036 100644 --- a/src/marker.rs +++ b/src/marker.rs @@ -130,6 +130,7 @@ use crate::version::PythonVersionInfo; use crate::PyNativeType; use crate::{ffi, Bound, IntoPy, Py, PyObject, PyTypeInfo}; #[allow(deprecated)] +#[cfg(feature = "gil-refs")] use crate::{gil::GILPool, FromPyPointer}; use std::ffi::{CStr, CString}; use std::marker::PhantomData; @@ -354,36 +355,9 @@ pub use nightly::Ungil; /// # Releasing and freeing memory /// /// The [`Python`] type can be used to create references to variables owned by the Python -/// interpreter, using functions such as `Python::eval` and `PyModule::import`. These -/// references are tied to a [`GILPool`] whose references are not cleared until it is dropped. -/// This can cause apparent "memory leaks" if it is kept around for a long time. -/// -/// ```rust -/// use pyo3::prelude::*; -/// use pyo3::types::PyString; -/// -/// # fn main () -> PyResult<()> { -/// Python::with_gil(|py| -> PyResult<()> { -/// for _ in 0..10 { -/// let hello = py.eval_bound("\"Hello World!\"", None, None)?.downcast_into::()?; -/// println!("Python says: {}", hello.to_cow()?); -/// // Normally variables in a loop scope are dropped here, but `hello` is a reference to -/// // something owned by the Python interpreter. Dropping this reference does nothing. -/// } -/// Ok(()) -/// }) -/// // This is where the `hello`'s reference counts start getting decremented. -/// # } -/// ``` -/// -/// The variable `hello` is dropped at the end of each loop iteration, but the lifetime of the -/// pointed-to memory is bound to [`Python::with_gil`]'s [`GILPool`] which will not be dropped until -/// the end of [`Python::with_gil`]'s scope. Only then is each `hello`'s Python reference count -/// decreased. This means that at the last line of the example there are 10 copies of `hello` in -/// Python's memory, not just one at a time as we might expect from Rust's [scoping rules]. +/// interpreter, using functions such as [`Python::eval_bound`] and [`PyModule::import_bound`]. /// -/// See the [Memory Management] chapter of the guide for more information about how PyO3 uses -/// [`GILPool`] to manage memory. +/// See the [Memory Management] chapter of the guide for more information about how PyO3 manages memory. /// /// [scoping rules]: https://doc.rust-lang.org/stable/book/ch04-01-what-is-ownership.html#ownership-rules /// [`Py::clone_ref`]: crate::Py::clone_ref @@ -874,12 +848,10 @@ impl<'py> Python<'py> { /// /// Callers must ensure that ensure that the cast is valid. #[allow(clippy::wrong_self_convention, deprecated)] - #[cfg_attr( - not(feature = "gil-refs"), - deprecated( - since = "0.21.0", - note = "use `Py::from_owned_ptr(py, ptr)` or `Bound::from_owned_ptr(py, ptr)` instead" - ) + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "use `Py::from_owned_ptr(py, ptr)` or `Bound::from_owned_ptr(py, ptr)` instead" )] pub unsafe fn from_owned_ptr(self, ptr: *mut ffi::PyObject) -> &'py T where @@ -897,12 +869,10 @@ impl<'py> Python<'py> { /// /// Callers must ensure that ensure that the cast is valid. #[allow(clippy::wrong_self_convention, deprecated)] - #[cfg_attr( - not(feature = "gil-refs"), - deprecated( - since = "0.21.0", - note = "use `Py::from_owned_ptr_or_err(py, ptr)` or `Bound::from_owned_ptr_or_err(py, ptr)` instead" - ) + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "use `Py::from_owned_ptr_or_err(py, ptr)` or `Bound::from_owned_ptr_or_err(py, ptr)` instead" )] pub unsafe fn from_owned_ptr_or_err(self, ptr: *mut ffi::PyObject) -> PyResult<&'py T> where @@ -920,12 +890,10 @@ impl<'py> Python<'py> { /// /// Callers must ensure that ensure that the cast is valid. #[allow(clippy::wrong_self_convention, deprecated)] - #[cfg_attr( - not(feature = "gil-refs"), - deprecated( - since = "0.21.0", - note = "use `Py::from_owned_ptr_or_opt(py, ptr)` or `Bound::from_owned_ptr_or_opt(py, ptr)` instead" - ) + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "use `Py::from_owned_ptr_or_opt(py, ptr)` or `Bound::from_owned_ptr_or_opt(py, ptr)` instead" )] pub unsafe fn from_owned_ptr_or_opt(self, ptr: *mut ffi::PyObject) -> Option<&'py T> where @@ -942,12 +910,10 @@ impl<'py> Python<'py> { /// /// Callers must ensure that ensure that the cast is valid. #[allow(clippy::wrong_self_convention, deprecated)] - #[cfg_attr( - not(feature = "gil-refs"), - deprecated( - since = "0.21.0", - note = "use `Py::from_borrowed_ptr(py, ptr)` or `Bound::from_borrowed_ptr(py, ptr)` instead" - ) + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "use `Py::from_borrowed_ptr(py, ptr)` or `Bound::from_borrowed_ptr(py, ptr)` instead" )] pub unsafe fn from_borrowed_ptr(self, ptr: *mut ffi::PyObject) -> &'py T where @@ -964,12 +930,10 @@ impl<'py> Python<'py> { /// /// Callers must ensure that ensure that the cast is valid. #[allow(clippy::wrong_self_convention, deprecated)] - #[cfg_attr( - not(feature = "gil-refs"), - deprecated( - since = "0.21.0", - note = "use `Py::from_borrowed_ptr_or_err(py, ptr)` or `Bound::from_borrowed_ptr_or_err(py, ptr)` instead" - ) + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "use `Py::from_borrowed_ptr_or_err(py, ptr)` or `Bound::from_borrowed_ptr_or_err(py, ptr)` instead" )] pub unsafe fn from_borrowed_ptr_or_err(self, ptr: *mut ffi::PyObject) -> PyResult<&'py T> where @@ -986,12 +950,10 @@ impl<'py> Python<'py> { /// /// Callers must ensure that ensure that the cast is valid. #[allow(clippy::wrong_self_convention, deprecated)] - #[cfg_attr( - not(feature = "gil-refs"), - deprecated( - since = "0.21.0", - note = "use `Py::from_borrowed_ptr_or_opt(py, ptr)` or `Bound::from_borrowed_ptr_or_opt(py, ptr)` instead" - ) + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "use `Py::from_borrowed_ptr_or_opt(py, ptr)` or `Bound::from_borrowed_ptr_or_opt(py, ptr)` instead" )] pub unsafe fn from_borrowed_ptr_or_opt(self, ptr: *mut ffi::PyObject) -> Option<&'py T> where @@ -1103,12 +1065,10 @@ impl<'py> Python<'py> { /// /// [`.python()`]: crate::GILPool::python #[inline] - #[cfg_attr( - not(feature = "gil-refs"), - deprecated( - since = "0.21.0", - note = "code not using the GIL Refs API can safely remove use of `Python::new_pool`" - ) + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "code not using the GIL Refs API can safely remove use of `Python::new_pool`" )] #[allow(deprecated)] pub unsafe fn new_pool(self) -> GILPool { @@ -1172,12 +1132,10 @@ impl Python<'_> { /// }); /// ``` #[inline] - #[cfg_attr( - not(feature = "gil-refs"), - deprecated( - since = "0.21.0", - note = "code not using the GIL Refs API can safely remove use of `Python::with_pool`" - ) + #[cfg(feature = "gil-refs")] + #[deprecated( + since = "0.21.0", + note = "code not using the GIL Refs API can safely remove use of `Python::with_pool`" )] #[allow(deprecated)] pub fn with_pool(&self, f: F) -> R diff --git a/src/prelude.rs b/src/prelude.rs index ef42b2706e9..7aa45f6ccc2 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -9,6 +9,7 @@ //! ``` pub use crate::conversion::{FromPyObject, IntoPy, ToPyObject}; +#[cfg(feature = "gil-refs")] #[allow(deprecated)] pub use crate::conversion::{PyTryFrom, PyTryInto}; pub use crate::err::{PyErr, PyResult}; diff --git a/src/pycell.rs b/src/pycell.rs index 215ed7bba66..dc5ccc45ea1 100644 --- a/src/pycell.rs +++ b/src/pycell.rs @@ -518,6 +518,7 @@ impl ToPyObject for &PyCell { } } +#[cfg(feature = "gil-refs")] #[allow(deprecated)] impl AsRef for PyCell { fn as_ref(&self) -> &PyAny { @@ -528,6 +529,7 @@ impl AsRef for PyCell { } } +#[cfg(feature = "gil-refs")] #[allow(deprecated)] impl Deref for PyCell { type Target = PyAny; From 033caa8fd1fefbf51fba97f85f5fbcb191c264bb Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Sat, 11 May 2024 15:48:17 +0200 Subject: [PATCH 071/495] split more impl blocks (#4175) --- src/types/boolobject.rs | 36 ++++++++++--------- src/types/bytearray.rs | 80 ++++++++++++++++++++--------------------- src/types/bytes.rs | 77 ++++++++++++++++++++------------------- src/types/capsule.rs | 70 ++++++++++++++++++------------------ src/types/complex.rs | 26 ++++++++------ src/types/datetime.rs | 8 +++++ src/types/float.rs | 31 ++++++++-------- src/types/slice.rs | 47 ++++++++++++------------ src/types/traceback.rs | 6 ++-- src/types/typeobject.rs | 56 +++++++++++++++-------------- 10 files changed, 235 insertions(+), 202 deletions(-) diff --git a/src/types/boolobject.rs b/src/types/boolobject.rs index 43184e31565..9b5aa659fdf 100644 --- a/src/types/boolobject.rs +++ b/src/types/boolobject.rs @@ -1,9 +1,11 @@ #[cfg(feature = "experimental-inspect")] use crate::inspect::types::TypeInfo; +#[cfg(feature = "gil-refs")] +use crate::PyNativeType; use crate::{ exceptions::PyTypeError, ffi, ffi_ptr_ext::FfiPtrExt, instance::Bound, - types::typeobject::PyTypeMethods, Borrowed, FromPyObject, IntoPy, PyAny, PyNativeType, - PyObject, PyResult, Python, ToPyObject, + types::typeobject::PyTypeMethods, Borrowed, FromPyObject, IntoPy, PyAny, PyObject, PyResult, + Python, ToPyObject, }; use super::any::PyAnyMethods; @@ -15,20 +17,6 @@ pub struct PyBool(PyAny); pyobject_native_type!(PyBool, ffi::PyObject, pyobject_native_static_type_object!(ffi::PyBool_Type), #checkfunction=ffi::PyBool_Check); impl PyBool { - /// Deprecated form of [`PyBool::new_bound`] - #[cfg(feature = "gil-refs")] - #[deprecated( - since = "0.21.0", - note = "`PyBool::new` will be replaced by `PyBool::new_bound` in a future PyO3 version" - )] - #[inline] - pub fn new(py: Python<'_>, val: bool) -> &PyBool { - #[allow(deprecated)] - unsafe { - py.from_borrowed_ptr(if val { ffi::Py_True() } else { ffi::Py_False() }) - } - } - /// Depending on `val`, returns `true` or `false`. /// /// # Note @@ -42,6 +30,22 @@ impl PyBool { .downcast_unchecked() } } +} + +#[cfg(feature = "gil-refs")] +impl PyBool { + /// Deprecated form of [`PyBool::new_bound`] + #[deprecated( + since = "0.21.0", + note = "`PyBool::new` will be replaced by `PyBool::new_bound` in a future PyO3 version" + )] + #[inline] + pub fn new(py: Python<'_>, val: bool) -> &PyBool { + #[allow(deprecated)] + unsafe { + py.from_borrowed_ptr(if val { ffi::Py_True() } else { ffi::Py_False() }) + } + } /// Gets whether this boolean is `true`. #[inline] diff --git a/src/types/bytearray.rs b/src/types/bytearray.rs index ef20509e629..ec3d7eafbfd 100644 --- a/src/types/bytearray.rs +++ b/src/types/bytearray.rs @@ -3,9 +3,9 @@ use crate::ffi_ptr_ext::FfiPtrExt; use crate::instance::{Borrowed, Bound}; use crate::py_result_ext::PyResultExt; use crate::types::any::PyAnyMethods; +use crate::{ffi, PyAny, Python}; #[cfg(feature = "gil-refs")] -use crate::AsPyPointer; -use crate::{ffi, PyAny, PyNativeType, Python}; +use crate::{AsPyPointer, PyNativeType}; use std::os::raw::c_char; use std::slice; @@ -16,16 +16,6 @@ pub struct PyByteArray(PyAny); pyobject_native_type_core!(PyByteArray, pyobject_native_static_type_object!(ffi::PyByteArray_Type), #checkfunction=ffi::PyByteArray_Check); impl PyByteArray { - /// Deprecated form of [`PyByteArray::new_bound`] - #[cfg(feature = "gil-refs")] - #[deprecated( - since = "0.21.0", - note = "`PyByteArray::new` will be replaced by `PyByteArray::new_bound` in a future PyO3 version" - )] - pub fn new<'py>(py: Python<'py>, src: &[u8]) -> &'py PyByteArray { - Self::new_bound(py, src).into_gil_ref() - } - /// Creates a new Python bytearray object. /// /// The byte string is initialized by copying the data from the `&[u8]`. @@ -39,19 +29,6 @@ impl PyByteArray { } } - /// Deprecated form of [`PyByteArray::new_bound_with`] - #[cfg(feature = "gil-refs")] - #[deprecated( - since = "0.21.0", - note = "`PyByteArray::new_with` will be replaced by `PyByteArray::new_bound_with` in a future PyO3 version" - )] - pub fn new_with(py: Python<'_>, len: usize, init: F) -> PyResult<&PyByteArray> - where - F: FnOnce(&mut [u8]) -> PyResult<()>, - { - Self::new_bound_with(py, len, init).map(Bound::into_gil_ref) - } - /// Creates a new Python `bytearray` object with an `init` closure to write its contents. /// Before calling `init` the bytearray is zero-initialised. /// * If Python raises a MemoryError on the allocation, `new_with` will return @@ -101,16 +78,6 @@ impl PyByteArray { } } - /// Deprecated form of [`PyByteArray::from_bound`] - #[cfg(feature = "gil-refs")] - #[deprecated( - since = "0.21.0", - note = "`PyByteArray::from` will be replaced by `PyByteArray::from_bound` in a future PyO3 version" - )] - pub fn from(src: &PyAny) -> PyResult<&PyByteArray> { - PyByteArray::from_bound(&src.as_borrowed()).map(Bound::into_gil_ref) - } - /// Creates a new Python `bytearray` object from another Python object that /// implements the buffer protocol. pub fn from_bound<'py>(src: &Bound<'py, PyAny>) -> PyResult> { @@ -120,6 +87,39 @@ impl PyByteArray { .downcast_into_unchecked() } } +} + +#[cfg(feature = "gil-refs")] +impl PyByteArray { + /// Deprecated form of [`PyByteArray::new_bound`] + #[deprecated( + since = "0.21.0", + note = "`PyByteArray::new` will be replaced by `PyByteArray::new_bound` in a future PyO3 version" + )] + pub fn new<'py>(py: Python<'py>, src: &[u8]) -> &'py PyByteArray { + Self::new_bound(py, src).into_gil_ref() + } + + /// Deprecated form of [`PyByteArray::new_bound_with`] + #[deprecated( + since = "0.21.0", + note = "`PyByteArray::new_with` will be replaced by `PyByteArray::new_bound_with` in a future PyO3 version" + )] + pub fn new_with(py: Python<'_>, len: usize, init: F) -> PyResult<&PyByteArray> + where + F: FnOnce(&mut [u8]) -> PyResult<()>, + { + Self::new_bound_with(py, len, init).map(Bound::into_gil_ref) + } + + /// Deprecated form of [`PyByteArray::from_bound`] + #[deprecated( + since = "0.21.0", + note = "`PyByteArray::from` will be replaced by `PyByteArray::from_bound` in a future PyO3 version" + )] + pub fn from(src: &PyAny) -> PyResult<&PyByteArray> { + PyByteArray::from_bound(&src.as_borrowed()).map(Bound::into_gil_ref) + } /// Gets the length of the bytearray. #[inline] @@ -300,7 +300,7 @@ pub trait PyByteArrayMethods<'py>: crate::sealed::Sealed { /// /// # Safety /// - /// See the safety requirements of [`PyByteArray::as_bytes`] and [`PyByteArray::as_bytes_mut`]. + /// See the safety requirements of [`PyByteArrayMethods::as_bytes`] and [`PyByteArrayMethods::as_bytes_mut`]. fn data(&self) -> *mut u8; /// Extracts a slice of the `ByteArray`'s entire buffer. @@ -311,7 +311,7 @@ pub trait PyByteArrayMethods<'py>: crate::sealed::Sealed { /// undefined. /// /// These mutations may occur in Python code as well as from Rust: - /// - Calling methods like [`PyByteArray::as_bytes_mut`] and [`PyByteArray::resize`] will + /// - Calling methods like [`PyByteArrayMethods::as_bytes_mut`] and [`PyByteArrayMethods::resize`] will /// invalidate the slice. /// - Actions like dropping objects or raising exceptions can invoke `__del__`methods or signal /// handlers, which may execute arbitrary Python code. This means that if Python code has a @@ -405,7 +405,7 @@ pub trait PyByteArrayMethods<'py>: crate::sealed::Sealed { /// # Safety /// /// Any other accesses of the `bytearray`'s buffer invalidate the slice. If it is used - /// afterwards, the behavior is undefined. The safety requirements of [`PyByteArray::as_bytes`] + /// afterwards, the behavior is undefined. The safety requirements of [`PyByteArrayMethods::as_bytes`] /// apply to this function as well. #[allow(clippy::mut_from_ref)] unsafe fn as_bytes_mut(&self) -> &mut [u8]; @@ -432,8 +432,8 @@ pub trait PyByteArrayMethods<'py>: crate::sealed::Sealed { /// Resizes the bytearray object to the new length `len`. /// - /// Note that this will invalidate any pointers obtained by [PyByteArray::data], as well as - /// any (unsafe) slices obtained from [PyByteArray::as_bytes] and [PyByteArray::as_bytes_mut]. + /// Note that this will invalidate any pointers obtained by [PyByteArrayMethods::data], as well as + /// any (unsafe) slices obtained from [PyByteArrayMethods::as_bytes] and [PyByteArrayMethods::as_bytes_mut]. fn resize(&self, len: usize) -> PyResult<()>; } diff --git a/src/types/bytes.rs b/src/types/bytes.rs index f3da65c7c97..1d6a2f8ec7d 100644 --- a/src/types/bytes.rs +++ b/src/types/bytes.rs @@ -1,7 +1,9 @@ use crate::ffi_ptr_ext::FfiPtrExt; use crate::instance::{Borrowed, Bound}; use crate::types::any::PyAnyMethods; -use crate::{ffi, Py, PyAny, PyNativeType, PyResult, Python}; +#[cfg(feature = "gil-refs")] +use crate::PyNativeType; +use crate::{ffi, Py, PyAny, PyResult, Python}; use std::ops::Index; use std::os::raw::c_char; use std::slice::SliceIndex; @@ -16,16 +18,6 @@ pub struct PyBytes(PyAny); pyobject_native_type_core!(PyBytes, pyobject_native_static_type_object!(ffi::PyBytes_Type), #checkfunction=ffi::PyBytes_Check); impl PyBytes { - /// Deprecated form of [`PyBytes::new_bound`]. - #[cfg(feature = "gil-refs")] - #[deprecated( - since = "0.21.0", - note = "`PyBytes::new` will be replaced by `PyBytes::new_bound` in a future PyO3 version" - )] - pub fn new<'p>(py: Python<'p>, s: &[u8]) -> &'p PyBytes { - Self::new_bound(py, s).into_gil_ref() - } - /// Creates a new Python bytestring object. /// The bytestring is initialized by copying the data from the `&[u8]`. /// @@ -40,19 +32,6 @@ impl PyBytes { } } - /// Deprecated form of [`PyBytes::new_bound_with`]. - #[cfg(feature = "gil-refs")] - #[deprecated( - since = "0.21.0", - note = "`PyBytes::new_with` will be replaced by `PyBytes::new_bound_with` in a future PyO3 version" - )] - pub fn new_with(py: Python<'_>, len: usize, init: F) -> PyResult<&PyBytes> - where - F: FnOnce(&mut [u8]) -> PyResult<()>, - { - Self::new_bound_with(py, len, init).map(Bound::into_gil_ref) - } - /// Creates a new Python `bytes` object with an `init` closure to write its contents. /// Before calling `init` the bytes' contents are zero-initialised. /// * If Python raises a MemoryError on the allocation, `new_with` will return @@ -95,19 +74,6 @@ impl PyBytes { } } - /// Deprecated form of [`PyBytes::bound_from_ptr`]. - /// - /// # Safety - /// See [`PyBytes::bound_from_ptr`]. - #[cfg(feature = "gil-refs")] - #[deprecated( - since = "0.21.0", - note = "`PyBytes::from_ptr` will be replaced by `PyBytes::bound_from_ptr` in a future PyO3 version" - )] - pub unsafe fn from_ptr(py: Python<'_>, ptr: *const u8, len: usize) -> &PyBytes { - Self::bound_from_ptr(py, ptr, len).into_gil_ref() - } - /// Creates a new Python byte string object from a raw pointer and length. /// /// Panics if out of memory. @@ -123,6 +89,42 @@ impl PyBytes { .assume_owned(py) .downcast_into_unchecked() } +} + +#[cfg(feature = "gil-refs")] +impl PyBytes { + /// Deprecated form of [`PyBytes::new_bound`]. + #[deprecated( + since = "0.21.0", + note = "`PyBytes::new` will be replaced by `PyBytes::new_bound` in a future PyO3 version" + )] + pub fn new<'p>(py: Python<'p>, s: &[u8]) -> &'p PyBytes { + Self::new_bound(py, s).into_gil_ref() + } + + /// Deprecated form of [`PyBytes::new_bound_with`]. + #[deprecated( + since = "0.21.0", + note = "`PyBytes::new_with` will be replaced by `PyBytes::new_bound_with` in a future PyO3 version" + )] + pub fn new_with(py: Python<'_>, len: usize, init: F) -> PyResult<&PyBytes> + where + F: FnOnce(&mut [u8]) -> PyResult<()>, + { + Self::new_bound_with(py, len, init).map(Bound::into_gil_ref) + } + + /// Deprecated form of [`PyBytes::bound_from_ptr`]. + /// + /// # Safety + /// See [`PyBytes::bound_from_ptr`]. + #[deprecated( + since = "0.21.0", + note = "`PyBytes::from_ptr` will be replaced by `PyBytes::bound_from_ptr` in a future PyO3 version" + )] + pub unsafe fn from_ptr(py: Python<'_>, ptr: *const u8, len: usize) -> &PyBytes { + Self::bound_from_ptr(py, ptr, len).into_gil_ref() + } /// Gets the Python string as a byte slice. #[inline] @@ -172,6 +174,7 @@ impl Py { } /// This is the same way [Vec] is indexed. +#[cfg(feature = "gil-refs")] impl> Index for PyBytes { type Output = I::Output; diff --git a/src/types/capsule.rs b/src/types/capsule.rs index c2b73a0d0f7..2851970ba10 100644 --- a/src/types/capsule.rs +++ b/src/types/capsule.rs @@ -1,11 +1,12 @@ use crate::ffi_ptr_ext::FfiPtrExt; use crate::py_result_ext::PyResultExt; -use crate::{ffi, PyAny, PyNativeType}; +#[cfg(feature = "gil-refs")] +use crate::PyNativeType; +use crate::{ffi, PyAny}; use crate::{Bound, Python}; use crate::{PyErr, PyResult}; use std::ffi::{CStr, CString}; use std::os::raw::{c_char, c_int, c_void}; - /// Represents a Python Capsule /// as described in [Capsules](https://docs.python.org/3/c-api/capsule.html#capsules): /// > This subtype of PyObject represents an opaque value, useful for C extension @@ -46,20 +47,6 @@ pub struct PyCapsule(PyAny); pyobject_native_type_core!(PyCapsule, pyobject_native_static_type_object!(ffi::PyCapsule_Type), #checkfunction=ffi::PyCapsule_CheckExact); impl PyCapsule { - /// Deprecated form of [`PyCapsule::new_bound`]. - #[cfg(feature = "gil-refs")] - #[deprecated( - since = "0.21.0", - note = "`PyCapsule::new` will be replaced by `PyCapsule::new_bound` in a future PyO3 version" - )] - pub fn new( - py: Python<'_>, - value: T, - name: Option, - ) -> PyResult<&Self> { - Self::new_bound(py, value, name).map(Bound::into_gil_ref) - } - /// Constructs a new capsule whose contents are `value`, associated with `name`. /// `name` is the identifier for the capsule; if it is stored as an attribute of a module, /// the name should be in the format `"modulename.attribute"`. @@ -99,24 +86,6 @@ impl PyCapsule { Self::new_bound_with_destructor(py, value, name, |_, _| {}) } - /// Deprecated form of [`PyCapsule::new_bound_with_destructor`]. - #[cfg(feature = "gil-refs")] - #[deprecated( - since = "0.21.0", - note = "`PyCapsule::new_with_destructor` will be replaced by `PyCapsule::new_bound_with_destructor` in a future PyO3 version" - )] - pub fn new_with_destructor< - T: 'static + Send + AssertNotZeroSized, - F: FnOnce(T, *mut c_void) + Send, - >( - py: Python<'_>, - value: T, - name: Option, - destructor: F, - ) -> PyResult<&'_ Self> { - Self::new_bound_with_destructor(py, value, name, destructor).map(Bound::into_gil_ref) - } - /// Constructs a new capsule whose contents are `value`, associated with `name`. /// /// Also provides a destructor: when the `PyCapsule` is destroyed, it will be passed the original object, @@ -172,6 +141,39 @@ impl PyCapsule { Ok(&*ptr.cast::()) } } +} + +#[cfg(feature = "gil-refs")] +impl PyCapsule { + /// Deprecated form of [`PyCapsule::new_bound`]. + #[deprecated( + since = "0.21.0", + note = "`PyCapsule::new` will be replaced by `PyCapsule::new_bound` in a future PyO3 version" + )] + pub fn new( + py: Python<'_>, + value: T, + name: Option, + ) -> PyResult<&Self> { + Self::new_bound(py, value, name).map(Bound::into_gil_ref) + } + + /// Deprecated form of [`PyCapsule::new_bound_with_destructor`]. + #[deprecated( + since = "0.21.0", + note = "`PyCapsule::new_with_destructor` will be replaced by `PyCapsule::new_bound_with_destructor` in a future PyO3 version" + )] + pub fn new_with_destructor< + T: 'static + Send + AssertNotZeroSized, + F: FnOnce(T, *mut c_void) + Send, + >( + py: Python<'_>, + value: T, + name: Option, + destructor: F, + ) -> PyResult<&'_ Self> { + Self::new_bound_with_destructor(py, value, name, destructor).map(Bound::into_gil_ref) + } /// Sets the context pointer in the capsule. /// diff --git a/src/types/complex.rs b/src/types/complex.rs index cd3d5810d04..65b08cc9c5c 100644 --- a/src/types/complex.rs +++ b/src/types/complex.rs @@ -1,4 +1,6 @@ -use crate::{ffi, types::any::PyAnyMethods, Bound, PyAny, PyNativeType, Python}; +#[cfg(feature = "gil-refs")] +use crate::PyNativeType; +use crate::{ffi, types::any::PyAnyMethods, Bound, PyAny, Python}; use std::os::raw::c_double; /// Represents a Python [`complex`](https://docs.python.org/3/library/functions.html#complex) object. @@ -19,15 +21,6 @@ pyobject_native_type!( ); impl PyComplex { - /// Deprecated form of [`PyComplex::from_doubles_bound`] - #[cfg(feature = "gil-refs")] - #[deprecated( - since = "0.21.0", - note = "`PyComplex::from_doubles` will be replaced by `PyComplex::from_doubles_bound` in a future PyO3 version" - )] - pub fn from_doubles(py: Python<'_>, real: c_double, imag: c_double) -> &PyComplex { - Self::from_doubles_bound(py, real, imag).into_gil_ref() - } /// Creates a new `PyComplex` from the given real and imaginary values. pub fn from_doubles_bound( py: Python<'_>, @@ -41,6 +34,19 @@ impl PyComplex { .downcast_into_unchecked() } } +} + +#[cfg(feature = "gil-refs")] +impl PyComplex { + /// Deprecated form of [`PyComplex::from_doubles_bound`] + #[deprecated( + since = "0.21.0", + note = "`PyComplex::from_doubles` will be replaced by `PyComplex::from_doubles_bound` in a future PyO3 version" + )] + pub fn from_doubles(py: Python<'_>, real: c_double, imag: c_double) -> &PyComplex { + Self::from_doubles_bound(py, real, imag).into_gil_ref() + } + /// Returns the real part of the complex number. pub fn real(&self) -> c_double { self.as_borrowed().real() diff --git a/src/types/datetime.rs b/src/types/datetime.rs index 12db67ab961..cdf3b011e6c 100644 --- a/src/types/datetime.rs +++ b/src/types/datetime.rs @@ -22,6 +22,7 @@ use crate::ffi::{ PyDateTime_TIME_GET_MINUTE, PyDateTime_TIME_GET_SECOND, }; use crate::ffi_ptr_ext::FfiPtrExt; +#[cfg(feature = "gil-refs")] use crate::instance::PyNativeType; use crate::py_result_ext::PyResultExt; use crate::types::any::PyAnyMethods; @@ -249,6 +250,7 @@ impl PyDate { } } +#[cfg(feature = "gil-refs")] impl PyDateAccess for PyDate { fn get_year(&self) -> i32 { self.as_borrowed().get_year() @@ -461,6 +463,7 @@ impl PyDateTime { } } +#[cfg(feature = "gil-refs")] impl PyDateAccess for PyDateTime { fn get_year(&self) -> i32 { self.as_borrowed().get_year() @@ -489,6 +492,7 @@ impl PyDateAccess for Bound<'_, PyDateTime> { } } +#[cfg(feature = "gil-refs")] impl PyTimeAccess for PyDateTime { fn get_hour(&self) -> u8 { self.as_borrowed().get_hour() @@ -533,6 +537,7 @@ impl PyTimeAccess for Bound<'_, PyDateTime> { } } +#[cfg(feature = "gil-refs")] impl<'py> PyTzInfoAccess<'py> for &'py PyDateTime { fn get_tzinfo_bound(&self) -> Option> { self.as_borrowed().get_tzinfo_bound() @@ -688,6 +693,7 @@ impl PyTime { } } +#[cfg(feature = "gil-refs")] impl PyTimeAccess for PyTime { fn get_hour(&self) -> u8 { self.as_borrowed().get_hour() @@ -732,6 +738,7 @@ impl PyTimeAccess for Bound<'_, PyTime> { } } +#[cfg(feature = "gil-refs")] impl<'py> PyTzInfoAccess<'py> for &'py PyTime { fn get_tzinfo_bound(&self) -> Option> { self.as_borrowed().get_tzinfo_bound() @@ -878,6 +885,7 @@ impl PyDelta { } } +#[cfg(feature = "gil-refs")] impl PyDeltaAccess for PyDelta { fn get_days(&self) -> i32 { self.as_borrowed().get_days() diff --git a/src/types/float.rs b/src/types/float.rs index 2ed3d2921b9..8499d1e54aa 100644 --- a/src/types/float.rs +++ b/src/types/float.rs @@ -1,13 +1,14 @@ +use super::any::PyAnyMethods; #[cfg(feature = "experimental-inspect")] use crate::inspect::types::TypeInfo; +#[cfg(feature = "gil-refs")] +use crate::PyNativeType; use crate::{ - ffi, ffi_ptr_ext::FfiPtrExt, instance::Bound, FromPyObject, IntoPy, PyAny, PyErr, PyNativeType, - PyObject, PyResult, Python, ToPyObject, + ffi, ffi_ptr_ext::FfiPtrExt, instance::Bound, FromPyObject, IntoPy, PyAny, PyErr, PyObject, + PyResult, Python, ToPyObject, }; use std::os::raw::c_double; -use super::any::PyAnyMethods; - /// Represents a Python `float` object. /// /// You can usually avoid directly working with this type @@ -23,10 +24,21 @@ pyobject_native_type!( #checkfunction=ffi::PyFloat_Check ); +impl PyFloat { + /// Creates a new Python `float` object. + pub fn new_bound(py: Python<'_>, val: c_double) -> Bound<'_, PyFloat> { + unsafe { + ffi::PyFloat_FromDouble(val) + .assume_owned(py) + .downcast_into_unchecked() + } + } +} + +#[cfg(feature = "gil-refs")] impl PyFloat { /// Deprecated form of [`PyFloat::new_bound`]. #[inline] - #[cfg(feature = "gil-refs")] #[deprecated( since = "0.21.0", note = "`PyFloat::new` will be replaced by `PyFloat::new_bound` in a future PyO3 version" @@ -35,15 +47,6 @@ impl PyFloat { Self::new_bound(py, val).into_gil_ref() } - /// Creates a new Python `float` object. - pub fn new_bound(py: Python<'_>, val: c_double) -> Bound<'_, PyFloat> { - unsafe { - ffi::PyFloat_FromDouble(val) - .assume_owned(py) - .downcast_into_unchecked() - } - } - /// Gets the value of this float. pub fn value(&self) -> c_double { self.as_borrowed().value() diff --git a/src/types/slice.rs b/src/types/slice.rs index 70285c9c251..7daa2c030b6 100644 --- a/src/types/slice.rs +++ b/src/types/slice.rs @@ -2,7 +2,9 @@ use crate::err::{PyErr, PyResult}; use crate::ffi; use crate::ffi_ptr_ext::FfiPtrExt; use crate::types::any::PyAnyMethods; -use crate::{Bound, PyAny, PyNativeType, PyObject, Python, ToPyObject}; +#[cfg(feature = "gil-refs")] +use crate::PyNativeType; +use crate::{Bound, PyAny, PyObject, Python, ToPyObject}; /// Represents a Python `slice`. /// @@ -17,7 +19,7 @@ pyobject_native_type!( #checkfunction=ffi::PySlice_Check ); -/// Return value from [`PySlice::indices`]. +/// Return value from [`PySliceMethods::indices`]. #[derive(Debug, Eq, PartialEq)] pub struct PySliceIndices { /// Start of the slice @@ -47,16 +49,6 @@ impl PySliceIndices { } impl PySlice { - /// Deprecated form of `PySlice::new_bound`. - #[cfg(feature = "gil-refs")] - #[deprecated( - since = "0.21.0", - note = "`PySlice::new` will be replaced by `PySlice::new_bound` in a future PyO3 version" - )] - pub fn new(py: Python<'_>, start: isize, stop: isize, step: isize) -> &PySlice { - Self::new_bound(py, start, stop, step).into_gil_ref() - } - /// Constructs a new slice with the given elements. pub fn new_bound(py: Python<'_>, start: isize, stop: isize, step: isize) -> Bound<'_, PySlice> { unsafe { @@ -70,16 +62,6 @@ impl PySlice { } } - /// Deprecated form of `PySlice::full_bound`. - #[cfg(feature = "gil-refs")] - #[deprecated( - since = "0.21.0", - note = "`PySlice::full` will be replaced by `PySlice::full_bound` in a future PyO3 version" - )] - pub fn full(py: Python<'_>) -> &PySlice { - PySlice::full_bound(py).into_gil_ref() - } - /// Constructs a new full slice that is equivalent to `::`. pub fn full_bound(py: Python<'_>) -> Bound<'_, PySlice> { unsafe { @@ -88,6 +70,27 @@ impl PySlice { .downcast_into_unchecked() } } +} + +#[cfg(feature = "gil-refs")] +impl PySlice { + /// Deprecated form of `PySlice::new_bound`. + #[deprecated( + since = "0.21.0", + note = "`PySlice::new` will be replaced by `PySlice::new_bound` in a future PyO3 version" + )] + pub fn new(py: Python<'_>, start: isize, stop: isize, step: isize) -> &PySlice { + Self::new_bound(py, start, stop, step).into_gil_ref() + } + + /// Deprecated form of `PySlice::full_bound`. + #[deprecated( + since = "0.21.0", + note = "`PySlice::full` will be replaced by `PySlice::full_bound` in a future PyO3 version" + )] + pub fn full(py: Python<'_>) -> &PySlice { + PySlice::full_bound(py).into_gil_ref() + } /// Retrieves the start, stop, and step indices from the slice object, /// assuming a sequence of length `length`, and stores the length of the diff --git a/src/types/traceback.rs b/src/types/traceback.rs index c4cedd791f6..dbbdb6a85ea 100644 --- a/src/types/traceback.rs +++ b/src/types/traceback.rs @@ -1,7 +1,8 @@ use crate::err::{error_on_minusone, PyResult}; use crate::types::{any::PyAnyMethods, string::PyStringMethods, PyString}; -use crate::{ffi, Bound}; -use crate::{PyAny, PyNativeType}; +#[cfg(feature = "gil-refs")] +use crate::PyNativeType; +use crate::{ffi, Bound, PyAny}; /// Represents a Python traceback. #[repr(transparent)] @@ -13,6 +14,7 @@ pyobject_native_type_core!( #checkfunction=ffi::PyTraceBack_Check ); +#[cfg(feature = "gil-refs")] impl PyTraceback { /// Formats the traceback as a string. /// diff --git a/src/types/typeobject.rs b/src/types/typeobject.rs index 2261834ef2a..e6c4a2180d9 100644 --- a/src/types/typeobject.rs +++ b/src/types/typeobject.rs @@ -1,21 +1,47 @@ use crate::err::{self, PyResult}; use crate::instance::Borrowed; use crate::types::any::PyAnyMethods; -use crate::{ffi, Bound, PyAny, PyNativeType, PyTypeInfo, Python}; +#[cfg(feature = "gil-refs")] +use crate::PyNativeType; +use crate::{ffi, Bound, PyAny, PyTypeInfo, Python}; use std::borrow::Cow; #[cfg(not(any(Py_LIMITED_API, PyPy)))] use std::ffi::CStr; - /// Represents a reference to a Python `type object`. #[repr(transparent)] pub struct PyType(PyAny); pyobject_native_type_core!(PyType, pyobject_native_static_type_object!(ffi::PyType_Type), #checkfunction=ffi::PyType_Check); +impl PyType { + /// Creates a new type object. + #[inline] + pub fn new_bound(py: Python<'_>) -> Bound<'_, PyType> { + T::type_object_bound(py) + } + + /// Converts the given FFI pointer into `Bound`, to use in safe code. + /// + /// The function creates a new reference from the given pointer, and returns + /// it as a `Bound`. + /// + /// # Safety + /// - The pointer must be a valid non-null reference to a `PyTypeObject` + #[inline] + pub unsafe fn from_borrowed_type_ptr( + py: Python<'_>, + p: *mut ffi::PyTypeObject, + ) -> Bound<'_, PyType> { + Borrowed::from_ptr_unchecked(py, p.cast()) + .downcast_unchecked() + .to_owned() + } +} + +#[cfg(feature = "gil-refs")] impl PyType { /// Deprecated form of [`PyType::new_bound`]. #[inline] - #[cfg(feature = "gil-refs")] #[deprecated( since = "0.21.0", note = "`PyType::new` will be replaced by `PyType::new_bound` in a future PyO3 version" @@ -24,12 +50,6 @@ impl PyType { T::type_object_bound(py).into_gil_ref() } - /// Creates a new type object. - #[inline] - pub fn new_bound(py: Python<'_>) -> Bound<'_, PyType> { - T::type_object_bound(py) - } - /// Retrieves the underlying FFI pointer associated with this Python object. #[inline] pub fn as_type_ptr(&self) -> *mut ffi::PyTypeObject { @@ -42,7 +62,6 @@ impl PyType { /// /// - The pointer must a valid non-null reference to a `PyTypeObject`. #[inline] - #[cfg(feature = "gil-refs")] #[deprecated( since = "0.21.0", note = "Use `PyType::from_borrowed_type_ptr` instead" @@ -51,23 +70,6 @@ impl PyType { Self::from_borrowed_type_ptr(py, p).into_gil_ref() } - /// Converts the given FFI pointer into `Bound`, to use in safe code. - /// - /// The function creates a new reference from the given pointer, and returns - /// it as a `Bound`. - /// - /// # Safety - /// - The pointer must be a valid non-null reference to a `PyTypeObject` - #[inline] - pub unsafe fn from_borrowed_type_ptr( - py: Python<'_>, - p: *mut ffi::PyTypeObject, - ) -> Bound<'_, PyType> { - Borrowed::from_ptr_unchecked(py, p.cast()) - .downcast_unchecked() - .to_owned() - } - /// Gets the [qualified name](https://docs.python.org/3/glossary.html#term-qualified-name) of the `PyType`. pub fn qualname(&self) -> PyResult { self.as_borrowed().qualname() From c5f9001985c21ec9c5616c3ee09f2f1e5ca746af Mon Sep 17 00:00:00 2001 From: Adam Reichold Date: Sat, 11 May 2024 16:48:45 +0200 Subject: [PATCH 072/495] Remove deferred reference count increments and make the global reference pool optional (#4095) * Add feature controlling the global reference pool to enable avoiding its overhead. * Document reference-pool feature in the performance guide. * Invert semantics of feature to disable reference pool so the new behaviour becomes opt-in * Remove delayed reference count increments as we cannot prevent reference count errors as long as these are available * Adjust tests to be compatible with disable-reference-pool feature * Adjust tests to be compatible with py-clone feature * Adjust the GIL benchmark to the updated reference pool semantics. * Further extend and clarify the documentation of the py-clone and disable-reference-pool features * Replace disable-reference-pool feature by pyo3_disable_reference_pool conditional compilation flag Such a flag is harder to use and thereby also harder to abuse. This seems appropriate as this is purely a performance-oriented change which show only be enabled by leaf crates and brings with it additional highly implicit sources of process aborts. * Add pyo3_leak_on_drop_without_reference_pool to turn aborts into leaks when the global reference pool is disabled and the GIL is not held --- Cargo.toml | 4 + examples/Cargo.toml | 2 +- guide/src/class.md | 4 +- guide/src/faq.md | 7 +- guide/src/features.md | 10 ++ guide/src/memory.md | 9 +- guide/src/migration.md | 13 +- guide/src/performance.md | 44 ++++++ newsfragments/4095.added.md | 1 + newsfragments/4095.changed.md | 1 + pyo3-benches/benches/bench_gil.rs | 12 +- pyo3-build-config/src/lib.rs | 2 + src/conversions/std/option.rs | 2 +- src/err/err_state.rs | 14 +- src/err/mod.rs | 2 +- src/gil.rs | 242 ++++++------------------------ src/instance.rs | 32 +++- src/marker.rs | 4 +- src/pybacked.rs | 9 +- src/tests/hygiene/pyclass.rs | 2 +- src/types/capsule.rs | 8 +- src/types/iterator.rs | 2 +- src/types/sequence.rs | 2 +- tests/test_buffer_protocol.rs | 2 +- tests/test_bytes.rs | 2 + tests/test_class_basics.rs | 14 +- tests/test_class_conversion.rs | 4 + tests/test_gc.rs | 2 + tests/test_methods.rs | 3 + tests/test_no_imports.rs | 3 + tests/test_sequence.rs | 3 + tests/test_serde.rs | 5 +- 32 files changed, 226 insertions(+), 240 deletions(-) create mode 100644 newsfragments/4095.added.md create mode 100644 newsfragments/4095.changed.md diff --git a/Cargo.toml b/Cargo.toml index 4c5a083060d..aba8fd1bc98 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -108,6 +108,9 @@ auto-initialize = [] # Allows use of the deprecated "GIL Refs" APIs. gil-refs = [] +# Enables `Clone`ing references to Python objects `Py` which panics if the GIL is not held. +py-clone = [] + # Optimizes PyObject to Vec conversion and so on. nightly = [] @@ -129,6 +132,7 @@ full = [ "num-bigint", "num-complex", "num-rational", + "py-clone", "rust_decimal", "serde", "smallvec", diff --git a/examples/Cargo.toml b/examples/Cargo.toml index e54b3b5cde2..81557e7f534 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -10,5 +10,5 @@ pyo3 = { path = "..", features = ["auto-initialize", "extension-module"] } [[example]] name = "decorator" path = "decorator/src/lib.rs" -crate_type = ["cdylib"] +crate-type = ["cdylib"] doc-scrape-examples = true diff --git a/guide/src/class.md b/guide/src/class.md index 3fcfaca4bdc..91a6fb2c495 100644 --- a/guide/src/class.md +++ b/guide/src/class.md @@ -249,7 +249,7 @@ fn return_myclass() -> Py { let obj = return_myclass(); -Python::with_gil(|py| { +Python::with_gil(move |py| { let bound = obj.bind(py); // Py::bind returns &Bound<'py, MyClass> let obj_ref = bound.borrow(); // Get PyRef assert_eq!(obj_ref.num, 1); @@ -280,6 +280,8 @@ let py_counter: Py = Python::with_gil(|py| { }); py_counter.get().value.fetch_add(1, Ordering::Relaxed); + +Python::with_gil(move |_py| drop(py_counter)); ``` Frozen classes are likely to become the default thereby guiding the PyO3 ecosystem towards a more deliberate application of interior mutability. Eventually, this should enable further optimizations of PyO3's internals and avoid downstream code paying the cost of interior mutability when it is not actually required. diff --git a/guide/src/faq.md b/guide/src/faq.md index 19f9b5d50ab..b79641a3803 100644 --- a/guide/src/faq.md +++ b/guide/src/faq.md @@ -127,12 +127,10 @@ If you don't want that cloning to happen, a workaround is to allocate the field ```rust # use pyo3::prelude::*; #[pyclass] -#[derive(Clone)] struct Inner {/* fields omitted */} #[pyclass] struct Outer { - #[pyo3(get)] inner: Py, } @@ -144,6 +142,11 @@ impl Outer { inner: Py::new(py, Inner {})?, }) } + + #[getter] + fn inner(&self, py: Python<'_>) -> Py { + self.inner.clone_ref(py) + } } ``` This time `a` and `b` *are* the same object: diff --git a/guide/src/features.md b/guide/src/features.md index 07085a9e89c..6a25d40cedc 100644 --- a/guide/src/features.md +++ b/guide/src/features.md @@ -75,6 +75,14 @@ This feature is a backwards-compatibility feature to allow continued use of the This feature and the APIs it enables is expected to be removed in a future PyO3 version. +### `py-clone` + +This feature was introduced to ease migration. It was found that delayed reference counts cannot be made sound and hence `Clon`ing an instance of `Py` must panic without the GIL being held. To avoid migrations introducing new panics without warning, the `Clone` implementation itself is now gated behind this feature. + +### `pyo3_disable_reference_pool` + +This is a performance-oriented conditional compilation flag, e.g. [set via `$RUSTFLAGS`][set-configuration-options], which disabled the global reference pool and the assocaited overhead for the crossing the Python-Rust boundary. However, if enabled, `Drop`ping an instance of `Py` without the GIL being held will abort the process. + ### `macros` This feature enables a dependency on the `pyo3-macros` crate, which provides the procedural macros portion of PyO3's API: @@ -195,3 +203,5 @@ struct User { ### `smallvec` Adds a dependency on [smallvec](https://docs.rs/smallvec) and enables conversions into its [`SmallVec`](https://docs.rs/smallvec/latest/smallvec/struct.SmallVec.html) type. + +[set-configuration-options]: https://doc.rust-lang.org/reference/conditional-compilation.html#set-configuration-options diff --git a/guide/src/memory.md b/guide/src/memory.md index b37d83563e5..38a31f4d0ef 100644 --- a/guide/src/memory.md +++ b/guide/src/memory.md @@ -212,7 +212,8 @@ This example wasn't very interesting. We could have just used a GIL-bound we are *not* holding the GIL? ```rust -# #![allow(unused_imports)] +# #![allow(unused_imports, dead_code)] +# #[cfg(not(pyo3_disable_reference_pool))] { # use pyo3::prelude::*; # use pyo3::types::PyString; # fn main() -> PyResult<()> { @@ -239,12 +240,14 @@ Python::with_gil(|py| # } # Ok(()) # } +# } ``` When `hello` is dropped *nothing* happens to the pointed-to memory on Python's heap because nothing _can_ happen if we're not holding the GIL. Fortunately, -the memory isn't leaked. PyO3 keeps track of the memory internally and will -release it the next time we acquire the GIL. +the memory isn't leaked. If the `pyo3_disable_reference_pool` conditional compilation flag +is not enabled, PyO3 keeps track of the memory internally and will release it +the next time we acquire the GIL. We can avoid the delay in releasing memory if we are careful to drop the `Py` while the GIL is held. diff --git a/guide/src/migration.md b/guide/src/migration.md index 7e0420de22d..10b62002a02 100644 --- a/guide/src/migration.md +++ b/guide/src/migration.md @@ -35,7 +35,16 @@ fn increment(x: u64, amount: Option) -> u64 { x + amount.unwrap_or(1) } ``` +
+ +### `Py::clone` is now gated behind the `py-clone` feature +
+Click to expand +If you rely on `impl Clone for Py` to fulfil trait requirements imposed by existing Rust code written without PyO3-based code in mind, the newly introduced feature `py-clone` must be enabled. +However, take care to note that the behaviour is different from previous versions. If `Clone` was called without the GIL being held, we tried to delay the application of these reference count increments until PyO3-based code would re-acquire it. This turned out to be impossible to implement in a sound manner and hence was removed. Now, if `Clone` is called without the GIL being held, we panic instead for which calling code might not be prepared. + +Related to this, we also added a `pyo3_disable_reference_pool` conditional compilation flag which removes the infrastructure necessary to apply delayed reference count decrements implied by `impl Drop for Py`. They do not appear to be a soundness hazard as they should lead to memory leaks in the worst case. However, the global synchronization adds significant overhead to cross the Python-Rust boundary. Enabling this feature will remove these costs and make the `Drop` implementation abort the process if called without the GIL being held instead.
## from 0.20.* to 0.21 @@ -676,7 +685,7 @@ drop(second); The replacement is [`Python::with_gil`](https://docs.rs/pyo3/0.18.3/pyo3/marker/struct.Python.html#method.with_gil) which is more cumbersome but enforces the proper nesting by design, e.g. -```rust +```rust,ignore # #![allow(dead_code)] # use pyo3::prelude::*; @@ -701,7 +710,7 @@ let second = Python::with_gil(|py| Object::new(py)); drop(first); drop(second); -// Or it ensure releasing the inner lock before the outer one. +// Or it ensures releasing the inner lock before the outer one. Python::with_gil(|py| { let first = Object::new(py); let second = Python::with_gil(|py| Object::new(py)); diff --git a/guide/src/performance.md b/guide/src/performance.md index c47a91deee5..b3d160fe6b1 100644 --- a/guide/src/performance.md +++ b/guide/src/performance.md @@ -96,3 +96,47 @@ impl PartialEq for FooBound<'_> { } } ``` + +## Disable the global reference pool + +PyO3 uses global mutable state to keep track of deferred reference count updates implied by `impl Drop for Py` being called without the GIL being held. The necessary synchronization to obtain and apply these reference count updates when PyO3-based code next acquires the GIL is somewhat expensive and can become a significant part of the cost of crossing the Python-Rust boundary. + +This functionality can be avoided by setting the `pyo3_disable_reference_pool` conditional compilation flag. This removes the global reference pool and the associated costs completely. However, it does _not_ remove the `Drop` implementation for `Py` which is necessary to interoperate with existing Rust code written without PyO3-based code in mind. To stay compatible with the wider Rust ecosystem in these cases, we keep the implementation but abort when `Drop` is called without the GIL being held. If `pyo3_leak_on_drop_without_reference_pool` is additionally enabled, objects dropped without the GIL being held will be leaked instead which is always sound but might have determinal effects like resource exhaustion in the long term. + +This limitation is important to keep in mind when this setting is used, especially when embedding Python code into a Rust application as it is quite easy to accidentally drop a `Py` (or types containing it like `PyErr`, `PyBackedStr` or `PyBackedBytes`) returned from `Python::with_gil` without making sure to re-acquire the GIL beforehand. For example, the following code + +```rust,ignore +# use pyo3::prelude::*; +# use pyo3::types::PyList; +let numbers: Py = Python::with_gil(|py| PyList::empty_bound(py).unbind()); + +Python::with_gil(|py| { + numbers.bind(py).append(23).unwrap(); +}); + +Python::with_gil(|py| { + numbers.bind(py).append(42).unwrap(); +}); +``` + +will abort if the list not explicitly disposed via + +```rust +# use pyo3::prelude::*; +# use pyo3::types::PyList; +let numbers: Py = Python::with_gil(|py| PyList::empty_bound(py).unbind()); + +Python::with_gil(|py| { + numbers.bind(py).append(23).unwrap(); +}); + +Python::with_gil(|py| { + numbers.bind(py).append(42).unwrap(); +}); + +Python::with_gil(move |py| { + drop(numbers); +}); +``` + +[conditional-compilation]: https://doc.rust-lang.org/reference/conditional-compilation.html diff --git a/newsfragments/4095.added.md b/newsfragments/4095.added.md new file mode 100644 index 00000000000..c9940f70f12 --- /dev/null +++ b/newsfragments/4095.added.md @@ -0,0 +1 @@ +Add `pyo3_disable_reference_pool` conditional compilation flag to avoid the overhead of the global reference pool at the cost of known limitations as explained in the performance section of the guide. diff --git a/newsfragments/4095.changed.md b/newsfragments/4095.changed.md new file mode 100644 index 00000000000..7f155ae04ef --- /dev/null +++ b/newsfragments/4095.changed.md @@ -0,0 +1 @@ +`Clone`ing pointers into the Python heap has been moved behind the `py-clone` feature, as it must panic without the GIL being held as a soundness fix. diff --git a/pyo3-benches/benches/bench_gil.rs b/pyo3-benches/benches/bench_gil.rs index 59b9ff9686f..cede8836f35 100644 --- a/pyo3-benches/benches/bench_gil.rs +++ b/pyo3-benches/benches/bench_gil.rs @@ -1,4 +1,4 @@ -use codspeed_criterion_compat::{criterion_group, criterion_main, BatchSize, Bencher, Criterion}; +use codspeed_criterion_compat::{criterion_group, criterion_main, Bencher, Criterion}; use pyo3::prelude::*; @@ -9,14 +9,8 @@ fn bench_clean_acquire_gil(b: &mut Bencher<'_>) { fn bench_dirty_acquire_gil(b: &mut Bencher<'_>) { let obj = Python::with_gil(|py| py.None()); - b.iter_batched( - || { - // Clone and drop an object so that the GILPool has work to do. - let _ = obj.clone(); - }, - |_| Python::with_gil(|_| {}), - BatchSize::NumBatches(1), - ); + // Drop the returned clone of the object so that the reference pool has work to do. + b.iter(|| Python::with_gil(|py| obj.clone_ref(py))); } fn criterion_benchmark(c: &mut Criterion) { diff --git a/pyo3-build-config/src/lib.rs b/pyo3-build-config/src/lib.rs index 24d3ae28124..54aff4d10de 100644 --- a/pyo3-build-config/src/lib.rs +++ b/pyo3-build-config/src/lib.rs @@ -165,6 +165,8 @@ pub fn print_expected_cfgs() { println!("cargo:rustc-check-cfg=cfg(GraalPy)"); println!("cargo:rustc-check-cfg=cfg(py_sys_config, values(\"Py_DEBUG\", \"Py_REF_DEBUG\", \"Py_TRACE_REFS\", \"COUNT_ALLOCS\"))"); println!("cargo:rustc-check-cfg=cfg(invalid_from_utf8_lint)"); + println!("cargo:rustc-check-cfg=cfg(pyo3_disable_reference_pool)"); + println!("cargo:rustc-check-cfg=cfg(pyo3_leak_on_drop_without_reference_pool)"); // allow `Py_3_*` cfgs from the minimum supported version up to the // maximum minor version (+1 for development for the next) diff --git a/src/conversions/std/option.rs b/src/conversions/std/option.rs index 2fa082ba16a..13527315e70 100644 --- a/src/conversions/std/option.rs +++ b/src/conversions/std/option.rs @@ -61,7 +61,7 @@ mod tests { assert_eq!(option.as_ptr(), std::ptr::null_mut()); let none = py.None(); - option = Some(none.clone()); + option = Some(none.clone_ref(py)); let ref_cnt = none.get_refcnt(py); assert_eq!(option.as_ptr(), none.as_ptr()); diff --git a/src/err/err_state.rs b/src/err/err_state.rs index 9f85296f661..14345b275c9 100644 --- a/src/err/err_state.rs +++ b/src/err/err_state.rs @@ -5,7 +5,6 @@ use crate::{ Bound, IntoPy, Py, PyAny, PyObject, PyTypeInfo, Python, }; -#[derive(Clone)] pub(crate) struct PyErrStateNormalized { #[cfg(not(Py_3_12))] ptype: Py, @@ -63,6 +62,19 @@ impl PyErrStateNormalized { ptraceback: Py::from_owned_ptr_or_opt(py, ptraceback), } } + + pub fn clone_ref(&self, py: Python<'_>) -> Self { + Self { + #[cfg(not(Py_3_12))] + ptype: self.ptype.clone_ref(py), + pvalue: self.pvalue.clone_ref(py), + #[cfg(not(Py_3_12))] + ptraceback: self + .ptraceback + .as_ref() + .map(|ptraceback| ptraceback.clone_ref(py)), + } + } } pub(crate) struct PyErrStateLazyFnOutput { diff --git a/src/err/mod.rs b/src/err/mod.rs index 52e4b6c616a..29d4ac36294 100644 --- a/src/err/mod.rs +++ b/src/err/mod.rs @@ -837,7 +837,7 @@ impl PyErr { /// ``` #[inline] pub fn clone_ref(&self, py: Python<'_>) -> PyErr { - PyErr::from_state(PyErrState::Normalized(self.normalized(py).clone())) + PyErr::from_state(PyErrState::Normalized(self.normalized(py).clone_ref(py))) } /// Return the cause (either an exception instance, or None, set by `raise ... from ...`) diff --git a/src/gil.rs b/src/gil.rs index 29c4ffbe389..d4b92a4cff4 100644 --- a/src/gil.rs +++ b/src/gil.rs @@ -1,6 +1,8 @@ //! Interaction with Python's global interpreter lock use crate::impl_::not_send::{NotSend, NOT_SEND}; +#[cfg(pyo3_disable_reference_pool)] +use crate::impl_::panic::PanicTrap; use crate::{ffi, Python}; use std::cell::Cell; #[cfg(debug_assertions)] @@ -233,42 +235,32 @@ impl Drop for GILGuard { // Vector of PyObject type PyObjVec = Vec>; +#[cfg(not(pyo3_disable_reference_pool))] /// Thread-safe storage for objects which were inc_ref / dec_ref while the GIL was not held. struct ReferencePool { - // .0 is INCREFs, .1 is DECREFs - pointer_ops: sync::Mutex<(PyObjVec, PyObjVec)>, + pending_decrefs: sync::Mutex, } +#[cfg(not(pyo3_disable_reference_pool))] impl ReferencePool { const fn new() -> Self { Self { - pointer_ops: sync::Mutex::new((Vec::new(), Vec::new())), + pending_decrefs: sync::Mutex::new(Vec::new()), } } - fn register_incref(&self, obj: NonNull) { - self.pointer_ops.lock().unwrap().0.push(obj); - } - fn register_decref(&self, obj: NonNull) { - self.pointer_ops.lock().unwrap().1.push(obj); + self.pending_decrefs.lock().unwrap().push(obj); } fn update_counts(&self, _py: Python<'_>) { - let mut ops = self.pointer_ops.lock().unwrap(); - if ops.0.is_empty() && ops.1.is_empty() { + let mut pending_decrefs = self.pending_decrefs.lock().unwrap(); + if pending_decrefs.is_empty() { return; } - let (increfs, decrefs) = mem::take(&mut *ops); - drop(ops); - - // Always increase reference counts first - as otherwise objects which have a - // nonzero total reference count might be incorrectly dropped by Python during - // this update. - for ptr in increfs { - unsafe { ffi::Py_INCREF(ptr.as_ptr()) }; - } + let decrefs = mem::take(&mut *pending_decrefs); + drop(pending_decrefs); for ptr in decrefs { unsafe { ffi::Py_DECREF(ptr.as_ptr()) }; @@ -276,8 +268,10 @@ impl ReferencePool { } } +#[cfg(not(pyo3_disable_reference_pool))] unsafe impl Sync for ReferencePool {} +#[cfg(not(pyo3_disable_reference_pool))] static POOL: ReferencePool = ReferencePool::new(); /// A guard which can be used to temporarily release the GIL and restore on `Drop`. @@ -302,6 +296,7 @@ impl Drop for SuspendGIL { ffi::PyEval_RestoreThread(self.tstate); // Update counts of PyObjects / Py that were cloned or dropped while the GIL was released. + #[cfg(not(pyo3_disable_reference_pool))] POOL.update_counts(Python::assume_gil_acquired()); } } @@ -376,6 +371,7 @@ impl GILPool { pub unsafe fn new() -> GILPool { increment_gil_count(); // Update counts of PyObjects / Py that have been cloned or dropped since last acquisition + #[cfg(not(pyo3_disable_reference_pool))] POOL.update_counts(Python::assume_gil_acquired()); GILPool { start: OWNED_OBJECTS @@ -434,11 +430,13 @@ impl Drop for GILPool { /// /// # Safety /// The object must be an owned Python reference. +#[cfg(feature = "py-clone")] +#[track_caller] pub unsafe fn register_incref(obj: NonNull) { if gil_is_acquired() { ffi::Py_INCREF(obj.as_ptr()) } else { - POOL.register_incref(obj); + panic!("Cannot clone pointer into Python heap without the GIL being held."); } } @@ -450,11 +448,21 @@ pub unsafe fn register_incref(obj: NonNull) { /// /// # Safety /// The object must be an owned Python reference. +#[track_caller] pub unsafe fn register_decref(obj: NonNull) { if gil_is_acquired() { ffi::Py_DECREF(obj.as_ptr()) } else { + #[cfg(not(pyo3_disable_reference_pool))] POOL.register_decref(obj); + #[cfg(all( + pyo3_disable_reference_pool, + not(pyo3_leak_on_drop_without_reference_pool) + ))] + { + let _trap = PanicTrap::new("Aborting the process to avoid panic-from-drop."); + panic!("Cannot drop pointer into Python heap without the GIL being held."); + } } } @@ -508,15 +516,18 @@ fn decrement_gil_count() { mod tests { #[allow(deprecated)] use super::GILPool; - use super::{gil_is_acquired, GIL_COUNT, POOL}; + #[cfg(not(pyo3_disable_reference_pool))] + use super::POOL; + use super::{gil_is_acquired, GIL_COUNT}; + #[cfg(not(pyo3_disable_reference_pool))] + use crate::ffi; use crate::types::any::PyAnyMethods; - use crate::{ffi, PyObject, Python}; + use crate::{PyObject, Python}; #[cfg(feature = "gil-refs")] use {super::OWNED_OBJECTS, crate::gil}; + #[cfg(not(pyo3_disable_reference_pool))] use std::ptr::NonNull; - #[cfg(not(target_arch = "wasm32"))] - use std::sync; fn get_object(py: Python<'_>) -> PyObject { py.eval_bound("object()", None, None).unwrap().unbind() @@ -531,30 +542,20 @@ mod tests { len } - fn pool_inc_refs_does_not_contain(obj: &PyObject) -> bool { - !POOL - .pointer_ops - .lock() - .unwrap() - .0 - .contains(&unsafe { NonNull::new_unchecked(obj.as_ptr()) }) - } - + #[cfg(not(pyo3_disable_reference_pool))] fn pool_dec_refs_does_not_contain(obj: &PyObject) -> bool { !POOL - .pointer_ops + .pending_decrefs .lock() .unwrap() - .1 .contains(&unsafe { NonNull::new_unchecked(obj.as_ptr()) }) } - #[cfg(not(target_arch = "wasm32"))] + #[cfg(all(not(pyo3_disable_reference_pool), not(target_arch = "wasm32")))] fn pool_dec_refs_contains(obj: &PyObject) -> bool { - POOL.pointer_ops + POOL.pending_decrefs .lock() .unwrap() - .1 .contains(&unsafe { NonNull::new_unchecked(obj.as_ptr()) }) } @@ -629,20 +630,20 @@ mod tests { let reference = obj.clone_ref(py); assert_eq!(obj.get_refcnt(py), 2); - assert!(pool_inc_refs_does_not_contain(&obj)); + #[cfg(not(pyo3_disable_reference_pool))] assert!(pool_dec_refs_does_not_contain(&obj)); // With the GIL held, reference count will be decreased immediately. drop(reference); assert_eq!(obj.get_refcnt(py), 1); - assert!(pool_inc_refs_does_not_contain(&obj)); + #[cfg(not(pyo3_disable_reference_pool))] assert!(pool_dec_refs_does_not_contain(&obj)); }); } #[test] - #[cfg(not(target_arch = "wasm32"))] // We are building wasm Python with pthreads disabled + #[cfg(all(not(pyo3_disable_reference_pool), not(target_arch = "wasm32")))] // We are building wasm Python with pthreads disabled fn test_pyobject_drop_without_gil_doesnt_decrease_refcnt() { let obj = Python::with_gil(|py| { let obj = get_object(py); @@ -650,7 +651,6 @@ mod tests { let reference = obj.clone_ref(py); assert_eq!(obj.get_refcnt(py), 2); - assert!(pool_inc_refs_does_not_contain(&obj)); assert!(pool_dec_refs_does_not_contain(&obj)); // Drop reference in a separate thread which doesn't have the GIL. @@ -659,7 +659,6 @@ mod tests { // The reference count should not have changed (the GIL has always // been held by this thread), it is remembered to release later. assert_eq!(obj.get_refcnt(py), 2); - assert!(pool_inc_refs_does_not_contain(&obj)); assert!(pool_dec_refs_contains(&obj)); obj }); @@ -667,9 +666,7 @@ mod tests { // Next time the GIL is acquired, the reference is released Python::with_gil(|py| { assert_eq!(obj.get_refcnt(py), 1); - let non_null = unsafe { NonNull::new_unchecked(obj.as_ptr()) }; - assert!(!POOL.pointer_ops.lock().unwrap().0.contains(&non_null)); - assert!(!POOL.pointer_ops.lock().unwrap().1.contains(&non_null)); + assert!(pool_dec_refs_does_not_contain(&obj)); }); } @@ -725,19 +722,16 @@ mod tests { assert!(!gil_is_acquired()); } + #[cfg(feature = "py-clone")] #[test] + #[should_panic] fn test_allow_threads_updates_refcounts() { Python::with_gil(|py| { // Make a simple object with 1 reference let obj = get_object(py); assert!(obj.get_refcnt(py) == 1); - // Clone the object without the GIL to use internal tracking - let escaped_ref = py.allow_threads(|| obj.clone()); - // But after the block the refcounts are updated - assert!(obj.get_refcnt(py) == 2); - drop(escaped_ref); - assert!(obj.get_refcnt(py) == 1); - drop(obj); + // Clone the object without the GIL which should panic + py.allow_threads(|| obj.clone()); }); } @@ -752,6 +746,7 @@ mod tests { }) } + #[cfg(feature = "py-clone")] #[test] fn test_clone_with_gil() { Python::with_gil(|py| { @@ -765,147 +760,8 @@ mod tests { }) } - #[cfg(not(target_arch = "wasm32"))] - struct Event { - set: sync::Mutex, - wait: sync::Condvar, - } - - #[cfg(not(target_arch = "wasm32"))] - impl Event { - const fn new() -> Self { - Self { - set: sync::Mutex::new(false), - wait: sync::Condvar::new(), - } - } - - fn set(&self) { - *self.set.lock().unwrap() = true; - self.wait.notify_all(); - } - - fn wait(&self) { - drop( - self.wait - .wait_while(self.set.lock().unwrap(), |s| !*s) - .unwrap(), - ); - } - } - - #[test] - #[cfg(not(target_arch = "wasm32"))] // We are building wasm Python with pthreads disabled - fn test_clone_without_gil() { - use crate::{Py, PyAny}; - use std::{sync::Arc, thread}; - - // Some events for synchronizing - static GIL_ACQUIRED: Event = Event::new(); - static OBJECT_CLONED: Event = Event::new(); - static REFCNT_CHECKED: Event = Event::new(); - - Python::with_gil(|py| { - let obj: Arc> = Arc::new(get_object(py)); - let thread_obj = Arc::clone(&obj); - - let count = obj.get_refcnt(py); - println!( - "1: The object has been created and its reference count is {}", - count - ); - - let handle = thread::spawn(move || { - Python::with_gil(move |py| { - println!("3. The GIL has been acquired on another thread."); - GIL_ACQUIRED.set(); - - // Wait while the main thread registers obj in POOL - OBJECT_CLONED.wait(); - println!("5. Checking refcnt"); - assert_eq!(thread_obj.get_refcnt(py), count); - - REFCNT_CHECKED.set(); - }) - }); - - let cloned = py.allow_threads(|| { - println!("2. The GIL has been released."); - - // Wait until the GIL has been acquired on the thread. - GIL_ACQUIRED.wait(); - - println!("4. The other thread is now hogging the GIL, we clone without it held"); - // Cloning without GIL should not update reference count - let cloned = Py::clone(&*obj); - OBJECT_CLONED.set(); - cloned - }); - - REFCNT_CHECKED.wait(); - - println!("6. The main thread has acquired the GIL again and processed the pool."); - - // Total reference count should be one higher - assert_eq!(obj.get_refcnt(py), count + 1); - - // Clone dropped - drop(cloned); - // Ensure refcount of the arc is 1 - handle.join().unwrap(); - - // Overall count is now back to the original, and should be no pending change - assert_eq!(Arc::try_unwrap(obj).unwrap().get_refcnt(py), count); - }); - } - - #[test] - #[cfg(not(target_arch = "wasm32"))] // We are building wasm Python with pthreads disabled - fn test_clone_in_other_thread() { - use crate::Py; - use std::{sync::Arc, thread}; - - // Some events for synchronizing - static OBJECT_CLONED: Event = Event::new(); - - let (obj, count, ptr) = Python::with_gil(|py| { - let obj = Arc::new(get_object(py)); - let count = obj.get_refcnt(py); - let thread_obj = Arc::clone(&obj); - - // Start a thread which does not have the GIL, and clone it - let t = thread::spawn(move || { - // Cloning without GIL should not update reference count - #[allow(clippy::redundant_clone)] - let _ = Py::clone(&*thread_obj); - OBJECT_CLONED.set(); - }); - - OBJECT_CLONED.wait(); - assert_eq!(count, obj.get_refcnt(py)); - - t.join().unwrap(); - let ptr = NonNull::new(obj.as_ptr()).unwrap(); - - // The pointer should appear once in the incref pool, and once in the - // decref pool (for the clone being created and also dropped) - assert!(POOL.pointer_ops.lock().unwrap().0.contains(&ptr)); - assert!(POOL.pointer_ops.lock().unwrap().1.contains(&ptr)); - - (obj, count, ptr) - }); - - Python::with_gil(|py| { - // Acquiring the gil clears the pool - assert!(!POOL.pointer_ops.lock().unwrap().0.contains(&ptr)); - assert!(!POOL.pointer_ops.lock().unwrap().1.contains(&ptr)); - - // Overall count is still unchanged - assert_eq!(count, obj.get_refcnt(py)); - }); - } - #[test] + #[cfg(not(pyo3_disable_reference_pool))] fn test_update_counts_does_not_deadlock() { // update_counts can run arbitrary Python code during Py_DECREF. // if the locking is implemented incorrectly, it will deadlock. diff --git a/src/instance.rs b/src/instance.rs index 1c510b90be5..7835b9c79b6 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -81,10 +81,11 @@ where /// struct Foo {/* fields omitted */} /// /// # fn main() -> PyResult<()> { - /// Python::with_gil(|py| -> PyResult> { + /// let foo: Py = Python::with_gil(|py| -> PyResult<_> { /// let foo: Bound<'_, Foo> = Bound::new(py, Foo {})?; /// Ok(foo.into()) /// })?; + /// # Python::with_gil(move |_py| drop(foo)); /// # Ok(()) /// # } /// ``` @@ -865,7 +866,9 @@ impl IntoPy for Borrowed<'_, '_, T> { /// // All of these are valid syntax /// let second = Py::clone_ref(&first, py); /// let third = first.clone_ref(py); +/// #[cfg(feature = "py-clone")] /// let fourth = Py::clone(&first); +/// #[cfg(feature = "py-clone")] /// let fifth = first.clone(); /// /// // Disposing of our original `Py` just decrements the reference count. @@ -873,7 +876,9 @@ impl IntoPy for Borrowed<'_, '_, T> { /// /// // They all point to the same object /// assert!(second.is(&third)); +/// #[cfg(feature = "py-clone")] /// assert!(fourth.is(&fifth)); +/// #[cfg(feature = "py-clone")] /// assert!(second.is(&fourth)); /// }); /// # } @@ -935,10 +940,11 @@ where /// struct Foo {/* fields omitted */} /// /// # fn main() -> PyResult<()> { - /// Python::with_gil(|py| -> PyResult> { + /// let foo = Python::with_gil(|py| -> PyResult<_> { /// let foo: Py = Py::new(py, Foo {})?; /// Ok(foo) /// })?; + /// # Python::with_gil(move |_py| drop(foo)); /// # Ok(()) /// # } /// ``` @@ -1244,6 +1250,7 @@ where /// }); /// /// cell.get().value.fetch_add(1, Ordering::Relaxed); + /// # Python::with_gil(move |_py| drop(cell)); /// ``` #[inline] pub fn get(&self) -> &T @@ -1804,9 +1811,12 @@ where } /// If the GIL is held this increments `self`'s reference count. -/// Otherwise this registers the [`Py`]`` instance to have its reference count -/// incremented the next time PyO3 acquires the GIL. +/// Otherwise, it will panic. +/// +/// Only available if the `py-clone` feature is enabled. +#[cfg(feature = "py-clone")] impl Clone for Py { + #[track_caller] fn clone(&self) -> Self { unsafe { gil::register_incref(self.0); @@ -1815,8 +1825,16 @@ impl Clone for Py { } } -/// Dropping a `Py` instance decrements the reference count on the object by 1. +/// Dropping a `Py` instance decrements the reference count +/// on the object by one if the GIL is held. +/// +/// Otherwise and by default, this registers the underlying pointer to have its reference count +/// decremented the next time PyO3 acquires the GIL. +/// +/// However, if the `pyo3_disable_reference_pool` conditional compilation flag +/// is enabled, it will abort the process. impl Drop for Py { + #[track_caller] fn drop(&mut self) { unsafe { gil::register_decref(self.0); @@ -2039,7 +2057,9 @@ mod tests { Py::from(native) }); - assert_eq!(Python::with_gil(|py| dict.get_refcnt(py)), 1); + Python::with_gil(move |py| { + assert_eq!(dict.get_refcnt(py), 1); + }); } #[test] diff --git a/src/marker.rs b/src/marker.rs index b6821deb036..072b882c875 100644 --- a/src/marker.rs +++ b/src/marker.rs @@ -1178,7 +1178,6 @@ impl<'unbound> Python<'unbound> { mod tests { use super::*; use crate::types::{IntoPyDict, PyList}; - use std::sync::Arc; #[test] fn test_eval() { @@ -1264,11 +1263,12 @@ mod tests { }); } + #[cfg(not(pyo3_disable_reference_pool))] #[test] fn test_allow_threads_pass_stuff_in() { let list = Python::with_gil(|py| PyList::new_bound(py, vec!["foo", "bar"]).unbind()); let mut v = vec![1, 2, 3]; - let a = Arc::new(String::from("foo")); + let a = std::sync::Arc::new(String::from("foo")); Python::with_gil(|py| { py.allow_threads(|| { diff --git a/src/pybacked.rs b/src/pybacked.rs index e0bacb86144..ea5face516b 100644 --- a/src/pybacked.rs +++ b/src/pybacked.rs @@ -13,7 +13,7 @@ use crate::{ /// A wrapper around `str` where the storage is owned by a Python `bytes` or `str` object. /// /// This type gives access to the underlying data via a `Deref` implementation. -#[derive(Clone)] +#[cfg_attr(feature = "py-clone", derive(Clone))] pub struct PyBackedStr { #[allow(dead_code)] // only held so that the storage is not dropped storage: Py, @@ -88,7 +88,7 @@ impl FromPyObject<'_> for PyBackedStr { /// A wrapper around `[u8]` where the storage is either owned by a Python `bytes` object, or a Rust `Box<[u8]>`. /// /// This type gives access to the underlying data via a `Deref` implementation. -#[derive(Clone)] +#[cfg_attr(feature = "py-clone", derive(Clone))] pub struct PyBackedBytes { #[allow(dead_code)] // only held so that the storage is not dropped storage: PyBackedBytesStorage, @@ -96,7 +96,7 @@ pub struct PyBackedBytes { } #[allow(dead_code)] -#[derive(Clone)] +#[cfg_attr(feature = "py-clone", derive(Clone))] enum PyBackedBytesStorage { Python(Py), Rust(Arc<[u8]>), @@ -336,6 +336,7 @@ mod test { is_sync::(); } + #[cfg(feature = "py-clone")] #[test] fn test_backed_str_clone() { Python::with_gil(|py| { @@ -398,6 +399,7 @@ mod test { }) } + #[cfg(feature = "py-clone")] #[test] fn test_backed_bytes_from_bytes_clone() { Python::with_gil(|py| { @@ -410,6 +412,7 @@ mod test { }); } + #[cfg(feature = "py-clone")] #[test] fn test_backed_bytes_from_bytearray_clone() { Python::with_gil(|py| { diff --git a/src/tests/hygiene/pyclass.rs b/src/tests/hygiene/pyclass.rs index 0bdb280d066..34b30a8c6f4 100644 --- a/src/tests/hygiene/pyclass.rs +++ b/src/tests/hygiene/pyclass.rs @@ -25,7 +25,7 @@ pub struct Bar { a: u8, #[pyo3(get, set)] b: Foo, - #[pyo3(get, set)] + #[pyo3(set)] c: ::std::option::Option>, } diff --git a/src/types/capsule.rs b/src/types/capsule.rs index 2851970ba10..9b9445cdffe 100644 --- a/src/types/capsule.rs +++ b/src/types/capsule.rs @@ -489,7 +489,7 @@ mod tests { cap.into() }); - Python::with_gil(|py| { + Python::with_gil(move |py| { let f = unsafe { cap.bind(py).reference:: u32>() }; assert_eq!(f(123), 123); }); @@ -553,7 +553,7 @@ mod tests { cap.into() }); - Python::with_gil(|py| { + Python::with_gil(move |py| { let ctx: &Vec = unsafe { cap.bind(py).reference() }; assert_eq!(ctx, &[1, 2, 3, 4]); }) @@ -572,7 +572,7 @@ mod tests { cap.into() }); - Python::with_gil(|py| { + Python::with_gil(move |py| { let ctx_ptr: *mut c_void = cap.bind(py).context().unwrap(); let ctx = unsafe { *Box::from_raw(ctx_ptr.cast::<&Vec>()) }; assert_eq!(ctx, &vec![1_u8, 2, 3, 4]); @@ -589,7 +589,7 @@ mod tests { context.send(true).unwrap(); } - Python::with_gil(|py| { + Python::with_gil(move |py| { let name = CString::new("foo").unwrap(); let cap = PyCapsule::new_bound_with_destructor(py, 0, Some(name), destructor).unwrap(); cap.set_context(Box::into_raw(Box::new(tx)).cast()).unwrap(); diff --git a/src/types/iterator.rs b/src/types/iterator.rs index 4562efde3f8..1835f484adf 100644 --- a/src/types/iterator.rs +++ b/src/types/iterator.rs @@ -195,7 +195,7 @@ mod tests { ); }); - Python::with_gil(|py| { + Python::with_gil(move |py| { assert_eq!(count, obj.get_refcnt(py)); }); } diff --git a/src/types/sequence.rs b/src/types/sequence.rs index f75d851973d..a5765ebc8b2 100644 --- a/src/types/sequence.rs +++ b/src/types/sequence.rs @@ -823,7 +823,7 @@ mod tests { assert!(seq.get_item(1).unwrap().as_ptr() == obj.as_ptr()); }); - Python::with_gil(|py| { + Python::with_gil(move |py| { assert_eq!(1, obj.get_refcnt(py)); }); } diff --git a/tests/test_buffer_protocol.rs b/tests/test_buffer_protocol.rs index dca900808a8..700bcdcd206 100644 --- a/tests/test_buffer_protocol.rs +++ b/tests/test_buffer_protocol.rs @@ -122,7 +122,7 @@ fn test_releasebuffer_unraisable_error() { let capture = UnraisableCapture::install(py); let instance = Py::new(py, ReleaseBufferError {}).unwrap(); - let env = [("ob", instance.clone())].into_py_dict_bound(py); + let env = [("ob", instance.clone_ref(py))].into_py_dict_bound(py); assert!(capture.borrow(py).capture.is_none()); diff --git a/tests/test_bytes.rs b/tests/test_bytes.rs index a3f1e2fcafe..5adca3f154a 100644 --- a/tests/test_bytes.rs +++ b/tests/test_bytes.rs @@ -48,4 +48,6 @@ fn test_py_as_bytes() { let data = Python::with_gil(|py| pyobj.as_bytes(py)); assert_eq!(data, b"abc"); + + Python::with_gil(move |_py| drop(pyobj)); } diff --git a/tests/test_class_basics.rs b/tests/test_class_basics.rs index b7bee2638ca..d0e745377c8 100644 --- a/tests/test_class_basics.rs +++ b/tests/test_class_basics.rs @@ -172,6 +172,7 @@ fn empty_class_in_module() { }); } +#[cfg(feature = "py-clone")] #[pyclass] struct ClassWithObjectField { // It used to be that PyObject was not supported with (get, set) @@ -180,6 +181,7 @@ struct ClassWithObjectField { value: PyObject, } +#[cfg(feature = "py-clone")] #[pymethods] impl ClassWithObjectField { #[new] @@ -188,6 +190,7 @@ impl ClassWithObjectField { } } +#[cfg(feature = "py-clone")] #[test] fn class_with_object_field() { Python::with_gil(|py| { @@ -229,7 +232,7 @@ impl UnsendableChild { } fn test_unsendable() -> PyResult<()> { - let obj = Python::with_gil(|py| -> PyResult<_> { + let (keep_obj_here, obj) = Python::with_gil(|py| -> PyResult<_> { let obj: Py = PyType::new_bound::(py).call1((5,))?.extract()?; // Accessing the value inside this thread should not panic @@ -241,14 +244,13 @@ fn test_unsendable() -> PyResult<()> { .is_err(); assert!(!caught_panic); - Ok(obj) - })?; - let keep_obj_here = obj.clone(); + Ok((obj.clone_ref(py), obj)) + })?; let caught_panic = std::thread::spawn(move || { // This access must panic - Python::with_gil(|py| { + Python::with_gil(move |py| { obj.borrow(py); }); }) @@ -549,6 +551,8 @@ fn access_frozen_class_without_gil() { }); assert_eq!(py_counter.get().value.load(Ordering::Relaxed), 1); + + Python::with_gil(move |_py| drop(py_counter)); } #[test] diff --git a/tests/test_class_conversion.rs b/tests/test_class_conversion.rs index ede8928f865..a46132b9586 100644 --- a/tests/test_class_conversion.rs +++ b/tests/test_class_conversion.rs @@ -54,12 +54,14 @@ impl SubClass { } } +#[cfg(feature = "py-clone")] #[pyclass] struct PolymorphicContainer { #[pyo3(get, set)] inner: Py, } +#[cfg(feature = "py-clone")] #[test] fn test_polymorphic_container_stores_base_class() { Python::with_gil(|py| { @@ -76,6 +78,7 @@ fn test_polymorphic_container_stores_base_class() { }); } +#[cfg(feature = "py-clone")] #[test] fn test_polymorphic_container_stores_sub_class() { Python::with_gil(|py| { @@ -103,6 +106,7 @@ fn test_polymorphic_container_stores_sub_class() { }); } +#[cfg(feature = "py-clone")] #[test] fn test_polymorphic_container_does_not_accept_other_types() { Python::with_gil(|py| { diff --git a/tests/test_gc.rs b/tests/test_gc.rs index 11d9221c7ad..b95abd4adea 100644 --- a/tests/test_gc.rs +++ b/tests/test_gc.rs @@ -445,6 +445,7 @@ impl DropDuringTraversal { } } +#[cfg(not(pyo3_disable_reference_pool))] #[test] fn drop_during_traversal_with_gil() { let drop_called = Arc::new(AtomicBool::new(false)); @@ -476,6 +477,7 @@ fn drop_during_traversal_with_gil() { assert!(drop_called.load(Ordering::Relaxed)); } +#[cfg(not(pyo3_disable_reference_pool))] #[test] fn drop_during_traversal_without_gil() { let drop_called = Arc::new(AtomicBool::new(false)); diff --git a/tests/test_methods.rs b/tests/test_methods.rs index 37f3b2d8bd6..615e2dba0af 100644 --- a/tests/test_methods.rs +++ b/tests/test_methods.rs @@ -874,6 +874,7 @@ fn test_from_sequence() { }); } +#[cfg(feature = "py-clone")] #[pyclass] struct r#RawIdents { #[pyo3(get, set)] @@ -882,6 +883,7 @@ struct r#RawIdents { r#subsubtype: PyObject, } +#[cfg(feature = "py-clone")] #[pymethods] impl r#RawIdents { #[new] @@ -946,6 +948,7 @@ impl r#RawIdents { } } +#[cfg(feature = "py-clone")] #[test] fn test_raw_idents() { Python::with_gil(|py| { diff --git a/tests/test_no_imports.rs b/tests/test_no_imports.rs index 3509a11f4be..89d54f4e057 100644 --- a/tests/test_no_imports.rs +++ b/tests/test_no_imports.rs @@ -143,12 +143,14 @@ fn test_basic() { }); } +#[cfg(feature = "py-clone")] #[pyo3::pyclass] struct NewClassMethod { #[pyo3(get)] cls: pyo3::PyObject, } +#[cfg(feature = "py-clone")] #[pyo3::pymethods] impl NewClassMethod { #[new] @@ -160,6 +162,7 @@ impl NewClassMethod { } } +#[cfg(feature = "py-clone")] #[test] fn test_new_class_method() { pyo3::Python::with_gil(|py| { diff --git a/tests/test_sequence.rs b/tests/test_sequence.rs index 9627f06ca75..8adba35c86a 100644 --- a/tests/test_sequence.rs +++ b/tests/test_sequence.rs @@ -248,12 +248,14 @@ fn test_inplace_repeat() { // Check that #[pyo3(get, set)] works correctly for Vec +#[cfg(feature = "py-clone")] #[pyclass] struct GenericList { #[pyo3(get, set)] items: Vec, } +#[cfg(feature = "py-clone")] #[test] fn test_generic_list_get() { Python::with_gil(|py| { @@ -266,6 +268,7 @@ fn test_generic_list_get() { }); } +#[cfg(feature = "py-clone")] #[test] fn test_generic_list_set() { Python::with_gil(|py| { diff --git a/tests/test_serde.rs b/tests/test_serde.rs index f1d5bee4bee..9e97946b5f2 100644 --- a/tests/test_serde.rs +++ b/tests/test_serde.rs @@ -11,7 +11,7 @@ mod test_serde { } #[pyclass] - #[derive(Debug, Clone, Serialize, Deserialize)] + #[derive(Debug, Serialize, Deserialize)] struct User { username: String, group: Option>, @@ -27,7 +27,8 @@ mod test_serde { }; let friend2 = User { username: "friend 2".into(), - ..friend1.clone() + group: None, + friends: vec![], }; let user = Python::with_gil(|py| { From 57500d9b090ae5249bb69943f22ceb22a03e1dea Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Sat, 11 May 2024 12:48:38 -0400 Subject: [PATCH 073/495] Updates comments regarding the reference pool that were inaccurate (#4176) --- src/gil.rs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/gil.rs b/src/gil.rs index d4b92a4cff4..db8311ab2fc 100644 --- a/src/gil.rs +++ b/src/gil.rs @@ -236,7 +236,7 @@ impl Drop for GILGuard { type PyObjVec = Vec>; #[cfg(not(pyo3_disable_reference_pool))] -/// Thread-safe storage for objects which were inc_ref / dec_ref while the GIL was not held. +/// Thread-safe storage for objects which were dec_ref while the GIL was not held. struct ReferencePool { pending_decrefs: sync::Mutex, } @@ -422,11 +422,8 @@ impl Drop for GILPool { } } -/// Registers a Python object pointer inside the release pool, to have its reference count increased -/// the next time the GIL is acquired in pyo3. -/// -/// If the GIL is held, the reference count will be increased immediately instead of being queued -/// for later. +/// Increments the reference count of a Python object if the GIL is held. If +/// the GIL is not held, this function will panic. /// /// # Safety /// The object must be an owned Python reference. From 10152a7078b6cd3470bd8400144629b94f74c59e Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Sun, 12 May 2024 20:30:08 +0200 Subject: [PATCH 074/495] feature gate `PyCell` (#4177) * feature gate `PyCell` * feature gate `HasPyGilRef` completely * bump version --- Cargo.toml | 10 +-- guide/src/class.md | 1 + guide/src/migration.md | 3 +- pyo3-build-config/Cargo.toml | 2 +- pyo3-ffi/Cargo.toml | 4 +- pyo3-macros-backend/Cargo.toml | 5 +- pyo3-macros-backend/src/module.rs | 7 +- pyo3-macros-backend/src/pyclass.rs | 10 ++- pyo3-macros/Cargo.toml | 5 +- pyproject.toml | 2 +- src/conversion.rs | 7 +- src/err/mod.rs | 9 ++- src/exceptions.rs | 1 + src/impl_/deprecations.rs | 1 + src/impl_/pyclass.rs | 10 ++- src/instance.rs | 22 +++--- src/lib.rs | 12 ++-- src/macros.rs | 1 + src/prelude.rs | 2 + src/pycell.rs | 76 +++++++++++--------- src/pycell/impl_.rs | 3 + src/pyclass.rs | 13 ++++ src/type_object.rs | 74 ++++++++++++++++++- src/types/any.rs | 2 +- src/types/mod.rs | 3 + tests/test_compile_error.rs | 4 +- tests/ui/deprecations.stderr | 6 ++ tests/ui/invalid_intern_arg.stderr | 4 +- tests/ui/invalid_pymethods_duplicates.stderr | 17 ----- tests/ui/wrong_aspyref_lifetimes.stderr | 2 +- 30 files changed, 220 insertions(+), 98 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index aba8fd1bc98..921e551751c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3" -version = "0.21.2" +version = "0.22.0-dev" description = "Bindings to Python interpreter" authors = ["PyO3 Project and Contributors "] readme = "README.md" @@ -20,10 +20,10 @@ libc = "0.2.62" 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.21.2" } +pyo3-ffi = { path = "pyo3-ffi", version = "=0.22.0-dev" } # support crates for macros feature -pyo3-macros = { path = "pyo3-macros", version = "=0.21.2", optional = true } +pyo3-macros = { path = "pyo3-macros", version = "=0.22.0-dev", optional = true } indoc = { version = "2.0.1", optional = true } unindent = { version = "0.2.1", optional = true } @@ -62,7 +62,7 @@ rayon = "1.6.1" futures = "0.3.28" [build-dependencies] -pyo3-build-config = { path = "pyo3-build-config", version = "=0.21.2", features = ["resolve-config"] } +pyo3-build-config = { path = "pyo3-build-config", version = "=0.22.0-dev", features = ["resolve-config"] } [features] default = ["macros"] @@ -106,7 +106,7 @@ generate-import-lib = ["pyo3-ffi/generate-import-lib"] auto-initialize = [] # Allows use of the deprecated "GIL Refs" APIs. -gil-refs = [] +gil-refs = ["pyo3-macros/gil-refs"] # Enables `Clone`ing references to Python objects `Py` which panics if the GIL is not held. py-clone = [] diff --git a/guide/src/class.md b/guide/src/class.md index 91a6fb2c495..ce86ec40e5f 100644 --- a/guide/src/class.md +++ b/guide/src/class.md @@ -1307,6 +1307,7 @@ struct MyClass { impl pyo3::types::DerefToPyAny for MyClass {} # #[allow(deprecated)] +# #[cfg(feature = "gil-refs")] unsafe impl pyo3::type_object::HasPyGilRef for MyClass { type AsRefTarget = pyo3::PyCell; } diff --git a/guide/src/migration.md b/guide/src/migration.md index 10b62002a02..f56db2a5fc7 100644 --- a/guide/src/migration.md +++ b/guide/src/migration.md @@ -1609,7 +1609,7 @@ For more, see [the constructor section](class.md#constructor) of this guide.
Click to expand -PyO3 0.9 introduces [`PyCell`], which is a [`RefCell`]-like object wrapper +PyO3 0.9 introduces `PyCell`, which is a [`RefCell`]-like object wrapper for ensuring Rust's rules regarding aliasing of references are upheld. For more detail, see the [Rust Book's section on Rust's rules of references](https://doc.rust-lang.org/book/ch04-02-references-and-borrowing.html#the-rules-of-references) @@ -1788,7 +1788,6 @@ impl PySequenceProtocol for ByteSequence { [`FromPyObject`]: {{#PYO3_DOCS_URL}}/pyo3/conversion/trait.FromPyObject.html [`PyAny`]: {{#PYO3_DOCS_URL}}/pyo3/types/struct.PyAny.html -[`PyCell`]: {{#PYO3_DOCS_URL}}/pyo3/pycell/struct.PyCell.html [`PyBorrowMutError`]: {{#PYO3_DOCS_URL}}/pyo3/pycell/struct.PyBorrowMutError.html [`PyRef`]: {{#PYO3_DOCS_URL}}/pyo3/pycell/struct.PyRef.html [`PyRefMut`]: {{#PYO3_DOCS_URL}}/pyo3/pycell/struct.PyRef.html diff --git a/pyo3-build-config/Cargo.toml b/pyo3-build-config/Cargo.toml index 60bf9a13ef9..600237f8646 100644 --- a/pyo3-build-config/Cargo.toml +++ b/pyo3-build-config/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3-build-config" -version = "0.21.2" +version = "0.22.0-dev" 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 8f7767254f1..865da93926a 100644 --- a/pyo3-ffi/Cargo.toml +++ b/pyo3-ffi/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3-ffi" -version = "0.21.2" +version = "0.22.0-dev" description = "Python-API bindings for the PyO3 ecosystem" authors = ["PyO3 Project and Contributors "] keywords = ["pyo3", "python", "cpython", "ffi"] @@ -38,7 +38,7 @@ abi3-py312 = ["abi3", "pyo3-build-config/abi3-py312"] generate-import-lib = ["pyo3-build-config/python3-dll-a"] [build-dependencies] -pyo3-build-config = { path = "../pyo3-build-config", version = "=0.21.2", features = ["resolve-config"] } +pyo3-build-config = { path = "../pyo3-build-config", version = "=0.22.0-dev", features = ["resolve-config"] } [lints] workspace = true diff --git a/pyo3-macros-backend/Cargo.toml b/pyo3-macros-backend/Cargo.toml index c2ffd53b0fc..7bc0f6a2da1 100644 --- a/pyo3-macros-backend/Cargo.toml +++ b/pyo3-macros-backend/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3-macros-backend" -version = "0.21.2" +version = "0.22.0-dev" description = "Code generation for PyO3 package" authors = ["PyO3 Project and Contributors "] keywords = ["pyo3", "python", "cpython", "ffi"] @@ -16,7 +16,7 @@ edition = "2021" [dependencies] heck = "0.5" proc-macro2 = { version = "1", default-features = false } -pyo3-build-config = { path = "../pyo3-build-config", version = "=0.21.2", features = ["resolve-config"] } +pyo3-build-config = { path = "../pyo3-build-config", version = "=0.22.0-dev", features = ["resolve-config"] } quote = { version = "1", default-features = false } [dependencies.syn] @@ -29,3 +29,4 @@ workspace = true [features] experimental-async = [] +gil-refs = [] diff --git a/pyo3-macros-backend/src/module.rs b/pyo3-macros-backend/src/module.rs index 626cde121e6..756037263e3 100644 --- a/pyo3-macros-backend/src/module.rs +++ b/pyo3-macros-backend/src/module.rs @@ -384,6 +384,11 @@ fn process_functions_in_module(options: &PyModuleOptions, func: &mut syn::ItemFn let Ctx { pyo3_path } = ctx; let mut stmts: Vec = Vec::new(); + #[cfg(feature = "gil-refs")] + let imports = quote!(use #pyo3_path::{PyNativeType, types::PyModuleMethods};); + #[cfg(not(feature = "gil-refs"))] + let imports = quote!(use #pyo3_path::types::PyModuleMethods;); + for mut stmt in func.block.stmts.drain(..) { if let syn::Stmt::Item(Item::Fn(func)) = &mut stmt { if let Some(pyfn_args) = get_pyfn_attr(&mut func.attrs)? { @@ -394,7 +399,7 @@ fn process_functions_in_module(options: &PyModuleOptions, func: &mut syn::ItemFn #wrapped_function { #[allow(unknown_lints, unused_imports, redundant_imports)] - use #pyo3_path::{PyNativeType, types::PyModuleMethods}; + #imports #module_name.as_borrowed().add_function(#pyo3_path::wrap_pyfunction!(#name, #module_name.as_borrowed())?)?; } }; diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index 179fe71bb9e..4e71a711802 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -1307,11 +1307,19 @@ fn impl_pytypeinfo( quote! { ::core::option::Option::None } }; - quote! { + #[cfg(feature = "gil-refs")] + let has_py_gil_ref = quote! { #[allow(deprecated)] unsafe impl #pyo3_path::type_object::HasPyGilRef for #cls { type AsRefTarget = #pyo3_path::PyCell; } + }; + + #[cfg(not(feature = "gil-refs"))] + let has_py_gil_ref = TokenStream::new(); + + quote! { + #has_py_gil_ref unsafe impl #pyo3_path::type_object::PyTypeInfo for #cls { const NAME: &'static str = #cls_name; diff --git a/pyo3-macros/Cargo.toml b/pyo3-macros/Cargo.toml index 690924c76a5..e4b550cfb8e 100644 --- a/pyo3-macros/Cargo.toml +++ b/pyo3-macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3-macros" -version = "0.21.2" +version = "0.22.0-dev" description = "Proc macros for PyO3 package" authors = ["PyO3 Project and Contributors "] keywords = ["pyo3", "python", "cpython", "ffi"] @@ -17,12 +17,13 @@ proc-macro = true multiple-pymethods = [] experimental-async = ["pyo3-macros-backend/experimental-async"] experimental-declarative-modules = [] +gil-refs = ["pyo3-macros-backend/gil-refs"] [dependencies] proc-macro2 = { version = "1", default-features = false } quote = "1" syn = { version = "2", features = ["full", "extra-traits"] } -pyo3-macros-backend = { path = "../pyo3-macros-backend", version = "=0.21.2" } +pyo3-macros-backend = { path = "../pyo3-macros-backend", version = "=0.22.0-dev" } [lints] workspace = true diff --git a/pyproject.toml b/pyproject.toml index 9a70116f301..a007ee6dc7a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ [tool.towncrier] filename = "CHANGELOG.md" -version = "0.21.2" +version = "0.22.0-dev" start_string = "\n" template = ".towncrier.template.md" title_format = "## [{version}] - {project_date}" diff --git a/src/conversion.rs b/src/conversion.rs index 95931d30d2e..44dbc3c7eed 100644 --- a/src/conversion.rs +++ b/src/conversion.rs @@ -5,14 +5,12 @@ use crate::inspect::types::TypeInfo; use crate::pyclass::boolean_struct::False; use crate::types::any::PyAnyMethods; use crate::types::PyTuple; -use crate::{ - ffi, Borrowed, Bound, Py, PyAny, PyClass, PyNativeType, PyObject, PyRef, PyRefMut, Python, -}; +use crate::{ffi, Borrowed, Bound, Py, PyAny, PyClass, PyObject, PyRef, PyRefMut, Python}; #[cfg(feature = "gil-refs")] use { crate::{ err::{self, PyDowncastError}, - gil, + gil, PyNativeType, }, std::ptr::NonNull, }; @@ -221,6 +219,7 @@ pub trait FromPyObject<'py>: Sized { /// /// Implementors are encouraged to implement `extract_bound` and leave this method as the /// default implementation, which will forward calls to `extract_bound`. + #[cfg(feature = "gil-refs")] fn extract(ob: &'py PyAny) -> PyResult { Self::extract_bound(&ob.as_borrowed()) } diff --git a/src/err/mod.rs b/src/err/mod.rs index 29d4ac36294..6bfe1a6cc99 100644 --- a/src/err/mod.rs +++ b/src/err/mod.rs @@ -3,11 +3,13 @@ use crate::panic::PanicException; use crate::type_object::PyTypeInfo; use crate::types::any::PyAnyMethods; use crate::types::{string::PyStringMethods, typeobject::PyTypeMethods, PyTraceback, PyType}; +#[cfg(feature = "gil-refs")] +use crate::PyNativeType; use crate::{ exceptions::{self, PyBaseException}, ffi, }; -use crate::{Borrowed, IntoPy, Py, PyAny, PyNativeType, PyObject, Python, ToPyObject}; +use crate::{Borrowed, IntoPy, Py, PyAny, PyObject, Python, ToPyObject}; use std::borrow::Cow; use std::cell::UnsafeCell; use std::ffi::CString; @@ -47,11 +49,13 @@ pub type PyResult = Result; /// Error that indicates a failure to convert a PyAny to a more specific Python type. #[derive(Debug)] +#[cfg(feature = "gil-refs")] pub struct PyDowncastError<'a> { from: &'a PyAny, to: Cow<'static, str>, } +#[cfg(feature = "gil-refs")] impl<'a> PyDowncastError<'a> { /// Create a new `PyDowncastError` representing a failure to convert the object /// `from` into the type named in `to`. @@ -64,7 +68,6 @@ impl<'a> PyDowncastError<'a> { /// Compatibility API to convert the Bound variant `DowncastError` into the /// gil-ref variant - #[cfg(feature = "gil-refs")] pub(crate) fn from_downcast_err(DowncastError { from, to }: DowncastError<'a, 'a>) -> Self { #[allow(deprecated)] let from = unsafe { from.py().from_borrowed_ptr(from.as_ptr()) }; @@ -1012,8 +1015,10 @@ impl<'a> std::convert::From> for PyErr { } } +#[cfg(feature = "gil-refs")] impl<'a> std::error::Error for PyDowncastError<'a> {} +#[cfg(feature = "gil-refs")] impl<'a> std::fmt::Display for PyDowncastError<'a> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { display_downcast_error(f, &self.from.as_borrowed(), &self.to) diff --git a/src/exceptions.rs b/src/exceptions.rs index b44a5c5a3fe..d6a6e859e3b 100644 --- a/src/exceptions.rs +++ b/src/exceptions.rs @@ -146,6 +146,7 @@ macro_rules! import_exception_bound { // FIXME remove this: was necessary while `PyTypeInfo` requires `HasPyGilRef`, // should change in 0.22. + #[cfg(feature = "gil-refs")] unsafe impl $crate::type_object::HasPyGilRef for $name { type AsRefTarget = $crate::PyAny; } diff --git a/src/impl_/deprecations.rs b/src/impl_/deprecations.rs index 650e01ce729..eb5caa8dffb 100644 --- a/src/impl_/deprecations.rs +++ b/src/impl_/deprecations.rs @@ -19,6 +19,7 @@ pub struct NotAGilRef(std::marker::PhantomData); pub trait IsGilRef {} +#[cfg(feature = "gil-refs")] impl IsGilRef for &'_ T {} impl GilRefs { diff --git a/src/impl_/pyclass.rs b/src/impl_/pyclass.rs index 1302834ca4b..3ec2e329e1a 100644 --- a/src/impl_/pyclass.rs +++ b/src/impl_/pyclass.rs @@ -1,3 +1,5 @@ +#[cfg(feature = "gil-refs")] +use crate::PyNativeType; use crate::{ exceptions::{PyAttributeError, PyNotImplementedError, PyRuntimeError, PyValueError}, ffi, @@ -7,8 +9,7 @@ use crate::{ pyclass_init::PyObjectInit, types::any::PyAnyMethods, types::PyBool, - Borrowed, Py, PyAny, PyClass, PyErr, PyMethodDefType, PyNativeType, PyResult, PyTypeInfo, - Python, + Borrowed, Py, PyAny, PyClass, PyErr, PyMethodDefType, PyResult, PyTypeInfo, Python, }; use std::{ borrow::Cow, @@ -168,7 +169,12 @@ pub trait PyClassImpl: Sized + 'static { /// The closest native ancestor. This is `PyAny` by default, and when you declare /// `#[pyclass(extends=PyDict)]`, it's `PyDict`. + #[cfg(feature = "gil-refs")] type BaseNativeType: PyTypeInfo + PyNativeType; + /// The closest native ancestor. This is `PyAny` by default, and when you declare + /// `#[pyclass(extends=PyDict)]`, it's `PyDict`. + #[cfg(not(feature = "gil-refs"))] + type BaseNativeType: PyTypeInfo; /// This handles following two situations: /// 1. In case `T` is `Send`, stub `ThreadChecker` is used and does nothing. diff --git a/src/instance.rs b/src/instance.rs index 7835b9c79b6..82b05e782ff 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -2,6 +2,7 @@ use crate::err::{self, PyErr, PyResult}; use crate::impl_::pycell::PyClassObject; use crate::pycell::{PyBorrowError, PyBorrowMutError}; use crate::pyclass::boolean_struct::{False, True}; +#[cfg(feature = "gil-refs")] use crate::type_object::HasPyGilRef; use crate::types::{any::PyAnyMethods, string::PyStringMethods, typeobject::PyTypeMethods}; use crate::types::{DerefToPyAny, PyDict, PyString, PyTuple}; @@ -24,6 +25,7 @@ use std::ptr::NonNull; /// # Safety /// /// This trait must only be implemented for types which cannot be accessed without the GIL. +#[cfg(feature = "gil-refs")] pub unsafe trait PyNativeType: Sized { /// The form of this which is stored inside a `Py` smart pointer. type AsRefSource: HasPyGilRef; @@ -666,11 +668,11 @@ impl<'a, 'py, T> From<&'a Bound<'py, T>> for Borrowed<'a, 'py, T> { } } +#[cfg(feature = "gil-refs")] impl<'py, T> Borrowed<'py, 'py, T> where T: HasPyGilRef, { - #[cfg(feature = "gil-refs")] pub(crate) fn into_gil_ref(self) -> &'py T::AsRefTarget { // Safety: self is a borrow over `'py`. #[allow(deprecated)] @@ -953,6 +955,7 @@ where } } +#[cfg(feature = "gil-refs")] impl Py where T: HasPyGilRef, @@ -1000,7 +1003,6 @@ where /// assert!(my_class_cell.try_borrow().is_ok()); /// }); /// ``` - #[cfg(feature = "gil-refs")] #[deprecated( since = "0.21.0", note = "use `obj.bind(py)` instead of `obj.as_ref(py)`" @@ -1053,7 +1055,6 @@ where /// obj.into_ref(py) /// } /// ``` - #[cfg(feature = "gil-refs")] #[deprecated( since = "0.21.0", note = "use `obj.into_bound(py)` instead of `obj.into_ref(py)`" @@ -1118,8 +1119,7 @@ where /// /// For frozen classes, the simpler [`get`][Self::get] is available. /// - /// Equivalent to `self.as_ref(py).borrow()` - - /// see [`PyCell::borrow`](crate::pycell::PyCell::borrow). + /// Equivalent to `self.bind(py).borrow()` - see [`Bound::borrow`]. /// /// # Examples /// @@ -1157,8 +1157,7 @@ where /// /// This borrow lasts while the returned [`PyRefMut`] exists. /// - /// Equivalent to `self.as_ref(py).borrow_mut()` - - /// see [`PyCell::borrow_mut`](crate::pycell::PyCell::borrow_mut). + /// Equivalent to `self.bind(py).borrow_mut()` - see [`Bound::borrow_mut`]. /// /// # Examples /// @@ -1202,8 +1201,7 @@ where /// /// For frozen classes, the simpler [`get`][Self::get] is available. /// - /// Equivalent to `self.as_ref(py).borrow_mut()` - - /// see [`PyCell::try_borrow`](crate::pycell::PyCell::try_borrow). + /// Equivalent to `self.bind(py).try_borrow()` - see [`Bound::try_borrow`]. #[inline] pub fn try_borrow<'py>(&'py self, py: Python<'py>) -> Result, PyBorrowError> { self.bind(py).try_borrow() @@ -1215,8 +1213,7 @@ where /// /// This is the non-panicking variant of [`borrow_mut`](#method.borrow_mut). /// - /// Equivalent to `self.as_ref(py).try_borrow_mut()` - - /// see [`PyCell::try_borrow_mut`](crate::pycell::PyCell::try_borrow_mut). + /// Equivalent to `self.bind(py).try_borrow_mut()` - see [`Bound::try_borrow_mut`]. #[inline] pub fn try_borrow_mut<'py>( &'py self, @@ -1742,6 +1739,7 @@ unsafe impl crate::AsPyPointer for Py { } } +#[cfg(feature = "gil-refs")] impl std::convert::From<&'_ T> for PyObject where T: PyNativeType, @@ -1866,6 +1864,7 @@ where /// /// However for GIL lifetime reasons, cause() cannot be implemented for `Py`. /// Use .as_ref() to get the GIL-scoped error if you need to inspect the cause. +#[cfg(feature = "gil-refs")] impl std::error::Error for Py where T: std::error::Error + PyTypeInfo, @@ -1876,7 +1875,6 @@ where impl std::fmt::Display for Py where T: PyTypeInfo, - T::AsRefTarget: std::fmt::Display, { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { Python::with_gil(|py| std::fmt::Display::fmt(self.bind(py), f)) diff --git a/src/lib.rs b/src/lib.rs index 2a40445222e..b1d8ae6c7cf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -320,15 +320,18 @@ pub use crate::conversion::{AsPyPointer, FromPyObject, IntoPy, ToPyObject}; #[cfg(feature = "gil-refs")] #[allow(deprecated)] pub use crate::conversion::{FromPyPointer, PyTryFrom, PyTryInto}; -pub use crate::err::{ - DowncastError, DowncastIntoError, PyDowncastError, PyErr, PyErrArguments, PyResult, ToPyErr, -}; +#[cfg(feature = "gil-refs")] +pub use crate::err::PyDowncastError; +pub use crate::err::{DowncastError, DowncastIntoError, PyErr, PyErrArguments, PyResult, ToPyErr}; #[allow(deprecated)] pub use crate::gil::GILPool; #[cfg(not(any(PyPy, GraalPy)))] pub use crate::gil::{prepare_freethreaded_python, with_embedded_python_interpreter}; -pub use crate::instance::{Borrowed, Bound, Py, PyNativeType, PyObject}; +#[cfg(feature = "gil-refs")] +pub use crate::instance::PyNativeType; +pub use crate::instance::{Borrowed, Bound, Py, PyObject}; pub use crate::marker::Python; +#[cfg(feature = "gil-refs")] #[allow(deprecated)] pub use crate::pycell::PyCell; pub use crate::pycell::{PyRef, PyRefMut}; @@ -443,6 +446,7 @@ mod conversions; pub mod coroutine; #[macro_use] #[doc(hidden)] +#[cfg(feature = "gil-refs")] pub mod derive_utils; mod err; pub mod exceptions; diff --git a/src/macros.rs b/src/macros.rs index d6f25c37308..6dde89e51a0 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -105,6 +105,7 @@ macro_rules! py_run_impl { ($py:expr, *$dict:expr, $code:expr) => {{ use ::std::option::Option::*; #[allow(unused_imports)] + #[cfg(feature = "gil-refs")] use $crate::PyNativeType; if let ::std::result::Result::Err(e) = $py.run_bound($code, None, Some(&$dict.as_borrowed())) { e.print($py); diff --git a/src/prelude.rs b/src/prelude.rs index 7aa45f6ccc2..4052f7c2d0b 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -15,11 +15,13 @@ pub use crate::conversion::{PyTryFrom, PyTryInto}; pub use crate::err::{PyErr, PyResult}; pub use crate::instance::{Borrowed, Bound, Py, PyObject}; pub use crate::marker::Python; +#[cfg(feature = "gil-refs")] #[allow(deprecated)] pub use crate::pycell::PyCell; pub use crate::pycell::{PyRef, PyRefMut}; pub use crate::pyclass_init::PyClassInitializer; pub use crate::types::{PyAny, PyModule}; +#[cfg(feature = "gil-refs")] pub use crate::PyNativeType; #[cfg(feature = "macros")] diff --git a/src/pycell.rs b/src/pycell.rs index dc5ccc45ea1..f15f5a54431 100644 --- a/src/pycell.rs +++ b/src/pycell.rs @@ -14,13 +14,13 @@ //! - However, methods and functions in Rust usually *do* need `&mut` references. While PyO3 can //! use the [`Python<'py>`](crate::Python) token to guarantee thread-safe access to them, it cannot //! statically guarantee uniqueness of `&mut` references. As such those references have to be tracked -//! dynamically at runtime, using [`PyCell`] and the other types defined in this module. This works +//! dynamically at runtime, using `PyCell` and the other types defined in this module. This works //! similar to std's [`RefCell`](std::cell::RefCell) type. //! //! # When *not* to use PyCell //! //! Usually you can use `&mut` references as method and function receivers and arguments, and you -//! won't need to use [`PyCell`] directly: +//! won't need to use `PyCell` directly: //! //! ```rust //! use pyo3::prelude::*; @@ -39,7 +39,7 @@ //! ``` //! //! The [`#[pymethods]`](crate::pymethods) proc macro will generate this wrapper function (and more), -//! using [`PyCell`] under the hood: +//! using `PyCell` under the hood: //! //! ```rust,ignore //! # use pyo3::prelude::*; @@ -76,7 +76,7 @@ //! # When to use PyCell //! ## Using pyclasses from Rust //! -//! However, we *do* need [`PyCell`] if we want to call its methods from Rust: +//! However, we *do* need `PyCell` if we want to call its methods from Rust: //! ```rust //! # use pyo3::prelude::*; //! # @@ -115,7 +115,7 @@ //! ``` //! ## Dealing with possibly overlapping mutable references //! -//! It is also necessary to use [`PyCell`] if you can receive mutable arguments that may overlap. +//! It is also necessary to use `PyCell` if you can receive mutable arguments that may overlap. //! Suppose the following function that swaps the values of two `Number`s: //! ``` //! # use pyo3::prelude::*; @@ -193,28 +193,30 @@ //! [guide]: https://pyo3.rs/latest/class.html#pycell-and-interior-mutability "PyCell and interior mutability" //! [Interior Mutability]: https://doc.rust-lang.org/book/ch15-05-interior-mutability.html "RefCell and the Interior Mutability Pattern - The Rust Programming Language" -use crate::conversion::{AsPyPointer, ToPyObject}; +use crate::conversion::AsPyPointer; use crate::exceptions::PyRuntimeError; use crate::ffi_ptr_ext::FfiPtrExt; -use crate::impl_::pyclass::PyClassImpl; -use crate::pyclass::{ - boolean_struct::{False, True}, - PyClass, -}; -use crate::type_object::{PyLayout, PySizedLayout}; +use crate::pyclass::{boolean_struct::False, PyClass}; use crate::types::any::PyAnyMethods; -use crate::types::PyAny; -use crate::{ffi, Bound, IntoPy, PyErr, PyNativeType, PyObject, PyTypeCheck, Python}; #[cfg(feature = "gil-refs")] -use crate::{pyclass_init::PyClassInitializer, PyResult}; +use crate::{ + conversion::ToPyObject, + impl_::pyclass::PyClassImpl, + pyclass::boolean_struct::True, + pyclass_init::PyClassInitializer, + type_object::{PyLayout, PySizedLayout}, + types::PyAny, + PyNativeType, PyResult, PyTypeCheck, +}; +use crate::{ffi, Bound, IntoPy, PyErr, PyObject, Python}; use std::fmt; use std::mem::ManuallyDrop; use std::ops::{Deref, DerefMut}; pub(crate) mod impl_; -use impl_::PyClassBorrowChecker; - -use self::impl_::{PyClassObject, PyClassObjectLayout}; +#[cfg(feature = "gil-refs")] +use self::impl_::PyClassObject; +use impl_::{PyClassBorrowChecker, PyClassObjectLayout}; /// A container type for (mutably) accessing [`PyClass`] values /// @@ -223,7 +225,7 @@ use self::impl_::{PyClassObject, PyClassObjectLayout}; /// # Examples /// /// This example demonstrates getting a mutable reference of the contained `PyClass`. -/// ```rust,ignore +/// ```rust /// use pyo3::prelude::*; /// /// #[pyclass] @@ -252,28 +254,27 @@ use self::impl_::{PyClassObject, PyClassObjectLayout}; /// ``` /// For more information on how, when and why (not) to use `PyCell` please see the /// [module-level documentation](self). -#[cfg_attr( - not(feature = "gil-refs"), - deprecated( - since = "0.21.0", - note = "`PyCell` was merged into `Bound`, use that instead; see the migration guide for more info" - ) +#[cfg(feature = "gil-refs")] +#[deprecated( + since = "0.21.0", + note = "`PyCell` was merged into `Bound`, use that instead; see the migration guide for more info" )] #[repr(transparent)] pub struct PyCell(PyClassObject); +#[cfg(feature = "gil-refs")] #[allow(deprecated)] unsafe impl PyNativeType for PyCell { type AsRefSource = T; } +#[cfg(feature = "gil-refs")] #[allow(deprecated)] impl PyCell { /// Makes a new `PyCell` on the Python heap and return the reference to it. /// /// In cases where the value in the cell does not need to be accessed immediately after /// creation, consider [`Py::new`](crate::Py::new) as a more efficient alternative. - #[cfg(feature = "gil-refs")] #[deprecated( since = "0.21.0", note = "use `Bound::new(py, value)` or `Py::new(py, value)` instead of `PyCell::new(py, value)`" @@ -316,7 +317,7 @@ impl PyCell { /// /// # Examples /// - /// ```ignore + /// ``` /// # use pyo3::prelude::*; /// #[pyclass] /// struct Class {} @@ -346,7 +347,7 @@ impl PyCell { /// /// # Examples /// - /// ```ignore + /// ``` /// # use pyo3::prelude::*; /// #[pyclass] /// struct Class {} @@ -379,7 +380,7 @@ impl PyCell { /// /// # Examples /// - /// ```ignore + /// ``` /// # use pyo3::prelude::*; /// #[pyclass] /// struct Class {} @@ -416,7 +417,7 @@ impl PyCell { /// /// # Examples /// - /// ```ignore + /// ``` /// use std::sync::atomic::{AtomicUsize, Ordering}; /// # use pyo3::prelude::*; /// @@ -487,11 +488,14 @@ impl PyCell { } } +#[cfg(feature = "gil-refs")] #[allow(deprecated)] unsafe impl PyLayout for PyCell {} +#[cfg(feature = "gil-refs")] #[allow(deprecated)] impl PySizedLayout for PyCell {} +#[cfg(feature = "gil-refs")] #[allow(deprecated)] impl PyTypeCheck for PyCell where @@ -503,7 +507,7 @@ where ::type_check(object) } } - +#[cfg(feature = "gil-refs")] #[allow(deprecated)] unsafe impl AsPyPointer for PyCell { fn as_ptr(&self) -> *mut ffi::PyObject { @@ -511,6 +515,7 @@ unsafe impl AsPyPointer for PyCell { } } +#[cfg(feature = "gil-refs")] #[allow(deprecated)] impl ToPyObject for &PyCell { fn to_object(&self, py: Python<'_>) -> PyObject { @@ -542,6 +547,7 @@ impl Deref for PyCell { } } +#[cfg(feature = "gil-refs")] #[allow(deprecated)] impl fmt::Debug for PyCell { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { @@ -768,6 +774,7 @@ impl IntoPy for &'_ PyRef<'_, T> { } } +#[cfg(feature = "gil-refs")] #[allow(deprecated)] impl<'a, T: PyClass> std::convert::TryFrom<&'a PyCell> for crate::PyRef<'a, T> { type Error = PyBorrowError; @@ -788,7 +795,7 @@ impl fmt::Debug for PyRef<'_, T> { } } -/// A wrapper type for a mutably borrowed value from a[`PyCell`]``. +/// A wrapper type for a mutably borrowed value from a [`Bound<'py, T>`]. /// /// See the [module-level documentation](self) for more information. pub struct PyRefMut<'p, T: PyClass> { @@ -928,6 +935,7 @@ unsafe impl<'a, T: PyClass> AsPyPointer for PyRefMut<'a, T> { } } +#[cfg(feature = "gil-refs")] #[allow(deprecated)] impl<'a, T: PyClass> std::convert::TryFrom<&'a PyCell> for crate::PyRefMut<'a, T> @@ -944,7 +952,7 @@ impl + fmt::Debug> fmt::Debug for PyRefMut<'_, T> { } } -/// An error type returned by [`PyCell::try_borrow`]. +/// An error type returned by [`Bound::try_borrow`]. /// /// If this error is allowed to bubble up into Python code it will raise a `RuntimeError`. pub struct PyBorrowError { @@ -969,7 +977,7 @@ impl From for PyErr { } } -/// An error type returned by [`PyCell::try_borrow_mut`]. +/// An error type returned by [`Bound::try_borrow_mut`]. /// /// If this error is allowed to bubble up into Python code it will raise a `RuntimeError`. pub struct PyBorrowMutError { diff --git a/src/pycell/impl_.rs b/src/pycell/impl_.rs index 1bdd44b9e9c..5404464caba 100644 --- a/src/pycell/impl_.rs +++ b/src/pycell/impl_.rs @@ -74,6 +74,7 @@ pub trait PyClassBorrowChecker { /// Increments immutable borrow count, if possible fn try_borrow(&self) -> Result<(), PyBorrowError>; + #[cfg(feature = "gil-refs")] fn try_borrow_unguarded(&self) -> Result<(), PyBorrowError>; /// Decrements immutable borrow count @@ -96,6 +97,7 @@ impl PyClassBorrowChecker for EmptySlot { } #[inline] + #[cfg(feature = "gil-refs")] fn try_borrow_unguarded(&self) -> Result<(), PyBorrowError> { Ok(()) } @@ -130,6 +132,7 @@ impl PyClassBorrowChecker for BorrowChecker { } } + #[cfg(feature = "gil-refs")] fn try_borrow_unguarded(&self) -> Result<(), PyBorrowError> { let flag = self.0.get(); if flag != BorrowFlag::HAS_MUTABLE_BORROW { diff --git a/src/pyclass.rs b/src/pyclass.rs index b9b01cac26a..162ae0d3119 100644 --- a/src/pyclass.rs +++ b/src/pyclass.rs @@ -16,6 +16,7 @@ pub use self::gc::{PyTraverseError, PyVisit}; /// The `#[pyclass]` attribute implements this trait for your Rust struct - /// you shouldn't implement this trait directly. #[allow(deprecated)] +#[cfg(feature = "gil-refs")] pub trait PyClass: PyTypeInfo> + PyClassImpl { /// Whether the pyclass is frozen. /// @@ -23,6 +24,18 @@ pub trait PyClass: PyTypeInfo> + PyClassImpl { type Frozen: Frozen; } +/// Types that can be used as Python classes. +/// +/// The `#[pyclass]` attribute implements this trait for your Rust struct - +/// you shouldn't implement this trait directly. +#[cfg(not(feature = "gil-refs"))] +pub trait PyClass: PyTypeInfo + PyClassImpl { + /// Whether the pyclass is frozen. + /// + /// This can be enabled via `#[pyclass(frozen)]`. + type Frozen: Frozen; +} + /// Operators for the `__richcmp__` method #[derive(Debug, Clone, Copy)] pub enum CompareOp { diff --git a/src/type_object.rs b/src/type_object.rs index 7f35f7d967a..871e8366865 100644 --- a/src/type_object.rs +++ b/src/type_object.rs @@ -3,7 +3,9 @@ use crate::ffi_ptr_ext::FfiPtrExt; use crate::types::any::PyAnyMethods; use crate::types::{PyAny, PyType}; -use crate::{ffi, Bound, PyNativeType, Python}; +#[cfg(feature = "gil-refs")] +use crate::PyNativeType; +use crate::{ffi, Bound, Python}; /// `T: PyLayout` represents that `T` is a concrete representation of `U` in the Python heap. /// E.g., `PyClassObject` is a concrete representation of all `pyclass`es, and `ffi::PyObject` @@ -29,11 +31,13 @@ pub trait PySizedLayout: PyLayout + Sized {} /// /// - `Py::as_ref` will hand out references to `Self::AsRefTarget`. /// - `Self::AsRefTarget` must have the same layout as `UnsafeCell`. +#[cfg(feature = "gil-refs")] pub unsafe trait HasPyGilRef { /// Utility type to make Py::as_ref work. type AsRefTarget: PyNativeType; } +#[cfg(feature = "gil-refs")] unsafe impl HasPyGilRef for T where T: PyNativeType, @@ -54,6 +58,7 @@ where /// /// Implementations must provide an implementation for `type_object_raw` which infallibly produces a /// non-null pointer to the corresponding Python type object. +#[cfg(feature = "gil-refs")] pub unsafe trait PyTypeInfo: Sized + HasPyGilRef { /// Class name. const NAME: &'static str; @@ -132,7 +137,62 @@ pub unsafe trait PyTypeInfo: Sized + HasPyGilRef { } } +/// Python type information. +/// All Python native types (e.g., `PyDict`) and `#[pyclass]` structs implement this trait. +/// +/// This trait is marked unsafe because: +/// - specifying the incorrect layout can lead to memory errors +/// - the return value of type_object must always point to the same PyTypeObject instance +/// +/// It is safely implemented by the `pyclass` macro. +/// +/// # Safety +/// +/// Implementations must provide an implementation for `type_object_raw` which infallibly produces a +/// non-null pointer to the corresponding Python type object. +#[cfg(not(feature = "gil-refs"))] +pub unsafe trait PyTypeInfo: Sized { + /// Class name. + const NAME: &'static str; + + /// Module name, if any. + const MODULE: Option<&'static str>; + + /// Returns the PyTypeObject instance for this type. + fn type_object_raw(py: Python<'_>) -> *mut ffi::PyTypeObject; + + /// Returns the safe abstraction over the type object. + #[inline] + fn type_object_bound(py: Python<'_>) -> Bound<'_, PyType> { + // Making the borrowed object `Bound` is necessary for soundness reasons. It's an extreme + // edge case, but arbitrary Python code _could_ change the __class__ of an object and cause + // the type object to be freed. + // + // By making `Bound` we assume ownership which is then safe against races. + unsafe { + Self::type_object_raw(py) + .cast::() + .assume_borrowed_unchecked(py) + .to_owned() + .downcast_into_unchecked() + } + } + + /// Checks if `object` is an instance of this type or a subclass of this type. + #[inline] + fn is_type_of_bound(object: &Bound<'_, PyAny>) -> bool { + unsafe { ffi::PyObject_TypeCheck(object.as_ptr(), Self::type_object_raw(object.py())) != 0 } + } + + /// Checks if `object` is an instance of this type. + #[inline] + fn is_exact_type_of_bound(object: &Bound<'_, PyAny>) -> bool { + unsafe { ffi::Py_TYPE(object.as_ptr()) == Self::type_object_raw(object.py()) } + } +} + /// Implemented by types which can be used as a concrete Python type inside `Py` smart pointers. +#[cfg(feature = "gil-refs")] pub trait PyTypeCheck: HasPyGilRef { /// Name of self. This is used in error messages, for example. const NAME: &'static str; @@ -143,6 +203,18 @@ pub trait PyTypeCheck: HasPyGilRef { fn type_check(object: &Bound<'_, PyAny>) -> bool; } +/// Implemented by types which can be used as a concrete Python type inside `Py` smart pointers. +#[cfg(not(feature = "gil-refs"))] +pub trait PyTypeCheck { + /// Name of self. This is used in error messages, for example. + const NAME: &'static str; + + /// Checks if `object` is an instance of `Self`, which may include a subtype. + /// + /// This should be equivalent to the Python expression `isinstance(object, Self)`. + fn type_check(object: &Bound<'_, PyAny>) -> bool; +} + impl PyTypeCheck for T where T: PyTypeInfo, diff --git a/src/types/any.rs b/src/types/any.rs index 17837835be1..ba5ea01b1a3 100644 --- a/src/types/any.rs +++ b/src/types/any.rs @@ -1646,7 +1646,7 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed { /// Extracts some type from the Python object. /// /// This is a wrapper function around - /// [`FromPyObject::extract()`](crate::FromPyObject::extract). + /// [`FromPyObject::extract_bound()`](crate::FromPyObject::extract_bound). fn extract<'a, T>(&'a self) -> PyResult where T: FromPyObjectBound<'a, 'py>; diff --git a/src/types/mod.rs b/src/types/mod.rs index 2203ccdf2dc..12dabda7463 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -122,10 +122,12 @@ pub trait DerefToPyAny { #[macro_export] macro_rules! pyobject_native_type_base( ($name:ty $(;$generics:ident)* ) => { + #[cfg(feature = "gil-refs")] unsafe impl<$($generics,)*> $crate::PyNativeType for $name { type AsRefSource = Self; } + #[cfg(feature = "gil-refs")] impl<$($generics,)*> ::std::fmt::Debug for $name { fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::result::Result<(), ::std::fmt::Error> @@ -136,6 +138,7 @@ macro_rules! pyobject_native_type_base( } } + #[cfg(feature = "gil-refs")] impl<$($generics,)*> ::std::fmt::Display for $name { fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::result::Result<(), ::std::fmt::Error> diff --git a/tests/test_compile_error.rs b/tests/test_compile_error.rs index 975d26009a5..d31c558f096 100644 --- a/tests/test_compile_error.rs +++ b/tests/test_compile_error.rs @@ -14,7 +14,7 @@ fn test_compile_errors() { #[cfg(any(not(Py_LIMITED_API), Py_3_11))] t.compile_fail("tests/ui/invalid_pymethods_buffer.rs"); // The output is not stable across abi3 / not abi3 and features - #[cfg(all(not(Py_LIMITED_API), feature = "full"))] + #[cfg(all(not(Py_LIMITED_API), feature = "full", not(feature = "gil-refs")))] t.compile_fail("tests/ui/invalid_pymethods_duplicates.rs"); t.compile_fail("tests/ui/invalid_pymethod_enum.rs"); t.compile_fail("tests/ui/invalid_pymethod_names.rs"); @@ -27,12 +27,14 @@ fn test_compile_errors() { t.compile_fail("tests/ui/invalid_argument_attributes.rs"); t.compile_fail("tests/ui/invalid_frompy_derive.rs"); t.compile_fail("tests/ui/static_ref.rs"); + #[cfg(not(feature = "gil-refs"))] t.compile_fail("tests/ui/wrong_aspyref_lifetimes.rs"); t.compile_fail("tests/ui/invalid_pyfunctions.rs"); t.compile_fail("tests/ui/invalid_pymethods.rs"); // output changes with async feature #[cfg(all(Py_LIMITED_API, feature = "experimental-async"))] t.compile_fail("tests/ui/abi3_nativetype_inheritance.rs"); + #[cfg(not(feature = "gil-refs"))] t.compile_fail("tests/ui/invalid_intern_arg.rs"); t.compile_fail("tests/ui/invalid_frozen_pyclass_borrow.rs"); t.compile_fail("tests/ui/invalid_pymethod_receiver.rs"); diff --git a/tests/ui/deprecations.stderr b/tests/ui/deprecations.stderr index b11c0058ce2..d014a06bbcc 100644 --- a/tests/ui/deprecations.stderr +++ b/tests/ui/deprecations.stderr @@ -34,6 +34,12 @@ error: use of deprecated constant `__pyfunction_pyfunction_option_4::SIGNATURE`: 138 | fn pyfunction_option_4( | ^^^^^^^^^^^^^^^^^^^ +error: use of deprecated struct `pyo3::PyCell`: `PyCell` was merged into `Bound`, use that instead; see the migration guide for more info + --> tests/ui/deprecations.rs:23:30 + | +23 | fn method_gil_ref(_slf: &PyCell) {} + | ^^^^^^ + error: use of deprecated method `pyo3::deprecations::GilRefs::::from_py_with_arg`: use `&Bound<'_, PyAny>` as the argument for this `from_py_with` extractor --> tests/ui/deprecations.rs:45:44 | diff --git a/tests/ui/invalid_intern_arg.stderr b/tests/ui/invalid_intern_arg.stderr index 5d2131bd845..7d1aad1ae28 100644 --- a/tests/ui/invalid_intern_arg.stderr +++ b/tests/ui/invalid_intern_arg.stderr @@ -13,5 +13,5 @@ error: lifetime may not live long enough 5 | Python::with_gil(|py| py.import_bound(pyo3::intern!(py, _foo)).unwrap()); | --- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ returning this value requires that `'1` must outlive `'2` | | | - | | return type of closure is pyo3::Bound<'2, pyo3::prelude::PyModule> - | has type `pyo3::Python<'1>` + | | return type of closure is pyo3::Bound<'2, PyModule> + | has type `Python<'1>` diff --git a/tests/ui/invalid_pymethods_duplicates.stderr b/tests/ui/invalid_pymethods_duplicates.stderr index 753c4b1b8dc..db301336e4f 100644 --- a/tests/ui/invalid_pymethods_duplicates.stderr +++ b/tests/ui/invalid_pymethods_duplicates.stderr @@ -26,23 +26,6 @@ error[E0277]: the trait bound `TwoNew: PyTypeInfo` is not satisfied PyDate and $N others -error[E0277]: the trait bound `TwoNew: HasPyGilRef` is not satisfied - --> tests/ui/invalid_pymethods_duplicates.rs:9:6 - | -9 | impl TwoNew { - | ^^^^^^ the trait `PyNativeType` is not implemented for `TwoNew`, which is required by `TwoNew: HasPyGilRef` - | - = help: the trait `HasPyGilRef` is implemented for `pyo3::coroutine::Coroutine` - = note: required for `TwoNew` to implement `HasPyGilRef` -note: required by a bound in `pyo3::PyTypeInfo::NAME` - --> src/type_object.rs - | - | pub unsafe trait PyTypeInfo: Sized + HasPyGilRef { - | ^^^^^^^^^^^ required by this bound in `PyTypeInfo::NAME` - | /// Class name. - | const NAME: &'static str; - | ---- required by a bound in this associated constant - error[E0592]: duplicate definitions with name `__pymethod___new____` --> tests/ui/invalid_pymethods_duplicates.rs:8:1 | diff --git a/tests/ui/wrong_aspyref_lifetimes.stderr b/tests/ui/wrong_aspyref_lifetimes.stderr index 30e63bb8261..f2f43d99f25 100644 --- a/tests/ui/wrong_aspyref_lifetimes.stderr +++ b/tests/ui/wrong_aspyref_lifetimes.stderr @@ -5,4 +5,4 @@ error: lifetime may not live long enough | --- ^^^^^^^^^^^^^ returning this value requires that `'1` must outlive `'2` | | | | | return type of closure is &'2 pyo3::Bound<'_, PyDict> - | has type `pyo3::Python<'1>` + | has type `Python<'1>` From 7790dab48046f01cbee967b054a05de767d3586d Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Wed, 15 May 2024 07:11:49 -0400 Subject: [PATCH 075/495] emit rustc-check-cfg only on rust 1.80+ (#4168) --- pyo3-build-config/src/lib.rs | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/pyo3-build-config/src/lib.rs b/pyo3-build-config/src/lib.rs index 54aff4d10de..6d4ec759653 100644 --- a/pyo3-build-config/src/lib.rs +++ b/pyo3-build-config/src/lib.rs @@ -18,7 +18,6 @@ use std::{ use std::{env, process::Command, str::FromStr}; -#[cfg(feature = "resolve-config")] use once_cell::sync::OnceCell; pub use impl_::{ @@ -135,17 +134,6 @@ fn resolve_cross_compile_config_path() -> Option { /// so this function is unstable. #[doc(hidden)] pub fn print_feature_cfgs() { - fn rustc_minor_version() -> Option { - let rustc = env::var_os("RUSTC")?; - let output = Command::new(rustc).arg("--version").output().ok()?; - let version = core::str::from_utf8(&output.stdout).ok()?; - let mut pieces = version.split('.'); - if pieces.next() != Some("rustc 1") { - return None; - } - pieces.next()?.parse().ok() - } - let rustc_minor_version = rustc_minor_version().unwrap_or(0); // invalid_from_utf8 lint was added in Rust 1.74 @@ -160,6 +148,11 @@ pub fn print_feature_cfgs() { /// - #[doc(hidden)] pub fn print_expected_cfgs() { + if rustc_minor_version().map_or(false, |version| version < 80) { + // rustc 1.80.0 stabilized `rustc-check-cfg` feature, don't emit before + return; + } + println!("cargo:rustc-check-cfg=cfg(Py_LIMITED_API)"); println!("cargo:rustc-check-cfg=cfg(PyPy)"); println!("cargo:rustc-check-cfg=cfg(GraalPy)"); @@ -233,6 +226,20 @@ pub mod pyo3_build_script_impl { } } +fn rustc_minor_version() -> Option { + static RUSTC_MINOR_VERSION: OnceCell> = OnceCell::new(); + *RUSTC_MINOR_VERSION.get_or_init(|| { + let rustc = env::var_os("RUSTC")?; + let output = Command::new(rustc).arg("--version").output().ok()?; + let version = core::str::from_utf8(&output.stdout).ok()?; + let mut pieces = version.split('.'); + if pieces.next() != Some("rustc 1") { + return None; + } + pieces.next()?.parse().ok() + }) +} + #[cfg(test)] mod tests { use super::*; From 8de1787580c26c1d19c7a99028139340a8498cae Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Thu, 16 May 2024 17:55:05 -0400 Subject: [PATCH 076/495] Change `GILGuard` to be able to represent a GIL that was already held (#4187) See #4181 --- src/gil.rs | 41 +++++++++++++++++++++++++---------------- 1 file changed, 25 insertions(+), 16 deletions(-) diff --git a/src/gil.rs b/src/gil.rs index db8311ab2fc..a1d9feba494 100644 --- a/src/gil.rs +++ b/src/gil.rs @@ -148,20 +148,26 @@ where } /// RAII type that represents the Global Interpreter Lock acquisition. -pub(crate) struct GILGuard { - gstate: ffi::PyGILState_STATE, - #[allow(deprecated)] // TODO: remove this with the gil-refs feature in 0.22 - pool: mem::ManuallyDrop, +pub(crate) enum GILGuard { + /// Indicates the GIL was already held with this GILGuard was acquired. + Assumed, + /// Indicates that we actually acquired the GIL when this GILGuard was acquired + Ensured { + gstate: ffi::PyGILState_STATE, + #[allow(deprecated)] // TODO: remove this with the gil-refs feature in 0.22 + pool: mem::ManuallyDrop, + }, } impl GILGuard { /// PyO3 internal API for acquiring the GIL. The public API is Python::with_gil. /// - /// If the GIL was already acquired via PyO3, this returns `None`. Otherwise, - /// the GIL will be acquired and a new `GILPool` created. - pub(crate) fn acquire() -> Option { + /// If the GIL was already acquired via PyO3, this returns + /// `GILGuard::Assumed`. Otherwise, the GIL will be acquired and + /// `GILGuard::Ensured` will be returned. + pub(crate) fn acquire() -> Self { if gil_is_acquired() { - return None; + return GILGuard::Assumed; } // Maybe auto-initialize the GIL: @@ -207,27 +213,30 @@ impl GILGuard { /// This can be called in "unsafe" contexts where the normal interpreter state /// checking performed by `GILGuard::acquire` may fail. This includes calling /// as part of multi-phase interpreter initialization. - pub(crate) fn acquire_unchecked() -> Option { + pub(crate) fn acquire_unchecked() -> Self { if gil_is_acquired() { - return None; + return GILGuard::Assumed; } let gstate = unsafe { ffi::PyGILState_Ensure() }; // acquire GIL #[allow(deprecated)] let pool = unsafe { mem::ManuallyDrop::new(GILPool::new()) }; - Some(GILGuard { gstate, pool }) + GILGuard::Ensured { gstate, pool } } } /// The Drop implementation for `GILGuard` will release the GIL. impl Drop for GILGuard { fn drop(&mut self) { - unsafe { - // Drop the objects in the pool before attempting to release the thread state - mem::ManuallyDrop::drop(&mut self.pool); - - ffi::PyGILState_Release(self.gstate); + match self { + GILGuard::Assumed => {} + GILGuard::Ensured { gstate, pool } => unsafe { + // Drop the objects in the pool before attempting to release the thread state + mem::ManuallyDrop::drop(pool); + + ffi::PyGILState_Release(*gstate); + }, } } } From 88f2f6f4d56f2bac1220d1f0d0ac912b8c160c4a Mon Sep 17 00:00:00 2001 From: newcomertv Date: Fri, 17 May 2024 04:59:00 +0200 Subject: [PATCH 077/495] feat: support pyclass on tuple enums (#4072) * feat: support pyclass on tuple enums * cargo fmt * changelog * ruff format * rebase with adaptation for FnArg refactor * fix class.md from pr comments * add enum tuple variant getitem implementation * fmt * progress toward getitem and len impl on derive pyclass for complex enum tuple * working getitem and len slots for complex tuple enum pyclass derivation * refactor code generation * address PR concerns - take py from function argument on get_item - make more general slot def implementation - remove unnecessary function arguments - add testcases for uncovered cases including future feature match_args * add tracking issue * fmt * ruff * remove me * support match_args for tuple enum * integrate FnArg now takes Cow * fix empty and single element tuples * use impl_py_slot_def for cimplex tuple enum slots * reverse erroneous doc change * Address latest comments * formatting suggestion * fix : - clippy beta - better compile error (+related doc and test) --------- Co-authored-by: Chris Arderne --- guide/src/class.md | 21 +- newsfragments/4072.added.md | 1 + pyo3-macros-backend/src/pyclass.rs | 333 ++++++++++++++++++++++++-- pyo3-macros-backend/src/pymethod.rs | 5 +- pytests/src/enums.rs | 42 ++++ pytests/tests/test_enums.py | 64 +++++ pytests/tests/test_enums_match.py | 99 ++++++++ tests/ui/invalid_pyclass_enum.rs | 6 - tests/ui/invalid_pyclass_enum.stderr | 14 +- tests/ui/invalid_pymethod_enum.rs | 16 ++ tests/ui/invalid_pymethod_enum.stderr | 25 ++ 11 files changed, 581 insertions(+), 45 deletions(-) create mode 100644 newsfragments/4072.added.md diff --git a/guide/src/class.md b/guide/src/class.md index ce86ec40e5f..57a5cf6d467 100644 --- a/guide/src/class.md +++ b/guide/src/class.md @@ -52,15 +52,18 @@ enum HttpResponse { // ... } -// PyO3 also supports enums with non-unit variants +// PyO3 also supports enums with Struct and Tuple variants // These complex enums have sligtly different behavior from the simple enums above // They are meant to work with instance checks and match statement patterns +// The variants can be mixed and matched +// Struct variants have named fields while tuple enums generate generic names for fields in order _0, _1, _2, ... +// Apart from this both types are functionally identical #[pyclass] enum Shape { Circle { radius: f64 }, Rectangle { width: f64, height: f64 }, - RegularPolygon { side_count: u32, radius: f64 }, - Nothing {}, + RegularPolygon(u32, f64), + Nothing(), } ``` @@ -1180,7 +1183,7 @@ enum BadSubclass { An enum is complex if it has any non-unit (struct or tuple) variants. -Currently PyO3 supports only struct variants in a complex enum. Support for unit and tuple variants is planned. +PyO3 supports only struct and tuple variants in a complex enum. Unit variants aren't supported at present (the recommendation is to use an empty tuple enum instead). PyO3 adds a class attribute for each variant, which may be used to construct values and in match patterns. PyO3 also provides getter methods for all fields of each variant. @@ -1190,14 +1193,14 @@ PyO3 adds a class attribute for each variant, which may be used to construct val enum Shape { Circle { radius: f64 }, Rectangle { width: f64, height: f64 }, - RegularPolygon { side_count: u32, radius: f64 }, + RegularPolygon(u32, f64), Nothing { }, } # #[cfg(Py_3_10)] Python::with_gil(|py| { let circle = Shape::Circle { radius: 10.0 }.into_py(py); - let square = Shape::RegularPolygon { side_count: 4, radius: 10.0 }.into_py(py); + let square = Shape::RegularPolygon(4, 10.0).into_py(py); let cls = py.get_type_bound::(); pyo3::py_run!(py, circle square cls, r#" assert isinstance(circle, cls) @@ -1206,8 +1209,8 @@ Python::with_gil(|py| { assert isinstance(square, cls) assert isinstance(square, cls.RegularPolygon) - assert square.side_count == 4 - assert square.radius == 10.0 + assert square[0] == 4 # Gets _0 field + assert square[1] == 10.0 # Gets _1 field def count_vertices(cls, shape): match shape: @@ -1215,7 +1218,7 @@ Python::with_gil(|py| { return 0 case cls.Rectangle(): return 4 - case cls.RegularPolygon(side_count=n): + case cls.RegularPolygon(n): return n case cls.Nothing(): return 0 diff --git a/newsfragments/4072.added.md b/newsfragments/4072.added.md new file mode 100644 index 00000000000..23207c849d8 --- /dev/null +++ b/newsfragments/4072.added.md @@ -0,0 +1 @@ +Support `#[pyclass]` on enums that have tuple variants. \ No newline at end of file diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index 4e71a711802..47c52c84518 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -12,7 +12,7 @@ use crate::pyfunction::ConstructorAttribute; use crate::pyimpl::{gen_py_const, PyClassMethodsType}; use crate::pymethod::{ impl_py_getter_def, impl_py_setter_def, MethodAndMethodDef, MethodAndSlotDef, PropertyType, - SlotDef, __INT__, __REPR__, __RICHCMP__, + SlotDef, __GETITEM__, __INT__, __LEN__, __REPR__, __RICHCMP__, }; use crate::utils::Ctx; use crate::utils::{self, apply_renaming_rule, PythonDoc}; @@ -504,10 +504,10 @@ impl<'a> PyClassComplexEnum<'a> { let variant = match &variant.fields { Fields::Unit => { bail_spanned!(variant.span() => format!( - "Unit variant `{ident}` is not yet supported in a complex enum\n\ - = help: change to a struct variant with no fields: `{ident} {{ }}`\n\ - = note: the enum is complex because of non-unit variant `{witness}`", - ident=ident, witness=witness)) + "Unit variant `{ident}` is not yet supported in a complex enum\n\ + = help: change to an empty tuple variant instead: `{ident}()`\n\ + = note: the enum is complex because of non-unit variant `{witness}`", + ident=ident, witness=witness)) } Fields::Named(fields) => { let fields = fields @@ -526,12 +526,21 @@ impl<'a> PyClassComplexEnum<'a> { options, }) } - Fields::Unnamed(_) => { - bail_spanned!(variant.span() => format!( - "Tuple variant `{ident}` is not yet supported in a complex enum\n\ - = help: change to a struct variant with named fields: `{ident} {{ /* fields */ }}`\n\ - = note: the enum is complex because of non-unit variant `{witness}`", - ident=ident, witness=witness)) + Fields::Unnamed(types) => { + let fields = types + .unnamed + .iter() + .map(|field| PyClassEnumVariantUnnamedField { + ty: &field.ty, + span: field.span(), + }) + .collect(); + + PyClassEnumVariant::Tuple(PyClassEnumTupleVariant { + ident, + fields, + options, + }) } }; @@ -553,7 +562,7 @@ impl<'a> PyClassComplexEnum<'a> { enum PyClassEnumVariant<'a> { // TODO(mkovaxx): Unit(PyClassEnumUnitVariant<'a>), Struct(PyClassEnumStructVariant<'a>), - // TODO(mkovaxx): Tuple(PyClassEnumTupleVariant<'a>), + Tuple(PyClassEnumTupleVariant<'a>), } trait EnumVariant { @@ -581,12 +590,14 @@ impl<'a> EnumVariant for PyClassEnumVariant<'a> { fn get_ident(&self) -> &syn::Ident { match self { PyClassEnumVariant::Struct(struct_variant) => struct_variant.ident, + PyClassEnumVariant::Tuple(tuple_variant) => tuple_variant.ident, } } fn get_options(&self) -> &EnumVariantPyO3Options { match self { PyClassEnumVariant::Struct(struct_variant) => &struct_variant.options, + PyClassEnumVariant::Tuple(tuple_variant) => &tuple_variant.options, } } } @@ -614,12 +625,23 @@ struct PyClassEnumStructVariant<'a> { options: EnumVariantPyO3Options, } +struct PyClassEnumTupleVariant<'a> { + ident: &'a syn::Ident, + fields: Vec>, + options: EnumVariantPyO3Options, +} + struct PyClassEnumVariantNamedField<'a> { ident: &'a syn::Ident, ty: &'a syn::Type, span: Span, } +struct PyClassEnumVariantUnnamedField<'a> { + ty: &'a syn::Type, + span: Span, +} + /// `#[pyo3()]` options for pyclass enum variants #[derive(Default)] struct EnumVariantPyO3Options { @@ -930,17 +952,19 @@ fn impl_complex_enum( let variant_cls_pytypeinfo = impl_pytypeinfo(&variant_cls, &variant_args, None, ctx); variant_cls_pytypeinfos.push(variant_cls_pytypeinfo); - let (variant_cls_impl, field_getters) = impl_complex_enum_variant_cls(cls, &variant, ctx)?; + let (variant_cls_impl, field_getters, mut slots) = + impl_complex_enum_variant_cls(cls, &variant, ctx)?; variant_cls_impls.push(variant_cls_impl); let variant_new = complex_enum_variant_new(cls, variant, ctx)?; + slots.push(variant_new); let pyclass_impl = PyClassImplsBuilder::new( &variant_cls, &variant_args, methods_type, field_getters, - vec![variant_new], + slots, ) .impl_all(ctx)?; @@ -970,19 +994,52 @@ fn impl_complex_enum_variant_cls( enum_name: &syn::Ident, variant: &PyClassEnumVariant<'_>, ctx: &Ctx, -) -> Result<(TokenStream, Vec)> { +) -> Result<(TokenStream, Vec, Vec)> { match variant { PyClassEnumVariant::Struct(struct_variant) => { impl_complex_enum_struct_variant_cls(enum_name, struct_variant, ctx) } + PyClassEnumVariant::Tuple(tuple_variant) => { + impl_complex_enum_tuple_variant_cls(enum_name, tuple_variant, ctx) + } } } +fn impl_complex_enum_variant_match_args( + ctx: &Ctx, + variant_cls_type: &syn::Type, + field_names: &mut Vec, +) -> (MethodAndMethodDef, syn::ImplItemConst) { + let match_args_const_impl: syn::ImplItemConst = { + let args_tp = field_names.iter().map(|_| { + quote! { &'static str } + }); + parse_quote! { + const __match_args__: ( #(#args_tp,)* ) = ( + #(stringify!(#field_names),)* + ); + } + }; + + let spec = ConstSpec { + rust_ident: format_ident!("__match_args__"), + attributes: ConstAttributes { + is_class_attr: true, + name: None, + deprecations: Deprecations::new(ctx), + }, + }; + + let variant_match_args = gen_py_const(variant_cls_type, &spec, ctx); + + (variant_match_args, match_args_const_impl) +} + fn impl_complex_enum_struct_variant_cls( enum_name: &syn::Ident, variant: &PyClassEnumStructVariant<'_>, ctx: &Ctx, -) -> Result<(TokenStream, Vec)> { +) -> Result<(TokenStream, Vec, Vec)> { let Ctx { pyo3_path } = ctx; let variant_ident = &variant.ident; let variant_cls = gen_complex_enum_variant_class_ident(enum_name, variant.ident); @@ -1015,6 +1072,11 @@ fn impl_complex_enum_struct_variant_cls( field_getter_impls.push(field_getter_impl); } + let (variant_match_args, match_args_const_impl) = + impl_complex_enum_variant_match_args(ctx, &variant_cls_type, &mut field_names); + + field_getters.push(variant_match_args); + let cls_impl = quote! { #[doc(hidden)] #[allow(non_snake_case)] @@ -1024,11 +1086,190 @@ fn impl_complex_enum_struct_variant_cls( #pyo3_path::PyClassInitializer::from(base_value).add_subclass(#variant_cls) } + #match_args_const_impl + #(#field_getter_impls)* } }; - Ok((cls_impl, field_getters)) + Ok((cls_impl, field_getters, Vec::new())) +} + +fn impl_complex_enum_tuple_variant_field_getters( + ctx: &Ctx, + variant: &PyClassEnumTupleVariant<'_>, + enum_name: &syn::Ident, + variant_cls_type: &syn::Type, + variant_ident: &&Ident, + field_names: &mut Vec, + fields_types: &mut Vec, +) -> Result<(Vec, Vec)> { + let Ctx { pyo3_path } = ctx; + + let mut field_getters = vec![]; + let mut field_getter_impls = vec![]; + + for (index, field) in variant.fields.iter().enumerate() { + let field_name = format_ident!("_{}", index); + let field_type = field.ty; + + let field_getter = + complex_enum_variant_field_getter(variant_cls_type, &field_name, field.span, ctx)?; + + // Generate the match arms needed to destructure the tuple and access the specific field + let field_access_tokens: Vec<_> = (0..variant.fields.len()) + .map(|i| { + if i == index { + quote! { val } + } else { + quote! { _ } + } + }) + .collect(); + + let field_getter_impl: syn::ImplItemFn = parse_quote! { + fn #field_name(slf: #pyo3_path::PyRef) -> #pyo3_path::PyResult<#field_type> { + match &*slf.into_super() { + #enum_name::#variant_ident ( #(#field_access_tokens), *) => Ok(val.clone()), + _ => unreachable!("Wrong complex enum variant found in variant wrapper PyClass"), + } + } + }; + + field_names.push(field_name); + fields_types.push(field_type.clone()); + field_getters.push(field_getter); + field_getter_impls.push(field_getter_impl); + } + + Ok((field_getters, field_getter_impls)) +} + +fn impl_complex_enum_tuple_variant_len( + ctx: &Ctx, + + variant_cls_type: &syn::Type, + num_fields: usize, +) -> Result<(MethodAndSlotDef, syn::ImplItemFn)> { + let Ctx { pyo3_path } = ctx; + + let mut len_method_impl: syn::ImplItemFn = parse_quote! { + fn __len__(slf: #pyo3_path::PyRef) -> #pyo3_path::PyResult { + Ok(#num_fields) + } + }; + + let variant_len = + generate_default_protocol_slot(variant_cls_type, &mut len_method_impl, &__LEN__, ctx)?; + + Ok((variant_len, len_method_impl)) +} + +fn impl_complex_enum_tuple_variant_getitem( + ctx: &Ctx, + variant_cls: &syn::Ident, + variant_cls_type: &syn::Type, + num_fields: usize, +) -> Result<(MethodAndSlotDef, syn::ImplItemFn)> { + let Ctx { pyo3_path } = ctx; + + let match_arms: Vec<_> = (0..num_fields) + .map(|i| { + let field_access = format_ident!("_{}", i); + quote! { + #i => Ok( + #pyo3_path::IntoPy::into_py( + #variant_cls::#field_access(slf)? + , py) + ) + + } + }) + .collect(); + + let mut get_item_method_impl: syn::ImplItemFn = parse_quote! { + fn __getitem__(slf: #pyo3_path::PyRef, idx: usize) -> #pyo3_path::PyResult< #pyo3_path::PyObject> { + let py = slf.py(); + match idx { + #( #match_arms, )* + _ => Err(pyo3::exceptions::PyIndexError::new_err("tuple index out of range")), + } + } + }; + + let variant_getitem = generate_default_protocol_slot( + variant_cls_type, + &mut get_item_method_impl, + &__GETITEM__, + ctx, + )?; + + Ok((variant_getitem, get_item_method_impl)) +} + +fn impl_complex_enum_tuple_variant_cls( + enum_name: &syn::Ident, + variant: &PyClassEnumTupleVariant<'_>, + ctx: &Ctx, +) -> Result<(TokenStream, Vec, Vec)> { + let Ctx { pyo3_path } = ctx; + let variant_ident = &variant.ident; + let variant_cls = gen_complex_enum_variant_class_ident(enum_name, variant.ident); + let variant_cls_type = parse_quote!(#variant_cls); + + let mut slots = vec![]; + + // represents the index of the field + let mut field_names: Vec = vec![]; + let mut field_types: Vec = vec![]; + + let (mut field_getters, field_getter_impls) = impl_complex_enum_tuple_variant_field_getters( + ctx, + variant, + enum_name, + &variant_cls_type, + variant_ident, + &mut field_names, + &mut field_types, + )?; + + let num_fields = variant.fields.len(); + + let (variant_len, len_method_impl) = + impl_complex_enum_tuple_variant_len(ctx, &variant_cls_type, num_fields)?; + + slots.push(variant_len); + + let (variant_getitem, getitem_method_impl) = + impl_complex_enum_tuple_variant_getitem(ctx, &variant_cls, &variant_cls_type, num_fields)?; + + slots.push(variant_getitem); + + let (variant_match_args, match_args_method_impl) = + impl_complex_enum_variant_match_args(ctx, &variant_cls_type, &mut field_names); + + field_getters.push(variant_match_args); + + let cls_impl = quote! { + #[doc(hidden)] + #[allow(non_snake_case)] + impl #variant_cls { + fn __pymethod_constructor__(py: #pyo3_path::Python<'_>, #(#field_names : #field_types,)*) -> #pyo3_path::PyClassInitializer<#variant_cls> { + let base_value = #enum_name::#variant_ident ( #(#field_names,)* ); + #pyo3_path::PyClassInitializer::from(base_value).add_subclass(#variant_cls) + } + + #len_method_impl + + #getitem_method_impl + + #match_args_method_impl + + #(#field_getter_impls)* + } + }; + + Ok((cls_impl, field_getters, slots)) } fn gen_complex_enum_variant_class_ident(enum_: &syn::Ident, variant: &syn::Ident) -> syn::Ident { @@ -1149,6 +1390,9 @@ fn complex_enum_variant_new<'a>( PyClassEnumVariant::Struct(struct_variant) => { complex_enum_struct_variant_new(cls, struct_variant, ctx) } + PyClassEnumVariant::Tuple(tuple_variant) => { + complex_enum_tuple_variant_new(cls, tuple_variant, ctx) + } } } @@ -1209,6 +1453,61 @@ fn complex_enum_struct_variant_new<'a>( crate::pymethod::impl_py_method_def_new(&variant_cls_type, &spec, ctx) } +fn complex_enum_tuple_variant_new<'a>( + cls: &'a syn::Ident, + variant: PyClassEnumTupleVariant<'a>, + ctx: &Ctx, +) -> Result { + let Ctx { pyo3_path } = ctx; + + let variant_cls: Ident = format_ident!("{}_{}", cls, variant.ident); + let variant_cls_type: syn::Type = parse_quote!(#variant_cls); + + let arg_py_ident: syn::Ident = parse_quote!(py); + let arg_py_type: syn::Type = parse_quote!(#pyo3_path::Python<'_>); + + let args = { + let mut args = vec![FnArg::Py(PyArg { + name: &arg_py_ident, + ty: &arg_py_type, + })]; + + for (i, field) in variant.fields.iter().enumerate() { + args.push(FnArg::Regular(RegularArg { + name: std::borrow::Cow::Owned(format_ident!("_{}", i)), + ty: field.ty, + from_py_with: None, + default_value: None, + option_wrapped_type: None, + })); + } + args + }; + + let signature = if let Some(constructor) = variant.options.constructor { + crate::pyfunction::FunctionSignature::from_arguments_and_attribute( + args, + constructor.into_signature(), + )? + } else { + crate::pyfunction::FunctionSignature::from_arguments(args)? + }; + + let spec = FnSpec { + tp: crate::method::FnType::FnNew, + name: &format_ident!("__pymethod_constructor__"), + python_name: format_ident!("__new__"), + signature, + convention: crate::method::CallingConvention::TpNew, + text_signature: None, + asyncness: None, + unsafety: None, + deprecations: Deprecations::new(ctx), + }; + + crate::pymethod::impl_py_method_def_new(&variant_cls_type, &spec, ctx) +} + fn complex_enum_variant_field_getter<'a>( variant_cls_type: &'a syn::Type, field_name: &'a syn::Ident, diff --git a/pyo3-macros-backend/src/pymethod.rs b/pyo3-macros-backend/src/pymethod.rs index 208735f2619..f5b11af3c27 100644 --- a/pyo3-macros-backend/src/pymethod.rs +++ b/pyo3-macros-backend/src/pymethod.rs @@ -934,7 +934,7 @@ const __ANEXT__: SlotDef = SlotDef::new("Py_am_anext", "unaryfunc").return_speci ), TokenGenerator(|_| quote! { async_iter_tag }), ); -const __LEN__: SlotDef = SlotDef::new("Py_mp_length", "lenfunc").ret_ty(Ty::PySsizeT); +pub const __LEN__: SlotDef = SlotDef::new("Py_mp_length", "lenfunc").ret_ty(Ty::PySsizeT); const __CONTAINS__: SlotDef = SlotDef::new("Py_sq_contains", "objobjproc") .arguments(&[Ty::Object]) .ret_ty(Ty::Int); @@ -944,7 +944,8 @@ const __INPLACE_CONCAT__: SlotDef = SlotDef::new("Py_sq_concat", "binaryfunc").arguments(&[Ty::Object]); const __INPLACE_REPEAT__: SlotDef = SlotDef::new("Py_sq_repeat", "ssizeargfunc").arguments(&[Ty::PySsizeT]); -const __GETITEM__: SlotDef = SlotDef::new("Py_mp_subscript", "binaryfunc").arguments(&[Ty::Object]); +pub const __GETITEM__: SlotDef = + SlotDef::new("Py_mp_subscript", "binaryfunc").arguments(&[Ty::Object]); const __POS__: SlotDef = SlotDef::new("Py_nb_positive", "unaryfunc"); const __NEG__: SlotDef = SlotDef::new("Py_nb_negative", "unaryfunc"); diff --git a/pytests/src/enums.rs b/pytests/src/enums.rs index 68a5fc93dfe..964f0d431c3 100644 --- a/pytests/src/enums.rs +++ b/pytests/src/enums.rs @@ -8,8 +8,13 @@ use pyo3::{ pub fn enums(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; m.add_wrapped(wrap_pyfunction_bound!(do_simple_stuff))?; m.add_wrapped(wrap_pyfunction_bound!(do_complex_stuff))?; + m.add_wrapped(wrap_pyfunction_bound!(do_tuple_stuff))?; + m.add_wrapped(wrap_pyfunction_bound!(do_mixed_complex_stuff))?; Ok(()) } @@ -79,3 +84,40 @@ pub fn do_complex_stuff(thing: &ComplexEnum) -> ComplexEnum { }, } } + +#[pyclass] +enum SimpleTupleEnum { + Int(i32), + Str(String), +} + +#[pyclass] +pub enum TupleEnum { + #[pyo3(constructor = (_0 = 1, _1 = 1.0, _2 = true))] + FullWithDefault(i32, f64, bool), + Full(i32, f64, bool), + EmptyTuple(), +} + +#[pyfunction] +pub fn do_tuple_stuff(thing: &TupleEnum) -> TupleEnum { + match thing { + TupleEnum::FullWithDefault(a, b, c) => TupleEnum::FullWithDefault(*a, *b, *c), + TupleEnum::Full(a, b, c) => TupleEnum::Full(*a, *b, *c), + TupleEnum::EmptyTuple() => TupleEnum::EmptyTuple(), + } +} + +#[pyclass] +pub enum MixedComplexEnum { + Nothing {}, + Empty(), +} + +#[pyfunction] +pub fn do_mixed_complex_stuff(thing: &MixedComplexEnum) -> MixedComplexEnum { + match thing { + MixedComplexEnum::Nothing {} => MixedComplexEnum::Empty(), + MixedComplexEnum::Empty() => MixedComplexEnum::Nothing {}, + } +} diff --git a/pytests/tests/test_enums.py b/pytests/tests/test_enums.py index cd1d7aedaf8..cd4f7e124c9 100644 --- a/pytests/tests/test_enums.py +++ b/pytests/tests/test_enums.py @@ -137,3 +137,67 @@ def test_complex_enum_pyfunction_in_out_desugared_match(variant: enums.ComplexEn assert y == "HELLO" else: assert False + + +def test_tuple_enum_variant_constructors(): + tuple_variant = enums.TupleEnum.Full(42, 3.14, False) + assert isinstance(tuple_variant, enums.TupleEnum.Full) + + empty_tuple_variant = enums.TupleEnum.EmptyTuple() + assert isinstance(empty_tuple_variant, enums.TupleEnum.EmptyTuple) + + +@pytest.mark.parametrize( + "variant", + [ + enums.TupleEnum.FullWithDefault(), + enums.TupleEnum.Full(42, 3.14, False), + enums.TupleEnum.EmptyTuple(), + ], +) +def test_tuple_enum_variant_subclasses(variant: enums.TupleEnum): + assert isinstance(variant, enums.TupleEnum) + + +def test_tuple_enum_defaults(): + variant = enums.TupleEnum.FullWithDefault() + assert variant._0 == 1 + assert variant._1 == 1.0 + assert variant._2 is True + + +def test_tuple_enum_field_getters(): + tuple_variant = enums.TupleEnum.Full(42, 3.14, False) + assert tuple_variant._0 == 42 + assert tuple_variant._1 == 3.14 + assert tuple_variant._2 is False + + +def test_tuple_enum_index_getter(): + tuple_variant = enums.TupleEnum.Full(42, 3.14, False) + assert len(tuple_variant) == 3 + assert tuple_variant[0] == 42 + + +@pytest.mark.parametrize( + "variant", + [enums.MixedComplexEnum.Nothing()], +) +def test_mixed_complex_enum_pyfunction_instance_nothing( + variant: enums.MixedComplexEnum, +): + assert isinstance(variant, enums.MixedComplexEnum.Nothing) + assert isinstance( + enums.do_mixed_complex_stuff(variant), enums.MixedComplexEnum.Empty + ) + + +@pytest.mark.parametrize( + "variant", + [enums.MixedComplexEnum.Empty()], +) +def test_mixed_complex_enum_pyfunction_instance_empty(variant: enums.MixedComplexEnum): + assert isinstance(variant, enums.MixedComplexEnum.Empty) + assert isinstance( + enums.do_mixed_complex_stuff(variant), enums.MixedComplexEnum.Nothing + ) diff --git a/pytests/tests/test_enums_match.py b/pytests/tests/test_enums_match.py index 4d55bbbe351..6c4b5f6aa07 100644 --- a/pytests/tests/test_enums_match.py +++ b/pytests/tests/test_enums_match.py @@ -57,3 +57,102 @@ def test_complex_enum_pyfunction_in_out(variant: enums.ComplexEnum): assert z is True case _: assert False + + +@pytest.mark.parametrize( + "variant", + [ + enums.ComplexEnum.MultiFieldStruct(42, 3.14, True), + ], +) +def test_complex_enum_partial_match(variant: enums.ComplexEnum): + match variant: + case enums.ComplexEnum.MultiFieldStruct(a): + assert a == 42 + case _: + assert False + + +@pytest.mark.parametrize( + "variant", + [ + enums.TupleEnum.Full(42, 3.14, True), + enums.TupleEnum.EmptyTuple(), + ], +) +def test_tuple_enum_match_statement(variant: enums.TupleEnum): + match variant: + case enums.TupleEnum.Full(_0=x, _1=y, _2=z): + assert x == 42 + assert y == 3.14 + assert z is True + case enums.TupleEnum.EmptyTuple(): + assert True + case _: + print(variant) + assert False + + +@pytest.mark.parametrize( + "variant", + [ + enums.SimpleTupleEnum.Int(42), + enums.SimpleTupleEnum.Str("hello"), + ], +) +def test_simple_tuple_enum_match_statement(variant: enums.SimpleTupleEnum): + match variant: + case enums.SimpleTupleEnum.Int(x): + assert x == 42 + case enums.SimpleTupleEnum.Str(x): + assert x == "hello" + case _: + assert False + + +@pytest.mark.parametrize( + "variant", + [ + enums.TupleEnum.Full(42, 3.14, True), + ], +) +def test_tuple_enum_match_match_args(variant: enums.TupleEnum): + match variant: + case enums.TupleEnum.Full(x, y, z): + assert x == 42 + assert y == 3.14 + assert z is True + assert True + case _: + assert False + + +@pytest.mark.parametrize( + "variant", + [ + enums.TupleEnum.Full(42, 3.14, True), + ], +) +def test_tuple_enum_partial_match(variant: enums.TupleEnum): + match variant: + case enums.TupleEnum.Full(a): + assert a == 42 + case _: + assert False + + +@pytest.mark.parametrize( + "variant", + [ + enums.MixedComplexEnum.Nothing(), + enums.MixedComplexEnum.Empty(), + ], +) +def test_mixed_complex_enum_match_statement(variant: enums.MixedComplexEnum): + match variant: + case enums.MixedComplexEnum.Nothing(): + assert True + case enums.MixedComplexEnum.Empty(): + assert True + case _: + assert False diff --git a/tests/ui/invalid_pyclass_enum.rs b/tests/ui/invalid_pyclass_enum.rs index 116b8968da8..e98010fea32 100644 --- a/tests/ui/invalid_pyclass_enum.rs +++ b/tests/ui/invalid_pyclass_enum.rs @@ -21,12 +21,6 @@ enum NoUnitVariants { UnitVariant, } -#[pyclass] -enum NoTupleVariants { - StructVariant { field: i32 }, - TupleVariant(i32), -} - #[pyclass] enum SimpleNoSignature { #[pyo3(constructor = (a, b))] diff --git a/tests/ui/invalid_pyclass_enum.stderr b/tests/ui/invalid_pyclass_enum.stderr index e9ba9806da8..7e3b6ffa425 100644 --- a/tests/ui/invalid_pyclass_enum.stderr +++ b/tests/ui/invalid_pyclass_enum.stderr @@ -17,23 +17,15 @@ error: #[pyclass] can't be used on enums without any variants | ^^ error: Unit variant `UnitVariant` is not yet supported in a complex enum - = help: change to a struct variant with no fields: `UnitVariant { }` + = help: change to an empty tuple variant instead: `UnitVariant()` = note: the enum is complex because of non-unit variant `StructVariant` --> tests/ui/invalid_pyclass_enum.rs:21:5 | 21 | UnitVariant, | ^^^^^^^^^^^ -error: Tuple variant `TupleVariant` is not yet supported in a complex enum - = help: change to a struct variant with named fields: `TupleVariant { /* fields */ }` - = note: the enum is complex because of non-unit variant `StructVariant` - --> tests/ui/invalid_pyclass_enum.rs:27:5 - | -27 | TupleVariant(i32), - | ^^^^^^^^^^^^ - error: `constructor` can't be used on a simple enum variant - --> tests/ui/invalid_pyclass_enum.rs:32:12 + --> tests/ui/invalid_pyclass_enum.rs:26:12 | -32 | #[pyo3(constructor = (a, b))] +26 | #[pyo3(constructor = (a, b))] | ^^^^^^^^^^^ diff --git a/tests/ui/invalid_pymethod_enum.rs b/tests/ui/invalid_pymethod_enum.rs index 9b596e087ff..5c41d19d4e7 100644 --- a/tests/ui/invalid_pymethod_enum.rs +++ b/tests/ui/invalid_pymethod_enum.rs @@ -16,4 +16,20 @@ impl ComplexEnum { } } +#[pyclass] +enum TupleEnum { + Int(i32), + Str(String), +} + +#[pymethods] +impl TupleEnum { + fn mutate_in_place(&mut self) { + *self = match self { + TupleEnum::Int(int) => TupleEnum::Str(int.to_string()), + TupleEnum::Str(string) => TupleEnum::Int(string.len() as i32), + } + } +} + fn main() {} diff --git a/tests/ui/invalid_pymethod_enum.stderr b/tests/ui/invalid_pymethod_enum.stderr index 6cf6fe89bdf..bc377d2a055 100644 --- a/tests/ui/invalid_pymethod_enum.stderr +++ b/tests/ui/invalid_pymethod_enum.stderr @@ -22,3 +22,28 @@ note: required by a bound in `PyRefMut` | pub struct PyRefMut<'p, T: PyClass> { | ^^^^^^^^^^^^^^ required by this bound in `PyRefMut` = note: this error originates in the attribute macro `pymethods` (in Nightly builds, run with -Z macro-backtrace for more info) + +error[E0271]: type mismatch resolving `::Frozen == False` + --> tests/ui/invalid_pymethod_enum.rs:27:24 + | +27 | fn mutate_in_place(&mut self) { + | ^ expected `False`, found `True` + | +note: required by a bound in `extract_pyclass_ref_mut` + --> src/impl_/extract_argument.rs + | + | pub fn extract_pyclass_ref_mut<'a, 'py: 'a, T: PyClass>( + | ^^^^^^^^^^^^^^ required by this bound in `extract_pyclass_ref_mut` + +error[E0271]: type mismatch resolving `::Frozen == False` + --> tests/ui/invalid_pymethod_enum.rs:25:1 + | +25 | #[pymethods] + | ^^^^^^^^^^^^ expected `False`, found `True` + | +note: required by a bound in `PyRefMut` + --> src/pycell.rs + | + | pub struct PyRefMut<'p, T: PyClass> { + | ^^^^^^^^^^^^^^ required by this bound in `PyRefMut` + = note: this error originates in the attribute macro `pymethods` (in Nightly builds, run with -Z macro-backtrace for more info) From 1c64a03ea084852572872c0d6b5fd029f116c807 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Fri, 17 May 2024 00:25:41 -0400 Subject: [PATCH 078/495] Move GIL counting from GILPool to GILGuard (#4188) --- src/gil.rs | 26 ++++++++++++++++++++------ src/impl_/trampoline.rs | 27 +++++++++++++++------------ src/marker.rs | 9 ++++----- 3 files changed, 39 insertions(+), 23 deletions(-) diff --git a/src/gil.rs b/src/gil.rs index a1d9feba494..b624e8c5fcd 100644 --- a/src/gil.rs +++ b/src/gil.rs @@ -167,6 +167,7 @@ impl GILGuard { /// `GILGuard::Ensured` will be returned. pub(crate) fn acquire() -> Self { if gil_is_acquired() { + increment_gil_count(); return GILGuard::Assumed; } @@ -215,6 +216,7 @@ impl GILGuard { /// as part of multi-phase interpreter initialization. pub(crate) fn acquire_unchecked() -> Self { if gil_is_acquired() { + increment_gil_count(); return GILGuard::Assumed; } @@ -222,8 +224,21 @@ impl GILGuard { #[allow(deprecated)] let pool = unsafe { mem::ManuallyDrop::new(GILPool::new()) }; + increment_gil_count(); + GILGuard::Ensured { gstate, pool } } + /// Acquires the `GILGuard` while assuming that the GIL is already held. + pub(crate) unsafe fn assume() -> Self { + increment_gil_count(); + GILGuard::Assumed + } + + /// Gets the Python token associated with this [`GILGuard`]. + #[inline] + pub fn python(&self) -> Python<'_> { + unsafe { Python::assume_gil_acquired() } + } } /// The Drop implementation for `GILGuard` will release the GIL. @@ -238,6 +253,7 @@ impl Drop for GILGuard { ffi::PyGILState_Release(*gstate); }, } + decrement_gil_count(); } } @@ -378,7 +394,6 @@ impl GILPool { /// As well as requiring the GIL, see the safety notes on `Python::new_pool`. #[inline] pub unsafe fn new() -> GILPool { - increment_gil_count(); // Update counts of PyObjects / Py that have been cloned or dropped since last acquisition #[cfg(not(pyo3_disable_reference_pool))] POOL.update_counts(Python::assume_gil_acquired()); @@ -427,7 +442,6 @@ impl Drop for GILPool { } } } - decrement_gil_count(); } } @@ -687,19 +701,19 @@ mod tests { assert_eq!(get_gil_count(), 1); let pool = unsafe { GILPool::new() }; - assert_eq!(get_gil_count(), 2); + assert_eq!(get_gil_count(), 1); let pool2 = unsafe { GILPool::new() }; - assert_eq!(get_gil_count(), 3); + assert_eq!(get_gil_count(), 1); drop(pool); - assert_eq!(get_gil_count(), 2); + assert_eq!(get_gil_count(), 1); Python::with_gil(|_| { // nested with_gil doesn't update gil count assert_eq!(get_gil_count(), 2); }); - assert_eq!(get_gil_count(), 2); + assert_eq!(get_gil_count(), 1); drop(pool2); assert_eq!(get_gil_count(), 1); diff --git a/src/impl_/trampoline.rs b/src/impl_/trampoline.rs index db493817cba..f485258e5e5 100644 --- a/src/impl_/trampoline.rs +++ b/src/impl_/trampoline.rs @@ -9,8 +9,7 @@ use std::{ panic::{self, UnwindSafe}, }; -#[allow(deprecated)] -use crate::gil::GILPool; +use crate::gil::GILGuard; use crate::{ callback::PyCallbackOutput, ffi, ffi_ptr_ext::FfiPtrExt, impl_::panic::PanicTrap, methods::IPowModulo, panic::PanicException, types::PyModule, Py, PyResult, Python, @@ -171,17 +170,19 @@ trampoline!( /// /// Panics during execution are trapped so that they don't propagate through any /// outer FFI boundary. +/// +/// The GIL must already be held when this is called. #[inline] -pub(crate) fn trampoline(body: F) -> R +pub(crate) unsafe fn trampoline(body: F) -> R where F: for<'py> FnOnce(Python<'py>) -> PyResult + UnwindSafe, R: PyCallbackOutput, { let trap = PanicTrap::new("uncaught panic at ffi boundary"); - // Necessary to construct a pool until PyO3 0.22 when the GIL Refs API is fully disabled - #[allow(deprecated)] - let pool = unsafe { GILPool::new() }; - let py = pool.python(); + + // SAFETY: This function requires the GIL to already be held. + let guard = GILGuard::assume(); + let py = guard.python(); let out = panic_result_into_callback_output( py, panic::catch_unwind(move || -> PyResult<_> { body(py) }), @@ -218,17 +219,19 @@ where /// /// # Safety /// -/// ctx must be either a valid ffi::PyObject or NULL +/// - ctx must be either a valid ffi::PyObject or NULL +/// - The GIL must already be held when this is called. #[inline] unsafe fn trampoline_unraisable(body: F, ctx: *mut ffi::PyObject) where F: for<'py> FnOnce(Python<'py>) -> PyResult<()> + UnwindSafe, { let trap = PanicTrap::new("uncaught panic at ffi boundary"); - // Necessary to construct a pool until PyO3 0.22 when the GIL Refs API is fully disabled - #[allow(deprecated)] - let pool = GILPool::new(); - let py = pool.python(); + + // SAFETY: The GIL is already held. + let guard = GILGuard::assume(); + let py = guard.python(); + if let Err(py_err) = panic::catch_unwind(move || body(py)) .unwrap_or_else(|payload| Err(PanicException::from_panic_payload(payload))) { diff --git a/src/marker.rs b/src/marker.rs index 072b882c875..e8e93c2419d 100644 --- a/src/marker.rs +++ b/src/marker.rs @@ -411,10 +411,10 @@ impl Python<'_> { where F: for<'py> FnOnce(Python<'py>) -> R, { - let _guard = GILGuard::acquire(); + let guard = GILGuard::acquire(); // SAFETY: Either the GIL was already acquired or we just created a new `GILGuard`. - f(unsafe { Python::assume_gil_acquired() }) + f(guard.python()) } /// Like [`Python::with_gil`] except Python interpreter state checking is skipped. @@ -445,10 +445,9 @@ impl Python<'_> { where F: for<'py> FnOnce(Python<'py>) -> R, { - let _guard = GILGuard::acquire_unchecked(); + let guard = GILGuard::acquire_unchecked(); - // SAFETY: Either the GIL was already acquired or we just created a new `GILGuard`. - f(Python::assume_gil_acquired()) + f(guard.python()) } } From fff053bde740d1e2bcc1bbb2dde8f7ed979c98bd Mon Sep 17 00:00:00 2001 From: Bruno Kolenbrander <59372212+mejrs@users.noreply.github.com> Date: Fri, 17 May 2024 12:55:41 +0200 Subject: [PATCH 079/495] Emit a better error for abi3 inheritance (#4185) * Emit a better error for abi3 inheritance * Update tests/test_compile_error.rs --------- Co-authored-by: David Hewitt --- pyo3-build-config/src/lib.rs | 5 +++++ src/impl_/pyclass.rs | 7 ++++++ tests/test_compile_error.rs | 3 +++ tests/ui/abi3_inheritance.rs | 10 +++++++++ tests/ui/abi3_inheritance.stderr | 24 +++++++++++++++++++++ tests/ui/abi3_nativetype_inheritance.stderr | 1 + 6 files changed, 50 insertions(+) create mode 100644 tests/ui/abi3_inheritance.rs create mode 100644 tests/ui/abi3_inheritance.stderr diff --git a/pyo3-build-config/src/lib.rs b/pyo3-build-config/src/lib.rs index 6d4ec759653..2b5e76e4b95 100644 --- a/pyo3-build-config/src/lib.rs +++ b/pyo3-build-config/src/lib.rs @@ -140,6 +140,10 @@ pub fn print_feature_cfgs() { if rustc_minor_version >= 74 { println!("cargo:rustc-cfg=invalid_from_utf8_lint"); } + + if rustc_minor_version >= 78 { + println!("cargo:rustc-cfg=diagnostic_namespace"); + } } /// Registers `pyo3`s config names as reachable cfg expressions @@ -160,6 +164,7 @@ pub fn print_expected_cfgs() { println!("cargo:rustc-check-cfg=cfg(invalid_from_utf8_lint)"); println!("cargo:rustc-check-cfg=cfg(pyo3_disable_reference_pool)"); println!("cargo:rustc-check-cfg=cfg(pyo3_leak_on_drop_without_reference_pool)"); + println!("cargo:rustc-check-cfg=cfg(diagnostic_namespace)"); // allow `Py_3_*` cfgs from the minimum supported version up to the // maximum minor version (+1 for development for the next) diff --git a/src/impl_/pyclass.rs b/src/impl_/pyclass.rs index 3ec2e329e1a..a3e466670a4 100644 --- a/src/impl_/pyclass.rs +++ b/src/impl_/pyclass.rs @@ -1102,6 +1102,13 @@ impl PyClassThreadChecker for ThreadCheckerImpl { } /// Trait denoting that this class is suitable to be used as a base type for PyClass. + +#[cfg_attr( + all(diagnostic_namespace, feature = "abi3"), + diagnostic::on_unimplemented( + note = "with the `abi3` feature enabled, PyO3 does not support subclassing native types" + ) +)] pub trait PyClassBaseType: Sized { type LayoutAsBase: PyClassObjectLayout; type BaseNativeType; diff --git a/tests/test_compile_error.rs b/tests/test_compile_error.rs index d31c558f096..2b32de2fcfa 100644 --- a/tests/test_compile_error.rs +++ b/tests/test_compile_error.rs @@ -64,4 +64,7 @@ fn test_compile_errors() { #[cfg(any(not(Py_LIMITED_API), Py_3_10))] // to avoid PyFunctionArgument for &str t.compile_fail("tests/ui/invalid_cancel_handle.rs"); t.pass("tests/ui/pymodule_missing_docs.rs"); + #[cfg(all(Py_LIMITED_API, not(feature = "experimental-async")))] + // output changes with async feature + t.compile_fail("tests/ui/abi3_inheritance.rs"); } diff --git a/tests/ui/abi3_inheritance.rs b/tests/ui/abi3_inheritance.rs new file mode 100644 index 00000000000..60972e4cf7a --- /dev/null +++ b/tests/ui/abi3_inheritance.rs @@ -0,0 +1,10 @@ +use pyo3::exceptions::PyException; +use pyo3::prelude::*; + +#[pyclass(extends=PyException)] +#[derive(Clone)] +struct MyException { + code: u32, +} + +fn main() {} diff --git a/tests/ui/abi3_inheritance.stderr b/tests/ui/abi3_inheritance.stderr new file mode 100644 index 00000000000..756df2eb75e --- /dev/null +++ b/tests/ui/abi3_inheritance.stderr @@ -0,0 +1,24 @@ +error[E0277]: the trait bound `PyException: PyClassBaseType` is not satisfied + --> tests/ui/abi3_inheritance.rs:4:19 + | +4 | #[pyclass(extends=PyException)] + | ^^^^^^^^^^^ the trait `PyClass` is not implemented for `PyException`, which is required by `PyException: PyClassBaseType` + | + = note: with the `abi3` feature enabled, PyO3 does not support subclassing native types + = help: the trait `PyClassBaseType` is implemented for `PyAny` + = note: required for `PyException` to implement `PyClassBaseType` +note: required by a bound in `PyClassImpl::BaseType` + --> src/impl_/pyclass.rs + | + | type BaseType: PyTypeInfo + PyClassBaseType; + | ^^^^^^^^^^^^^^^ required by this bound in `PyClassImpl::BaseType` + +error[E0277]: the trait bound `PyException: PyClass` is not satisfied + --> tests/ui/abi3_inheritance.rs:4:1 + | +4 | #[pyclass(extends=PyException)] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `PyClass` is not implemented for `PyException`, which is required by `PyException: PyClassBaseType` + | + = help: the trait `PyClass` is implemented for `MyException` + = note: required for `PyException` to implement `PyClassBaseType` + = note: this error originates in the attribute macro `pyclass` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/tests/ui/abi3_nativetype_inheritance.stderr b/tests/ui/abi3_nativetype_inheritance.stderr index f9ca7c61b89..5cd985ccfe5 100644 --- a/tests/ui/abi3_nativetype_inheritance.stderr +++ b/tests/ui/abi3_nativetype_inheritance.stderr @@ -4,6 +4,7 @@ error[E0277]: the trait bound `PyDict: PyClassBaseType` is not satisfied 5 | #[pyclass(extends=PyDict)] | ^^^^^^ the trait `PyClass` is not implemented for `PyDict`, which is required by `PyDict: PyClassBaseType` | + = note: with the `abi3` feature enabled, PyO3 does not support subclassing native types = help: the trait `PyClassBaseType` is implemented for `PyAny` = note: required for `PyDict` to implement `PyClassBaseType` note: required by a bound in `PyClassImpl::BaseType` From fe79f548174eb8108813f202bb0df9428ddfd806 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Fri, 17 May 2024 07:31:52 -0400 Subject: [PATCH 080/495] feature gate deprecated APIs for `GILPool` (#4181) --- src/gil.rs | 77 +++++++++++++++++++++++++------------------ src/impl_/not_send.rs | 1 + src/lib.rs | 1 + src/marker.rs | 4 +-- 4 files changed, 48 insertions(+), 35 deletions(-) diff --git a/src/gil.rs b/src/gil.rs index b624e8c5fcd..8689cde2c9d 100644 --- a/src/gil.rs +++ b/src/gil.rs @@ -1,5 +1,6 @@ //! Interaction with Python's global interpreter lock +#[cfg(feature = "gil-refs")] use crate::impl_::not_send::{NotSend, NOT_SEND}; #[cfg(pyo3_disable_reference_pool)] use crate::impl_::panic::PanicTrap; @@ -127,19 +128,16 @@ where ffi::Py_InitializeEx(0); - // Safety: the GIL is already held because of the Py_IntializeEx call. - #[allow(deprecated)] // TODO: remove this with the GIL Refs feature in 0.22 - let pool = GILPool::new(); - - // Import the threading module - this ensures that it will associate this thread as the "main" - // thread, which is important to avoid an `AssertionError` at finalization. - pool.python().import_bound("threading").unwrap(); + let result = { + let guard = GILGuard::assume(); + let py = guard.python(); + // Import the threading module - this ensures that it will associate this thread as the "main" + // thread, which is important to avoid an `AssertionError` at finalization. + py.import_bound("threading").unwrap(); - // Execute the closure. - let result = f(pool.python()); - - // Drop the pool before finalizing. - drop(pool); + // Execute the closure. + f(py) + }; // Finalize the Python interpreter. ffi::Py_Finalize(); @@ -154,7 +152,8 @@ pub(crate) enum GILGuard { /// Indicates that we actually acquired the GIL when this GILGuard was acquired Ensured { gstate: ffi::PyGILState_STATE, - #[allow(deprecated)] // TODO: remove this with the gil-refs feature in 0.22 + #[cfg(feature = "gil-refs")] + #[allow(deprecated)] pool: mem::ManuallyDrop, }, } @@ -221,12 +220,23 @@ impl GILGuard { } let gstate = unsafe { ffi::PyGILState_Ensure() }; // acquire GIL + #[cfg(feature = "gil-refs")] #[allow(deprecated)] - let pool = unsafe { mem::ManuallyDrop::new(GILPool::new()) }; + { + let pool = unsafe { mem::ManuallyDrop::new(GILPool::new()) }; + increment_gil_count(); + GILGuard::Ensured { gstate, pool } + } - increment_gil_count(); + #[cfg(not(feature = "gil-refs"))] + { + increment_gil_count(); + // Update counts of PyObjects / Py that have been cloned or dropped since last acquisition + #[cfg(not(pyo3_disable_reference_pool))] + POOL.update_counts(unsafe { Python::assume_gil_acquired() }); - GILGuard::Ensured { gstate, pool } + GILGuard::Ensured { gstate } + } } /// Acquires the `GILGuard` while assuming that the GIL is already held. pub(crate) unsafe fn assume() -> Self { @@ -246,12 +256,17 @@ impl Drop for GILGuard { fn drop(&mut self) { match self { GILGuard::Assumed => {} + #[cfg(feature = "gil-refs")] GILGuard::Ensured { gstate, pool } => unsafe { // Drop the objects in the pool before attempting to release the thread state mem::ManuallyDrop::drop(pool); ffi::PyGILState_Release(*gstate); }, + #[cfg(not(feature = "gil-refs"))] + GILGuard::Ensured { gstate } => unsafe { + ffi::PyGILState_Release(*gstate); + }, } decrement_gil_count(); } @@ -368,12 +383,10 @@ impl Drop for LockGIL { /// /// [Memory Management]: https://pyo3.rs/main/memory.html#gil-bound-memory -#[cfg_attr( - not(feature = "gil-refs"), - deprecated( - since = "0.21.0", - note = "`GILPool` has no function if PyO3's deprecated GIL Refs API is not used" - ) +#[cfg(feature = "gil-refs")] +#[deprecated( + since = "0.21.0", + note = "`GILPool` has no function if PyO3's deprecated GIL Refs API is not used" )] pub struct GILPool { /// Initial length of owned objects and anys. @@ -382,6 +395,7 @@ pub struct GILPool { _not_send: NotSend, } +#[cfg(feature = "gil-refs")] #[allow(deprecated)] impl GILPool { /// Creates a new [`GILPool`]. This function should only ever be called with the GIL held. @@ -419,6 +433,7 @@ impl GILPool { } } +#[cfg(feature = "gil-refs")] #[allow(deprecated)] impl Drop for GILPool { fn drop(&mut self) { @@ -534,19 +549,15 @@ fn decrement_gil_count() { #[cfg(test)] mod tests { - #[allow(deprecated)] - use super::GILPool; #[cfg(not(pyo3_disable_reference_pool))] - use super::POOL; - use super::{gil_is_acquired, GIL_COUNT}; - #[cfg(not(pyo3_disable_reference_pool))] - use crate::ffi; + use super::{gil_is_acquired, POOL}; + #[cfg(feature = "gil-refs")] + #[allow(deprecated)] + use super::{GILPool, GIL_COUNT, OWNED_OBJECTS}; use crate::types::any::PyAnyMethods; - use crate::{PyObject, Python}; #[cfg(feature = "gil-refs")] - use {super::OWNED_OBJECTS, crate::gil}; - - #[cfg(not(pyo3_disable_reference_pool))] + use crate::{ffi, gil}; + use crate::{PyObject, Python}; use std::ptr::NonNull; fn get_object(py: Python<'_>) -> PyObject { @@ -691,6 +702,7 @@ mod tests { } #[test] + #[cfg(feature = "gil-refs")] #[allow(deprecated)] fn test_gil_counts() { // Check with_gil and GILPool both increase counts correctly @@ -782,6 +794,7 @@ mod tests { #[test] #[cfg(not(pyo3_disable_reference_pool))] + #[cfg(feature = "gil-refs")] fn test_update_counts_does_not_deadlock() { // update_counts can run arbitrary Python code during Py_DECREF. // if the locking is implemented incorrectly, it will deadlock. diff --git a/src/impl_/not_send.rs b/src/impl_/not_send.rs index 97c2984aff8..382e07a14ee 100644 --- a/src/impl_/not_send.rs +++ b/src/impl_/not_send.rs @@ -6,4 +6,5 @@ use crate::Python; /// Workaround for lack of !Send on stable (). pub(crate) struct NotSend(PhantomData<*mut Python<'static>>); +#[cfg(feature = "gil-refs")] pub(crate) const NOT_SEND: NotSend = NotSend(PhantomData); diff --git a/src/lib.rs b/src/lib.rs index b1d8ae6c7cf..a9c5bd0b731 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -323,6 +323,7 @@ pub use crate::conversion::{FromPyPointer, PyTryFrom, PyTryInto}; #[cfg(feature = "gil-refs")] pub use crate::err::PyDowncastError; pub use crate::err::{DowncastError, DowncastIntoError, PyErr, PyErrArguments, PyResult, ToPyErr}; +#[cfg(feature = "gil-refs")] #[allow(deprecated)] pub use crate::gil::GILPool; #[cfg(not(any(PyPy, GraalPy)))] diff --git a/src/marker.rs b/src/marker.rs index e8e93c2419d..1f4655d9656 100644 --- a/src/marker.rs +++ b/src/marker.rs @@ -126,12 +126,10 @@ use crate::types::{ PyAny, PyDict, PyEllipsis, PyModule, PyNone, PyNotImplemented, PyString, PyType, }; use crate::version::PythonVersionInfo; -#[cfg(feature = "gil-refs")] -use crate::PyNativeType; use crate::{ffi, Bound, IntoPy, Py, PyObject, PyTypeInfo}; #[allow(deprecated)] #[cfg(feature = "gil-refs")] -use crate::{gil::GILPool, FromPyPointer}; +use crate::{gil::GILPool, FromPyPointer, PyNativeType}; use std::ffi::{CStr, CString}; use std::marker::PhantomData; use std::os::raw::c_int; From ac273a16122deadad0cabd09bff1457ddf68e277 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Sun, 19 May 2024 09:39:29 -0400 Subject: [PATCH 081/495] docs: minor updates to pyenv installs (#4189) --- Contributing.md | 4 ---- guide/src/getting-started.md | 9 ++------- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/Contributing.md b/Contributing.md index 111e814ac8f..1503f803e80 100644 --- a/Contributing.md +++ b/Contributing.md @@ -23,10 +23,6 @@ To work and develop PyO3, you need Python & Rust installed on your system. * [virtualenv](https://virtualenv.pypa.io/en/latest/) can also be used with or without Pyenv to use specific installed Python versions. * [`nox`][nox] is used to automate many of our CI tasks. -### Caveats - -* When using pyenv on macOS, installing a Python version using `--enable-shared` is required to make it work. i.e `env PYTHON_CONFIGURE_OPTS="--enable-shared" pyenv install 3.7.12` - ### Help users identify bugs The [PyO3 Discord server](https://discord.gg/33kcChzH7f) is very active with users who are new to PyO3, and often completely new to Rust. Helping them debug is a great way to get experience with the PyO3 codebase. diff --git a/guide/src/getting-started.md b/guide/src/getting-started.md index 94ab95cb3d6..5dbffaba99c 100644 --- a/guide/src/getting-started.md +++ b/guide/src/getting-started.md @@ -18,19 +18,14 @@ To use PyO3, you need at least Python 3.7. While you can simply use the default While you can use any virtualenv manager you like, we recommend the use of `pyenv` in particular if you want to develop or test for multiple different Python versions, so that is what the examples in this book will use. The installation instructions for `pyenv` can be found [here](https://github.com/pyenv/pyenv#getting-pyenv). (Note: To get the `pyenv activate` and `pyenv virtualenv` commands, you will also need to install the [`pyenv-virtualenv`](https://github.com/pyenv/pyenv-virtualenv) plugin. The [pyenv installer](https://github.com/pyenv/pyenv-installer#installation--update--uninstallation) will install both together.) -If you intend to run Python from Rust (for example in unit tests) you should set the following environment variable when installing a new Python version using `pyenv`: -```bash -PYTHON_CONFIGURE_OPTS="--enable-shared" -``` +It can be useful to keep the sources used when installing using `pyenv` so that future debugging can see the original source files. This can be done by passing the `--keep` flag as part of the `pyenv install` command. For example: ```bash -env PYTHON_CONFIGURE_OPTS="--enable-shared" pyenv install 3.12 +pyenv install 3.12 --keep ``` -You can read more about `pyenv`'s configuration options [here](https://github.com/pyenv/pyenv/blob/master/plugins/python-build/README.md#building-with---enable-shared). - ### Building There are a number of build and Python package management systems such as [`setuptools-rust`](https://github.com/PyO3/setuptools-rust) or [manually](./building-and-distribution.md#manual-builds). We recommend the use of `maturin`, which you can install [here](https://maturin.rs/installation.html). It is developed to work with PyO3 and provides the most "batteries included" experience, especially if you are aiming to publish to PyPI. `maturin` is just a Python package, so you can add it in the same way you already install Python packages. From 674708cb4c7fc41b7a5328f4e51a797b42388d9e Mon Sep 17 00:00:00 2001 From: Adam Reichold Date: Sun, 19 May 2024 15:40:55 +0200 Subject: [PATCH 082/495] Remove OWNED_OBJECTS thread local when GILPool is disabled. (#4193) --- src/gil.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/gil.rs b/src/gil.rs index 8689cde2c9d..6f97011b71c 100644 --- a/src/gil.rs +++ b/src/gil.rs @@ -6,9 +6,9 @@ use crate::impl_::not_send::{NotSend, NOT_SEND}; use crate::impl_::panic::PanicTrap; use crate::{ffi, Python}; use std::cell::Cell; -#[cfg(debug_assertions)] +#[cfg(all(feature = "gil-refs", debug_assertions))] use std::cell::RefCell; -#[cfg(not(debug_assertions))] +#[cfg(all(feature = "gil-refs", not(debug_assertions)))] use std::cell::UnsafeCell; use std::{mem, ptr::NonNull, sync}; @@ -27,9 +27,9 @@ std::thread_local! { static GIL_COUNT: Cell = const { Cell::new(0) }; /// Temporarily hold objects that will be released when the GILPool drops. - #[cfg(debug_assertions)] + #[cfg(all(feature = "gil-refs", debug_assertions))] static OWNED_OBJECTS: RefCell = const { RefCell::new(Vec::new()) }; - #[cfg(not(debug_assertions))] + #[cfg(all(feature = "gil-refs", not(debug_assertions)))] static OWNED_OBJECTS: UnsafeCell = const { UnsafeCell::new(Vec::new()) }; } From 3e4b3c5c52e06d6003a4a7be200ad8feb21e50ad Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Sun, 19 May 2024 16:13:11 -0400 Subject: [PATCH 083/495] docs: attempt to clarify magic methods supported by PyO3 (#4190) * docs: attempt to clarify magic methods supported by PyO3 * Update guide/src/class/protocols.md Co-authored-by: Icxolu <10486322+Icxolu@users.noreply.github.com> --------- Co-authored-by: Icxolu <10486322+Icxolu@users.noreply.github.com> --- guide/src/class/protocols.md | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/guide/src/class/protocols.md b/guide/src/class/protocols.md index 3b12fd531c3..4e5f6010e6d 100644 --- a/guide/src/class/protocols.md +++ b/guide/src/class/protocols.md @@ -1,20 +1,27 @@ -# Magic methods and slots +# Class customizations -Python's object model defines several protocols for different object behavior, such as the sequence, mapping, and number protocols. You may be familiar with implementing these protocols in Python classes by "magic" methods, such as `__str__` or `__repr__`. Because of the double-underscores surrounding their name, these are also known as "dunder" methods. +Python's object model defines several protocols for different object behavior, such as the sequence, mapping, and number protocols. Python classes support these protocols by implementing "magic" methods, such as `__str__` or `__repr__`. Because of the double-underscores surrounding their name, these are also known as "dunder" methods. -In the Python C-API which PyO3 is implemented upon, many of these magic methods have to be placed into special "slots" on the class type object, as covered in the previous section. +PyO3 makes it possible for every magic method to be implemented in `#[pymethods]` just as they would be done in a regular Python class, with a few notable differences: +- `__new__` and `__init__` are replaced by the [`#[new]` attribute](../class.md#constructor). +- `__del__` is not yet supported, but may be in the future. +- `__buffer__` and `__release_buffer__` are currently not supported and instead PyO3 supports [`__getbuffer__` and `__releasebuffer__`](#buffer-objects) methods (these predate [PEP 688](https://peps.python.org/pep-0688/#python-level-buffer-protocol)), again this may change in the future. +- PyO3 adds [`__traverse__` and `__clear__`](#garbage-collector-integration) methods for controlling garbage collection. +- The Python C-API which PyO3 is implemented upon requires many magic methods to have a specific function signature in C and be placed into special "slots" on the class type object. This limits the allowed argument and return types for these methods. They are listed in detail in the section below. -If a function name in `#[pymethods]` is a recognised magic method, it will be automatically placed into the correct slot in the Python type object. The function name is taken from the usual rules for naming `#[pymethods]`: the `#[pyo3(name = "...")]` attribute is used if present, otherwise the Rust function name is used. +If a magic method is not on the list above (for example `__init_subclass__`), then it should just work in PyO3. If this is not the case, please file a bug report. -The magic methods handled by PyO3 are very similar to the standard Python ones on [this page](https://docs.python.org/3/reference/datamodel.html#special-method-names) - in particular they are the subset which have slots as [defined here](https://docs.python.org/3/c-api/typeobj.html). Some of the slots do not have a magic method in Python, which leads to a few additional magic methods defined only in PyO3: - - Magic methods for garbage collection - - Magic methods for the buffer protocol +## Magic Methods handled by PyO3 + +If a function name in `#[pymethods]` is a magic method which is known to need special handling, it will be automatically placed into the correct slot in the Python type object. The function name is taken from the usual rules for naming `#[pymethods]`: the `#[pyo3(name = "...")]` attribute is used if present, otherwise the Rust function name is used. + +The magic methods handled by PyO3 are very similar to the standard Python ones on [this page](https://docs.python.org/3/reference/datamodel.html#special-method-names) - in particular they are the subset which have slots as [defined here](https://docs.python.org/3/c-api/typeobj.html). When PyO3 handles a magic method, a couple of changes apply compared to other `#[pymethods]`: - The Rust function signature is restricted to match the magic method. - The `#[pyo3(signature = (...)]` and `#[pyo3(text_signature = "...")]` attributes are not allowed. -The following sections list of all magic methods PyO3 currently handles. The +The following sections list all magic methods for which PyO3 implements the necessary special handling. The given signatures should be interpreted as follows: - All methods take a receiver as first argument, shown as ``. It can be `&self`, `&mut self` or a `Bound` reference like `self_: PyRef<'_, Self>` and @@ -31,7 +38,6 @@ given signatures should be interpreted as follows: checked by the Python interpreter. For example, `__str__` needs to return a string object. This is indicated by `object (Python type)`. - ### Basic object customization - `__str__() -> object (str)` From 81ba9a8cd529a7b70b63a983628e424629c998ee Mon Sep 17 00:00:00 2001 From: Cheuk Ting Ho Date: Tue, 21 May 2024 19:24:06 +0100 Subject: [PATCH 084/495] Include import hook in getting-started.md (#4198) --- guide/src/getting-started.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/guide/src/getting-started.md b/guide/src/getting-started.md index 5dbffaba99c..ede48d50c33 100644 --- a/guide/src/getting-started.md +++ b/guide/src/getting-started.md @@ -176,3 +176,7 @@ $ python ``` For more instructions on how to use Python code from Rust, see the [Python from Rust](python-from-rust.md) page. + +## Maturin Import Hook + +In development, any changes in the code would require running `maturin develop` before testing. To streamline the development process, you may want to install [Maturin Import Hook](https://github.com/PyO3/maturin-import-hook) which will run `maturin develop` automatically when the library with code changes is being imported. From 2c654b2906b7267c5c6a6d5cd75f0340676ee99c Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Tue, 21 May 2024 15:27:20 -0400 Subject: [PATCH 085/495] ci: adjust test to avoid type inference (#4199) --- src/pybacked.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pybacked.rs b/src/pybacked.rs index ea5face516b..ed68ea52ec7 100644 --- a/src/pybacked.rs +++ b/src/pybacked.rs @@ -291,9 +291,9 @@ mod test { #[test] fn py_backed_bytes_empty() { Python::with_gil(|py| { - let b = PyBytes::new_bound(py, &[]); + let b = PyBytes::new_bound(py, b""); let py_backed_bytes = b.extract::().unwrap(); - assert_eq!(&*py_backed_bytes, &[]); + assert_eq!(&*py_backed_bytes, b""); }); } From d21045cbc1a2df7edff12bc7d7911457ce3d408d Mon Sep 17 00:00:00 2001 From: Cheuk Ting Ho Date: Sat, 25 May 2024 23:39:48 +0100 Subject: [PATCH 086/495] adding new getter for type obj (#4197) * adding new getter for type obj * fixing limited api build * fix formating ssues from clippy * add changelog info * Update newsfragments/4197.added.md Co-authored-by: David Hewitt * Update src/types/typeobject.rs Co-authored-by: David Hewitt * Update src/types/typeobject.rs Co-authored-by: David Hewitt * Update src/types/typeobject.rs Co-authored-by: David Hewitt * Update src/types/typeobject.rs Co-authored-by: David Hewitt * using uncheck downcast * fix formating * move import * Update src/types/typeobject.rs Co-authored-by: Matt Hooks * Update src/types/typeobject.rs Co-authored-by: Matt Hooks --------- Co-authored-by: David Hewitt Co-authored-by: Matt Hooks --- newsfragments/4197.added.md | 1 + src/types/typeobject.rs | 97 ++++++++++++++++++++++++++++++++++++- 2 files changed, 96 insertions(+), 2 deletions(-) create mode 100644 newsfragments/4197.added.md diff --git a/newsfragments/4197.added.md b/newsfragments/4197.added.md new file mode 100644 index 00000000000..5652028cb76 --- /dev/null +++ b/newsfragments/4197.added.md @@ -0,0 +1 @@ +Add `PyTypeMethods::mro` and `PyTypeMethods::bases`. diff --git a/src/types/typeobject.rs b/src/types/typeobject.rs index e6c4a2180d9..9c2d5c5f2c4 100644 --- a/src/types/typeobject.rs +++ b/src/types/typeobject.rs @@ -1,6 +1,7 @@ use crate::err::{self, PyResult}; use crate::instance::Borrowed; use crate::types::any::PyAnyMethods; +use crate::types::PyTuple; #[cfg(feature = "gil-refs")] use crate::PyNativeType; use crate::{ffi, Bound, PyAny, PyTypeInfo, Python}; @@ -127,6 +128,16 @@ pub trait PyTypeMethods<'py>: crate::sealed::Sealed { fn is_subclass_of(&self) -> PyResult where T: PyTypeInfo; + + /// Return the method resolution order for this type. + /// + /// Equivalent to the Python expression `self.__mro__`. + fn mro(&self) -> Bound<'py, PyTuple>; + + /// Return Python bases + /// + /// Equivalent to the Python expression `self.__bases__`. + fn bases(&self) -> Bound<'py, PyTuple>; } impl<'py> PyTypeMethods<'py> for Bound<'py, PyType> { @@ -177,6 +188,48 @@ impl<'py> PyTypeMethods<'py> for Bound<'py, PyType> { { self.is_subclass(&T::type_object_bound(self.py())) } + + fn mro(&self) -> Bound<'py, PyTuple> { + #[cfg(any(Py_LIMITED_API, PyPy))] + let mro = self + .getattr(intern!(self.py(), "__mro__")) + .expect("Cannot get `__mro__` from object.") + .extract() + .expect("Unexpected type in `__mro__` attribute."); + + #[cfg(not(any(Py_LIMITED_API, PyPy)))] + let mro = unsafe { + use crate::ffi_ptr_ext::FfiPtrExt; + (*self.as_type_ptr()) + .tp_mro + .assume_borrowed(self.py()) + .to_owned() + .downcast_into_unchecked() + }; + + mro + } + + fn bases(&self) -> Bound<'py, PyTuple> { + #[cfg(any(Py_LIMITED_API, PyPy))] + let bases = self + .getattr(intern!(self.py(), "__bases__")) + .expect("Cannot get `__bases__` from object.") + .extract() + .expect("Unexpected type in `__bases__` attribute."); + + #[cfg(not(any(Py_LIMITED_API, PyPy)))] + let bases = unsafe { + use crate::ffi_ptr_ext::FfiPtrExt; + (*self.as_type_ptr()) + .tp_bases + .assume_borrowed(self.py()) + .to_owned() + .downcast_into_unchecked() + }; + + bases + } } impl<'a> Borrowed<'a, '_, PyType> { @@ -215,8 +268,8 @@ impl<'a> Borrowed<'a, '_, PyType> { #[cfg(test)] mod tests { - use crate::types::typeobject::PyTypeMethods; - use crate::types::{PyBool, PyLong}; + use crate::types::{PyAnyMethods, PyBool, PyInt, PyLong, PyTuple, PyTypeMethods}; + use crate::PyAny; use crate::Python; #[test] @@ -237,4 +290,44 @@ mod tests { .unwrap()); }); } + + #[test] + fn test_mro() { + Python::with_gil(|py| { + assert!(py + .get_type_bound::() + .mro() + .eq(PyTuple::new_bound( + py, + [ + py.get_type_bound::(), + py.get_type_bound::(), + py.get_type_bound::() + ] + )) + .unwrap()); + }); + } + + #[test] + fn test_bases_bool() { + Python::with_gil(|py| { + assert!(py + .get_type_bound::() + .bases() + .eq(PyTuple::new_bound(py, [py.get_type_bound::()])) + .unwrap()); + }); + } + + #[test] + fn test_bases_object() { + Python::with_gil(|py| { + assert!(py + .get_type_bound::() + .bases() + .eq(PyTuple::empty_bound(py)) + .unwrap()); + }); + } } From 388d1760b5d6545c94925dafe0d640200b9fded2 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Sat, 25 May 2024 23:41:26 +0100 Subject: [PATCH 087/495] ci: start testing on 3.13-dev (#4184) * ci: start testing on 3.13-dev * ffi fixes for 3.13 beta 1 * support 3.13 * move gevent to be binary-only * adjust for div_ceil * fixup pytests --- .github/workflows/ci.yml | 1 + newsfragments/4184.packaging.md | 1 + noxfile.py | 12 +-- pyo3-ffi/build.rs | 2 +- pyo3-ffi/src/cpython/code.rs | 11 ++- pyo3-ffi/src/cpython/compile.rs | 4 +- pyo3-ffi/src/cpython/import.rs | 2 +- pyo3-ffi/src/cpython/initconfig.rs | 4 + pyo3-ffi/src/cpython/longobject.rs | 74 ++++++++++++++++ pyo3-ffi/src/cpython/mod.rs | 2 + pyo3-ffi/src/cpython/object.rs | 2 + pyo3-ffi/src/longobject.rs | 27 +----- pytests/noxfile.py | 15 ++-- pytests/pyproject.toml | 1 - pytests/tests/test_misc.py | 17 ++-- src/conversions/num_bigint.rs | 134 ++++++++++++++++++++++------- src/conversions/std/num.rs | 99 +++++++++++++++------ 17 files changed, 302 insertions(+), 106 deletions(-) create mode 100644 newsfragments/4184.packaging.md create mode 100644 pyo3-ffi/src/cpython/longobject.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0c245f33483..5bf2b7ea1e8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -224,6 +224,7 @@ jobs: "3.10", "3.11", "3.12", + "3.13-dev", "pypy3.7", "pypy3.8", "pypy3.9", diff --git a/newsfragments/4184.packaging.md b/newsfragments/4184.packaging.md new file mode 100644 index 00000000000..c12302a7029 --- /dev/null +++ b/newsfragments/4184.packaging.md @@ -0,0 +1 @@ +Support Python 3.13. diff --git a/noxfile.py b/noxfile.py index 84676b1ff0c..2383e2f865f 100644 --- a/noxfile.py +++ b/noxfile.py @@ -30,7 +30,7 @@ PYO3_GUIDE_SRC = PYO3_DIR / "guide" / "src" PYO3_GUIDE_TARGET = PYO3_TARGET / "guide" PYO3_DOCS_TARGET = PYO3_TARGET / "doc" -PY_VERSIONS = ("3.7", "3.8", "3.9", "3.10", "3.11", "3.12") +PY_VERSIONS = ("3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13") PYPY_VERSIONS = ("3.7", "3.8", "3.9", "3.10") @@ -631,11 +631,11 @@ def test_version_limits(session: nox.Session): config_file.set("CPython", "3.6") _run_cargo(session, "check", env=env, expect_error=True) - assert "3.13" not in PY_VERSIONS - config_file.set("CPython", "3.13") + assert "3.14" not in PY_VERSIONS + config_file.set("CPython", "3.14") _run_cargo(session, "check", env=env, expect_error=True) - # 3.13 CPython should build with forward compatibility + # 3.14 CPython should build with forward compatibility env["PYO3_USE_ABI3_FORWARD_COMPATIBILITY"] = "1" _run_cargo(session, "check", env=env) @@ -734,7 +734,9 @@ def update_ui_tests(session: nox.Session): def _build_docs_for_ffi_check(session: nox.Session) -> None: # pyo3-ffi-check needs to scrape docs of pyo3-ffi - _run_cargo(session, "doc", _FFI_CHECK, "-p", "pyo3-ffi", "--no-deps") + env = os.environ.copy() + env["PYO3_PYTHON"] = sys.executable + _run_cargo(session, "doc", _FFI_CHECK, "-p", "pyo3-ffi", "--no-deps", env=env) @lru_cache() diff --git a/pyo3-ffi/build.rs b/pyo3-ffi/build.rs index 23f03f1a636..0f4931d6dc7 100644 --- a/pyo3-ffi/build.rs +++ b/pyo3-ffi/build.rs @@ -17,7 +17,7 @@ const SUPPORTED_VERSIONS_CPYTHON: SupportedVersions = SupportedVersions { min: PythonVersion { major: 3, minor: 7 }, max: PythonVersion { major: 3, - minor: 12, + minor: 13, }, }; diff --git a/pyo3-ffi/src/cpython/code.rs b/pyo3-ffi/src/cpython/code.rs index 74586eac595..86e862f21ab 100644 --- a/pyo3-ffi/src/cpython/code.rs +++ b/pyo3-ffi/src/cpython/code.rs @@ -20,7 +20,11 @@ pub const _PY_MONITORING_EVENTS: usize = 17; #[repr(C)] #[derive(Clone, Copy)] pub struct _Py_LocalMonitors { - pub tools: [u8; _PY_MONITORING_UNGROUPED_EVENTS], + pub tools: [u8; if cfg!(Py_3_13) { + _PY_MONITORING_LOCAL_EVENTS + } else { + _PY_MONITORING_UNGROUPED_EVENTS + }], } #[cfg(Py_3_12)] @@ -102,6 +106,9 @@ pub struct PyCodeObject { pub co_extra: *mut c_void, } +#[cfg(Py_3_13)] +opaque_struct!(_PyExecutorArray); + #[cfg(all(not(any(PyPy, GraalPy)), Py_3_8, not(Py_3_11)))] #[repr(C)] #[derive(Copy, Clone)] @@ -176,6 +183,8 @@ pub struct PyCodeObject { pub _co_code: *mut PyObject, #[cfg(not(Py_3_12))] pub _co_linearray: *mut c_char, + #[cfg(Py_3_13)] + pub co_executors: *mut _PyExecutorArray, #[cfg(Py_3_12)] pub _co_cached: *mut _PyCoCached, #[cfg(Py_3_12)] diff --git a/pyo3-ffi/src/cpython/compile.rs b/pyo3-ffi/src/cpython/compile.rs index 8bce9dacb3b..79f06c92003 100644 --- a/pyo3-ffi/src/cpython/compile.rs +++ b/pyo3-ffi/src/cpython/compile.rs @@ -30,7 +30,7 @@ pub struct PyCompilerFlags { // skipped non-limited _PyCompilerFlags_INIT -#[cfg(all(Py_3_12, not(any(PyPy, GraalPy))))] +#[cfg(all(Py_3_12, not(any(Py_3_13, PyPy, GraalPy))))] #[repr(C)] #[derive(Copy, Clone)] pub struct _PyCompilerSrcLocation { @@ -42,7 +42,7 @@ pub struct _PyCompilerSrcLocation { // skipped SRC_LOCATION_FROM_AST -#[cfg(not(any(PyPy, GraalPy)))] +#[cfg(not(any(PyPy, GraalPy, Py_3_13)))] #[repr(C)] #[derive(Copy, Clone)] pub struct PyFutureFeatures { diff --git a/pyo3-ffi/src/cpython/import.rs b/pyo3-ffi/src/cpython/import.rs index aafd71a8355..697d68a419c 100644 --- a/pyo3-ffi/src/cpython/import.rs +++ b/pyo3-ffi/src/cpython/import.rs @@ -57,7 +57,7 @@ pub struct _frozen { pub size: c_int, #[cfg(Py_3_11)] pub is_package: c_int, - #[cfg(Py_3_11)] + #[cfg(all(Py_3_11, not(Py_3_13)))] pub get_code: Option *mut PyObject>, } diff --git a/pyo3-ffi/src/cpython/initconfig.rs b/pyo3-ffi/src/cpython/initconfig.rs index 17fe7559e1b..32931415888 100644 --- a/pyo3-ffi/src/cpython/initconfig.rs +++ b/pyo3-ffi/src/cpython/initconfig.rs @@ -141,6 +141,8 @@ pub struct PyConfig { pub safe_path: c_int, #[cfg(Py_3_12)] pub int_max_str_digits: c_int, + #[cfg(Py_3_13)] + pub cpu_count: c_int, pub pathconfig_warnings: c_int, #[cfg(Py_3_10)] pub program_name: *mut wchar_t, @@ -165,6 +167,8 @@ pub struct PyConfig { pub run_command: *mut wchar_t, pub run_module: *mut wchar_t, pub run_filename: *mut wchar_t, + #[cfg(Py_3_13)] + pub sys_path_0: *mut wchar_t, pub _install_importlib: c_int, pub _init_main: c_int, #[cfg(all(Py_3_9, not(Py_3_12)))] diff --git a/pyo3-ffi/src/cpython/longobject.rs b/pyo3-ffi/src/cpython/longobject.rs new file mode 100644 index 00000000000..45acaae577d --- /dev/null +++ b/pyo3-ffi/src/cpython/longobject.rs @@ -0,0 +1,74 @@ +use crate::longobject::*; +use crate::object::*; +#[cfg(Py_3_13)] +use crate::pyport::Py_ssize_t; +use libc::size_t; +#[cfg(Py_3_13)] +use std::os::raw::c_void; +use std::os::raw::{c_int, c_uchar}; + +#[cfg(Py_3_13)] +extern "C" { + pub fn PyLong_FromUnicodeObject(u: *mut PyObject, base: c_int) -> *mut PyObject; +} + +#[cfg(Py_3_13)] +pub const Py_ASNATIVEBYTES_DEFAULTS: c_int = -1; +#[cfg(Py_3_13)] +pub const Py_ASNATIVEBYTES_BIG_ENDIAN: c_int = 0; +#[cfg(Py_3_13)] +pub const Py_ASNATIVEBYTES_LITTLE_ENDIAN: c_int = 1; +#[cfg(Py_3_13)] +pub const Py_ASNATIVEBYTES_NATIVE_ENDIAN: c_int = 3; +#[cfg(Py_3_13)] +pub const Py_ASNATIVEBYTES_UNSIGNED_BUFFER: c_int = 4; +#[cfg(Py_3_13)] +pub const Py_ASNATIVEBYTES_REJECT_NEGATIVE: c_int = 8; + +extern "C" { + // skipped _PyLong_Sign + + #[cfg(Py_3_13)] + pub fn PyLong_AsNativeBytes( + v: *mut PyObject, + buffer: *mut c_void, + n_bytes: Py_ssize_t, + flags: c_int, + ) -> Py_ssize_t; + + #[cfg(Py_3_13)] + pub fn PyLong_FromNativeBytes( + buffer: *const c_void, + n_bytes: size_t, + flags: c_int, + ) -> *mut PyObject; + + #[cfg(Py_3_13)] + pub fn PyLong_FromUnsignedNativeBytes( + buffer: *const c_void, + n_bytes: size_t, + flags: c_int, + ) -> *mut PyObject; + + // skipped PyUnstable_Long_IsCompact + // skipped PyUnstable_Long_CompactValue + + #[cfg_attr(PyPy, link_name = "_PyPyLong_FromByteArray")] + pub fn _PyLong_FromByteArray( + bytes: *const c_uchar, + n: size_t, + little_endian: c_int, + is_signed: c_int, + ) -> *mut PyObject; + + #[cfg_attr(PyPy, link_name = "_PyPyLong_AsByteArrayO")] + pub fn _PyLong_AsByteArray( + v: *mut PyLongObject, + bytes: *mut c_uchar, + n: size_t, + little_endian: c_int, + is_signed: c_int, + ) -> c_int; + + // skipped _PyLong_GCD +} diff --git a/pyo3-ffi/src/cpython/mod.rs b/pyo3-ffi/src/cpython/mod.rs index 1ab0e3c893f..1710dbc4122 100644 --- a/pyo3-ffi/src/cpython/mod.rs +++ b/pyo3-ffi/src/cpython/mod.rs @@ -18,6 +18,7 @@ pub(crate) mod import; pub(crate) mod initconfig; // skipped interpreteridobject.h pub(crate) mod listobject; +pub(crate) mod longobject; #[cfg(all(Py_3_9, not(PyPy)))] pub(crate) mod methodobject; pub(crate) mod object; @@ -53,6 +54,7 @@ pub use self::import::*; #[cfg(all(Py_3_8, not(PyPy)))] pub use self::initconfig::*; pub use self::listobject::*; +pub use self::longobject::*; #[cfg(all(Py_3_9, not(PyPy)))] pub use self::methodobject::*; pub use self::object::*; diff --git a/pyo3-ffi/src/cpython/object.rs b/pyo3-ffi/src/cpython/object.rs index 0f1778f6a3d..b4f6ce5a878 100644 --- a/pyo3-ffi/src/cpython/object.rs +++ b/pyo3-ffi/src/cpython/object.rs @@ -296,6 +296,8 @@ pub struct _specialization_cache { pub getitem: *mut PyObject, #[cfg(Py_3_12)] pub getitem_version: u32, + #[cfg(Py_3_13)] + pub init: *mut PyObject, } #[repr(C)] diff --git a/pyo3-ffi/src/longobject.rs b/pyo3-ffi/src/longobject.rs index 55ea8fa1462..35a2bc1b0ff 100644 --- a/pyo3-ffi/src/longobject.rs +++ b/pyo3-ffi/src/longobject.rs @@ -1,8 +1,6 @@ use crate::object::*; use crate::pyport::Py_ssize_t; use libc::size_t; -#[cfg(not(Py_LIMITED_API))] -use std::os::raw::c_uchar; use std::os::raw::{c_char, c_double, c_int, c_long, c_longlong, c_ulong, c_ulonglong, c_void}; use std::ptr::addr_of_mut; @@ -90,34 +88,12 @@ extern "C" { arg3: c_int, ) -> *mut PyObject; } -// skipped non-limited PyLong_FromUnicodeObject -// skipped non-limited _PyLong_FromBytes #[cfg(not(Py_LIMITED_API))] extern "C" { - // skipped non-limited _PyLong_Sign - #[cfg_attr(PyPy, link_name = "_PyPyLong_NumBits")] + #[cfg(not(Py_3_13))] pub fn _PyLong_NumBits(obj: *mut PyObject) -> size_t; - - // skipped _PyLong_DivmodNear - - #[cfg_attr(PyPy, link_name = "_PyPyLong_FromByteArray")] - pub fn _PyLong_FromByteArray( - bytes: *const c_uchar, - n: size_t, - little_endian: c_int, - is_signed: c_int, - ) -> *mut PyObject; - - #[cfg_attr(PyPy, link_name = "_PyPyLong_AsByteArrayO")] - pub fn _PyLong_AsByteArray( - v: *mut PyLongObject, - bytes: *mut c_uchar, - n: size_t, - little_endian: c_int, - is_signed: c_int, - ) -> c_int; } // skipped non-limited _PyLong_Format @@ -130,6 +106,5 @@ extern "C" { pub fn PyOS_strtol(arg1: *const c_char, arg2: *mut *mut c_char, arg3: c_int) -> c_long; } -// skipped non-limited _PyLong_GCD // skipped non-limited _PyLong_Rshift // skipped non-limited _PyLong_Lshift diff --git a/pytests/noxfile.py b/pytests/noxfile.py index 7c681ab1aa8..af2eb0d3a75 100644 --- a/pytests/noxfile.py +++ b/pytests/noxfile.py @@ -9,11 +9,16 @@ def test(session: nox.Session): session.env["MATURIN_PEP517_ARGS"] = "--profile=dev" session.run_always("python", "-m", "pip", "install", "-v", ".[dev]") - try: - session.install("--only-binary=numpy", "numpy>=1.16") - except CommandFailed: - # No binary wheel for numpy available on this platform - pass + + def try_install_binary(package: str, constraint: str): + try: + session.install(f"--only-binary={package}", f"{package}{constraint}") + except CommandFailed: + # No binary wheel available on this platform + pass + + try_install_binary("numpy", ">=1.16") + try_install_binary("gevent", ">=22.10.2") ignored_paths = [] if sys.version_info < (3, 10): # Match syntax is only available in Python >= 3.10 diff --git a/pytests/pyproject.toml b/pytests/pyproject.toml index aace57dd4d4..5f78a573124 100644 --- a/pytests/pyproject.toml +++ b/pytests/pyproject.toml @@ -20,7 +20,6 @@ classifiers = [ [project.optional-dependencies] dev = [ - "gevent>=22.10.2; implementation_name == 'cpython'", "hypothesis>=3.55", "pytest-asyncio>=0.21", "pytest-benchmark>=3.4", diff --git a/pytests/tests/test_misc.py b/pytests/tests/test_misc.py index de75f1c8a80..88af735e861 100644 --- a/pytests/tests/test_misc.py +++ b/pytests/tests/test_misc.py @@ -5,6 +5,11 @@ import pyo3_pytests.misc import pytest +if sys.version_info >= (3, 13): + subinterpreters = pytest.importorskip("subinterpreters") +else: + subinterpreters = pytest.importorskip("_xxsubinterpreters") + def test_issue_219(): # Should not deadlock @@ -31,23 +36,19 @@ def test_multiple_imports_same_interpreter_ok(): reason="PyPy and GraalPy do not support subinterpreters", ) def test_import_in_subinterpreter_forbidden(): - import _xxsubinterpreters - if sys.version_info < (3, 12): expected_error = "PyO3 modules do not yet support subinterpreters, see https://github.com/PyO3/pyo3/issues/576" else: expected_error = "module pyo3_pytests.pyo3_pytests does not support loading in subinterpreters" - sub_interpreter = _xxsubinterpreters.create() + sub_interpreter = subinterpreters.create() with pytest.raises( - _xxsubinterpreters.RunFailedError, + subinterpreters.RunFailedError, match=expected_error, ): - _xxsubinterpreters.run_string( - sub_interpreter, "import pyo3_pytests.pyo3_pytests" - ) + subinterpreters.run_string(sub_interpreter, "import pyo3_pytests.pyo3_pytests") - _xxsubinterpreters.destroy(sub_interpreter) + subinterpreters.destroy(sub_interpreter) def test_type_full_name_includes_module(): diff --git a/src/conversions/num_bigint.rs b/src/conversions/num_bigint.rs index 743df8a9923..196ae28e788 100644 --- a/src/conversions/num_bigint.rs +++ b/src/conversions/num_bigint.rs @@ -47,6 +47,8 @@ //! assert n + 1 == value //! ``` +#[cfg(not(Py_LIMITED_API))] +use crate::ffi_ptr_ext::FfiPtrExt; #[cfg(Py_LIMITED_API)] use crate::types::{bytes::PyBytesMethods, PyBytes}; use crate::{ @@ -63,20 +65,47 @@ use num_bigint::Sign; // for identical functionality between BigInt and BigUint macro_rules! bigint_conversion { - ($rust_ty: ty, $is_signed: expr, $to_bytes: path) => { + ($rust_ty: ty, $is_signed: literal, $to_bytes: path) => { #[cfg_attr(docsrs, doc(cfg(feature = "num-bigint")))] impl ToPyObject for $rust_ty { #[cfg(not(Py_LIMITED_API))] fn to_object(&self, py: Python<'_>) -> PyObject { let bytes = $to_bytes(self); - unsafe { - let obj = ffi::_PyLong_FromByteArray( - bytes.as_ptr().cast(), - bytes.len(), - 1, - $is_signed, - ); - PyObject::from_owned_ptr(py, obj) + #[cfg(not(Py_3_13))] + { + unsafe { + ffi::_PyLong_FromByteArray( + bytes.as_ptr().cast(), + bytes.len(), + 1, + $is_signed.into(), + ) + .assume_owned(py) + .unbind() + } + } + #[cfg(Py_3_13)] + { + if $is_signed { + unsafe { + ffi::PyLong_FromNativeBytes( + bytes.as_ptr().cast(), + bytes.len(), + ffi::Py_ASNATIVEBYTES_LITTLE_ENDIAN, + ) + .assume_owned(py) + } + } else { + unsafe { + ffi::PyLong_FromUnsignedNativeBytes( + bytes.as_ptr().cast(), + bytes.len(), + ffi::Py_ASNATIVEBYTES_LITTLE_ENDIAN, + ) + .assume_owned(py) + } + } + .unbind() } } @@ -84,7 +113,7 @@ macro_rules! bigint_conversion { fn to_object(&self, py: Python<'_>) -> PyObject { let bytes = $to_bytes(self); let bytes_obj = PyBytes::new_bound(py, &bytes); - let kwargs = if $is_signed > 0 { + let kwargs = if $is_signed { let kwargs = crate::types::PyDict::new_bound(py); kwargs.set_item(crate::intern!(py, "signed"), true).unwrap(); Some(kwargs) @@ -107,8 +136,8 @@ macro_rules! bigint_conversion { }; } -bigint_conversion!(BigUint, 0, BigUint::to_bytes_le); -bigint_conversion!(BigInt, 1, BigInt::to_signed_bytes_le); +bigint_conversion!(BigUint, false, BigUint::to_bytes_le); +bigint_conversion!(BigInt, true, BigInt::to_signed_bytes_le); #[cfg_attr(docsrs, doc(cfg(feature = "num-bigint")))] impl<'py> FromPyObject<'py> for BigInt { @@ -122,13 +151,9 @@ impl<'py> FromPyObject<'py> for BigInt { num_owned = unsafe { Py::from_owned_ptr_or_err(py, ffi::PyNumber_Index(ob.as_ptr()))? }; num_owned.bind(py) }; - let n_bits = int_n_bits(num)?; - if n_bits == 0 { - return Ok(BigInt::from(0isize)); - } #[cfg(not(Py_LIMITED_API))] { - let mut buffer = int_to_u32_vec(num, (n_bits + 32) / 32, true)?; + let mut buffer = int_to_u32_vec::(num)?; let sign = if buffer.last().copied().map_or(false, |last| last >> 31 != 0) { // BigInt::new takes an unsigned array, so need to convert from two's complement // flip all bits, 'subtract' 1 (by adding one to the unsigned array) @@ -152,6 +177,10 @@ impl<'py> FromPyObject<'py> for BigInt { } #[cfg(Py_LIMITED_API)] { + let n_bits = int_n_bits(num)?; + if n_bits == 0 { + return Ok(BigInt::from(0isize)); + } let bytes = int_to_py_bytes(num, (n_bits + 8) / 8, true)?; Ok(BigInt::from_signed_bytes_le(bytes.as_bytes())) } @@ -170,31 +199,37 @@ impl<'py> FromPyObject<'py> for BigUint { num_owned = unsafe { Py::from_owned_ptr_or_err(py, ffi::PyNumber_Index(ob.as_ptr()))? }; num_owned.bind(py) }; - let n_bits = int_n_bits(num)?; - if n_bits == 0 { - return Ok(BigUint::from(0usize)); - } #[cfg(not(Py_LIMITED_API))] { - let buffer = int_to_u32_vec(num, (n_bits + 31) / 32, false)?; + let buffer = int_to_u32_vec::(num)?; Ok(BigUint::new(buffer)) } #[cfg(Py_LIMITED_API)] { + let n_bits = int_n_bits(num)?; + if n_bits == 0 { + return Ok(BigUint::from(0usize)); + } let bytes = int_to_py_bytes(num, (n_bits + 7) / 8, false)?; Ok(BigUint::from_bytes_le(bytes.as_bytes())) } } } -#[cfg(not(Py_LIMITED_API))] +#[cfg(not(any(Py_LIMITED_API, Py_3_13)))] #[inline] -fn int_to_u32_vec( - long: &Bound<'_, PyLong>, - n_digits: usize, - is_signed: bool, -) -> PyResult> { - let mut buffer = Vec::with_capacity(n_digits); +fn int_to_u32_vec(long: &Bound<'_, PyLong>) -> PyResult> { + let mut buffer = Vec::new(); + let n_bits = int_n_bits(long)?; + if n_bits == 0 { + return Ok(buffer); + } + let n_digits = if SIGNED { + (n_bits + 32) / 32 + } else { + (n_bits + 31) / 32 + }; + buffer.reserve_exact(n_digits); unsafe { crate::err::error_on_minusone( long.py(), @@ -203,7 +238,7 @@ fn int_to_u32_vec( buffer.as_mut_ptr() as *mut u8, n_digits * 4, 1, - is_signed.into(), + SIGNED.into(), ), )?; buffer.set_len(n_digits) @@ -215,6 +250,44 @@ fn int_to_u32_vec( Ok(buffer) } +#[cfg(all(not(Py_LIMITED_API), Py_3_13))] +#[inline] +fn int_to_u32_vec(long: &Bound<'_, PyLong>) -> PyResult> { + let mut buffer = Vec::new(); + let mut flags = ffi::Py_ASNATIVEBYTES_LITTLE_ENDIAN; + if !SIGNED { + flags |= ffi::Py_ASNATIVEBYTES_UNSIGNED_BUFFER | ffi::Py_ASNATIVEBYTES_REJECT_NEGATIVE; + } + let n_bytes = + unsafe { ffi::PyLong_AsNativeBytes(long.as_ptr().cast(), std::ptr::null_mut(), 0, flags) }; + let n_bytes_unsigned: usize = n_bytes + .try_into() + .map_err(|_| crate::PyErr::fetch(long.py()))?; + if n_bytes == 0 { + return Ok(buffer); + } + // TODO: use div_ceil when MSRV >= 1.73 + let n_digits = { + let adjust = if n_bytes % 4 == 0 { 0 } else { 1 }; + (n_bytes_unsigned / 4) + adjust + }; + buffer.reserve_exact(n_digits); + unsafe { + ffi::PyLong_AsNativeBytes( + long.as_ptr().cast(), + buffer.as_mut_ptr().cast(), + (n_digits * 4).try_into().unwrap(), + flags, + ); + buffer.set_len(n_digits); + }; + buffer + .iter_mut() + .for_each(|chunk| *chunk = u32::from_le(*chunk)); + + Ok(buffer) +} + #[cfg(Py_LIMITED_API)] fn int_to_py_bytes<'py>( long: &Bound<'py, PyLong>, @@ -239,6 +312,7 @@ fn int_to_py_bytes<'py>( } #[inline] +#[cfg(any(not(Py_3_13), Py_LIMITED_API))] fn int_n_bits(long: &Bound<'_, PyLong>) -> PyResult { let py = long.py(); #[cfg(not(Py_LIMITED_API))] diff --git a/src/conversions/std/num.rs b/src/conversions/std/num.rs index e2072d210e0..effe7c7c062 100644 --- a/src/conversions/std/num.rs +++ b/src/conversions/std/num.rs @@ -1,3 +1,4 @@ +use crate::ffi_ptr_ext::FfiPtrExt; #[cfg(feature = "experimental-inspect")] use crate::inspect::types::TypeInfo; use crate::types::any::PyAnyMethods; @@ -63,14 +64,8 @@ macro_rules! extract_int { err_if_invalid_value($obj.py(), $error_val, unsafe { $pylong_as(long.as_ptr()) }) } else { unsafe { - let num = ffi::PyNumber_Index($obj.as_ptr()); - if num.is_null() { - Err(PyErr::fetch($obj.py())) - } else { - let result = err_if_invalid_value($obj.py(), $error_val, $pylong_as(num)); - ffi::Py_DECREF(num); - result - } + let num = ffi::PyNumber_Index($obj.as_ptr()).assume_owned_or_err($obj.py())?; + err_if_invalid_value($obj.py(), $error_val, $pylong_as(num.as_ptr())) } } }; @@ -181,7 +176,7 @@ mod fast_128bit_int_conversion { // for 128bit Integers macro_rules! int_convert_128 { - ($rust_type: ty, $is_signed: expr) => { + ($rust_type: ty, $is_signed: literal) => { impl ToPyObject for $rust_type { #[inline] fn to_object(&self, py: Python<'_>) -> PyObject { @@ -190,18 +185,44 @@ mod fast_128bit_int_conversion { } impl IntoPy for $rust_type { fn into_py(self, py: Python<'_>) -> PyObject { - // Always use little endian - let bytes = self.to_le_bytes(); - unsafe { - PyObject::from_owned_ptr( - py, + #[cfg(not(Py_3_13))] + { + let bytes = self.to_le_bytes(); + unsafe { ffi::_PyLong_FromByteArray( - bytes.as_ptr() as *const std::os::raw::c_uchar, + bytes.as_ptr().cast(), bytes.len(), 1, - $is_signed, - ), - ) + $is_signed.into(), + ) + .assume_owned(py) + .unbind() + } + } + #[cfg(Py_3_13)] + { + let bytes = self.to_ne_bytes(); + + if $is_signed { + unsafe { + ffi::PyLong_FromNativeBytes( + bytes.as_ptr().cast(), + bytes.len(), + ffi::Py_ASNATIVEBYTES_NATIVE_ENDIAN, + ) + .assume_owned(py) + } + } else { + unsafe { + ffi::PyLong_FromUnsignedNativeBytes( + bytes.as_ptr().cast(), + bytes.len(), + ffi::Py_ASNATIVEBYTES_NATIVE_ENDIAN, + ) + .assume_owned(py) + } + } + .unbind() } } @@ -213,20 +234,46 @@ mod fast_128bit_int_conversion { impl FromPyObject<'_> for $rust_type { fn extract_bound(ob: &Bound<'_, PyAny>) -> PyResult<$rust_type> { - let num = unsafe { - PyObject::from_owned_ptr_or_err(ob.py(), ffi::PyNumber_Index(ob.as_ptr()))? - }; - let mut buffer = [0; std::mem::size_of::<$rust_type>()]; + let num = + unsafe { ffi::PyNumber_Index(ob.as_ptr()).assume_owned_or_err(ob.py())? }; + let mut buffer = [0u8; std::mem::size_of::<$rust_type>()]; + #[cfg(not(Py_3_13))] crate::err::error_on_minusone(ob.py(), unsafe { ffi::_PyLong_AsByteArray( num.as_ptr() as *mut ffi::PyLongObject, buffer.as_mut_ptr(), buffer.len(), 1, - $is_signed, + $is_signed.into(), ) })?; - Ok(<$rust_type>::from_le_bytes(buffer)) + #[cfg(Py_3_13)] + { + let mut flags = ffi::Py_ASNATIVEBYTES_NATIVE_ENDIAN; + if !$is_signed { + flags |= ffi::Py_ASNATIVEBYTES_UNSIGNED_BUFFER + | ffi::Py_ASNATIVEBYTES_REJECT_NEGATIVE; + } + let actual_size: usize = unsafe { + ffi::PyLong_AsNativeBytes( + num.as_ptr(), + buffer.as_mut_ptr().cast(), + buffer + .len() + .try_into() + .expect("length of buffer fits in Py_ssize_t"), + flags, + ) + } + .try_into() + .map_err(|_| PyErr::fetch(ob.py()))?; + if actual_size as usize > buffer.len() { + return Err(crate::exceptions::PyOverflowError::new_err( + "Python int larger than 128 bits", + )); + } + } + Ok(<$rust_type>::from_ne_bytes(buffer)) } #[cfg(feature = "experimental-inspect")] @@ -237,8 +284,8 @@ mod fast_128bit_int_conversion { }; } - int_convert_128!(i128, 1); - int_convert_128!(u128, 0); + int_convert_128!(i128, true); + int_convert_128!(u128, false); } // For ABI3 we implement the conversion manually. From 934c6636120acf5a7a30e26de5d4358001c1ac5b Mon Sep 17 00:00:00 2001 From: JRRudy1 <31031841+JRRudy1@users.noreply.github.com> Date: Mon, 27 May 2024 20:49:52 -0500 Subject: [PATCH 088/495] Added `From>` impl for `PyClassInitializer`. (#4214) * Added `From>` impl for PyClassInitializer. * Added newsfragment entry. * Added tests for pyclass constructors returning `Py` and `Bound`. * Fixed tests. * Updated tests to properly cover the new impl. --------- Co-authored-by: jrudolph --- newsfragments/4214.added.md | 1 + src/pyclass_init.rs | 7 +++++++ tests/test_class_new.rs | 40 +++++++++++++++++++++++++++++++++++++ 3 files changed, 48 insertions(+) create mode 100644 newsfragments/4214.added.md diff --git a/newsfragments/4214.added.md b/newsfragments/4214.added.md new file mode 100644 index 00000000000..943e1e99e60 --- /dev/null +++ b/newsfragments/4214.added.md @@ -0,0 +1 @@ +Added `From>` impl for `PyClassInitializer`. \ No newline at end of file diff --git a/src/pyclass_init.rs b/src/pyclass_init.rs index 923bc5b7c5a..2e73c1518d8 100644 --- a/src/pyclass_init.rs +++ b/src/pyclass_init.rs @@ -301,6 +301,13 @@ impl From> for PyClassInitializer { } } +impl<'py, T: PyClass> From> for PyClassInitializer { + #[inline] + fn from(value: Bound<'py, T>) -> PyClassInitializer { + PyClassInitializer::from(value.unbind()) + } +} + // Implementation used by proc macros to allow anything convertible to PyClassInitializer to be // the return value of pyclass #[new] method (optionally wrapped in `Result`). impl IntoPyCallbackOutput> for U diff --git a/tests/test_class_new.rs b/tests/test_class_new.rs index 161c60e9489..01081d7afb0 100644 --- a/tests/test_class_new.rs +++ b/tests/test_class_new.rs @@ -258,3 +258,43 @@ fn test_new_existing() { assert!(!obj5.is(&obj6)); }); } + +#[pyclass] +struct NewReturnsPy; + +#[pymethods] +impl NewReturnsPy { + #[new] + fn new(py: Python<'_>) -> PyResult> { + Py::new(py, NewReturnsPy) + } +} + +#[test] +fn test_new_returns_py() { + Python::with_gil(|py| { + let type_ = py.get_type_bound::(); + let obj = type_.call0().unwrap(); + assert!(obj.is_exact_instance_of::()); + }) +} + +#[pyclass] +struct NewReturnsBound; + +#[pymethods] +impl NewReturnsBound { + #[new] + fn new(py: Python<'_>) -> PyResult> { + Bound::new(py, NewReturnsBound) + } +} + +#[test] +fn test_new_returns_bound() { + Python::with_gil(|py| { + let type_ = py.get_type_bound::(); + let obj = type_.call0().unwrap(); + assert!(obj.is_exact_instance_of::()); + }) +} From 4fe5e8c689f58e80e399761dd77408556f769227 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Tue, 28 May 2024 09:19:50 +0100 Subject: [PATCH 089/495] ci: turn off gh-pages benchmarks (#4209) * ci: turn off gh-pages benchmarks * update benchmark badge --- .github/workflows/gh-pages.yml | 85 ---------------------------------- README.md | 2 +- 2 files changed, 1 insertion(+), 86 deletions(-) diff --git a/.github/workflows/gh-pages.yml b/.github/workflows/gh-pages.yml index a101eb6ad25..3237440a99a 100644 --- a/.github/workflows/gh-pages.yml +++ b/.github/workflows/gh-pages.yml @@ -52,88 +52,3 @@ jobs: publish_dir: ./target/guide/ destination_dir: ${{ steps.prepare_tag.outputs.tag_name }} full_commit_message: "Upload documentation for ${{ steps.prepare_tag.outputs.tag_name }}" - - cargo-benchmark: - if: ${{ github.ref_name == 'main' }} - name: Cargo benchmark - needs: guide-build - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - - uses: dtolnay/rust-toolchain@stable - - - uses: actions/cache@v4 - with: - path: | - ~/.cargo/registry - ~/.cargo/git - target - key: cargo-${{ runner.os }}-bench-${{ hashFiles('**/Cargo.toml') }} - continue-on-error: true - - - name: Run benchmarks - run: | - python -m pip install --upgrade pip && pip install nox - for bench in pyo3-benches/benches/*.rs; do - bench_name=$(basename "$bench" .rs) - nox -s bench -- --bench "$bench_name" -- --output-format bencher | tee -a output.txt - done - - # Download previous benchmark result from cache (if exists) - - name: Download previous benchmark data - uses: actions/cache@v4 - with: - path: ./cache - key: ${{ runner.os }}-benchmark - - # Run `github-action-benchmark` action - - name: Store benchmark result - uses: benchmark-action/github-action-benchmark@v1 - with: - name: pyo3-bench - # What benchmark tool the output.txt came from - tool: "cargo" - # Where the output from the benchmark tool is stored - output-file-path: output.txt - # GitHub API token to make a commit comment - github-token: ${{ secrets.GITHUB_TOKEN }} - auto-push: ${{ github.event_name != 'pull_request' }} - - pytest-benchmark: - if: ${{ github.ref_name == 'main' }} - name: Pytest benchmark - needs: cargo-benchmark - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - - uses: dtolnay/rust-toolchain@stable - - - uses: actions/cache@v4 - with: - path: | - ~/.cargo/registry - ~/.cargo/git - target - key: cargo-${{ runner.os }}-pytest-bench-${{ hashFiles('**/Cargo.toml') }} - continue-on-error: true - - - name: Download previous benchmark data - uses: actions/cache@v4 - with: - path: ./cache - key: ${{ runner.os }}-pytest-benchmark - - - name: Run benchmarks - run: | - python -m pip install --upgrade pip && pip install nox - nox -f pytests/noxfile.py -s bench -- --benchmark-json $(pwd)/output.json - - name: Store benchmark result - uses: benchmark-action/github-action-benchmark@v1 - with: - name: pytest-bench - tool: "pytest" - output-file-path: output.json - github-token: ${{ secrets.GITHUB_TOKEN }} - auto-push: ${{ github.event_name != 'pull_request' }} diff --git a/README.md b/README.md index 5e34f35489d..ed77a62b28a 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # PyO3 [![actions status](https://img.shields.io/github/actions/workflow/status/PyO3/pyo3/ci.yml?branch=main&logo=github&style=)](https://github.com/PyO3/pyo3/actions) -[![benchmark](https://img.shields.io/badge/benchmark-✓-Green?logo=github)](https://pyo3.rs/dev/bench/) +[![benchmark](https://img.shields.io/endpoint?url=https://codspeed.io/badge.json)](https://codspeed.io/PyO3/pyo3) [![codecov](https://img.shields.io/codecov/c/gh/PyO3/pyo3?logo=codecov)](https://codecov.io/gh/PyO3/pyo3) [![crates.io](https://img.shields.io/crates/v/pyo3?logo=rust)](https://crates.io/crates/pyo3) [![minimum rustc 1.63](https://img.shields.io/badge/rustc-1.63+-blue?logo=rust)](https://rust-lang.github.io/rfcs/2495-min-rust-version.html) From cb347370ff1a0c6d77ab1865e603a32baa89cff0 Mon Sep 17 00:00:00 2001 From: David Brochart Date: Fri, 31 May 2024 11:44:09 +0200 Subject: [PATCH 090/495] Fix typo (#4222) --- guide/src/types.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/guide/src/types.md b/guide/src/types.md index 2a13c241de1..ab95998bcfb 100644 --- a/guide/src/types.md +++ b/guide/src/types.md @@ -2,7 +2,7 @@ PyO3 offers two main sets of types to interact with Python objects. This section of the guide expands into detail about these types and how to choose which to use. -The first set of types is are the [smart pointers][smart-pointers] which all Python objects are wrapped in. These are `Py`, `Bound<'py, T>`, and `Borrowed<'a, 'py, T>`. The [first section below](#pyo3s-smart-pointers) expands on each of these in detail and why there are three of them. +The first set of types are the [smart pointers][smart-pointers] which all Python objects are wrapped in. These are `Py`, `Bound<'py, T>`, and `Borrowed<'a, 'py, T>`. The [first section below](#pyo3s-smart-pointers) expands on each of these in detail and why there are three of them. The second set of types are types which fill in the generic parameter `T` of the smart pointers. The most common is `PyAny`, which represents any Python object (similar to Python's `typing.Any`). There are also concrete types for many Python built-in types, such as `PyList`, `PyDict`, and `PyTuple`. User defined `#[pyclass]` types also fit this category. The [second section below](#concrete-python-types) expands on how to use these types. From d1a7cf400a7e09b48df8bac4efe6679d987b303c Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Fri, 31 May 2024 16:13:30 +0200 Subject: [PATCH 091/495] add pyclass `eq` option (#4210) * add pyclass `eq` option * prevent manual impl of `__richcmp__` or `__eq__` with `#[pyclass(eq)]` * add simple enum `eq_int` option * rearrange names to fix deprecation warning * add newsfragment and migration * update docs --------- Co-authored-by: David Hewitt --- guide/pyclass-parameters.md | 2 + guide/src/class.md | 23 ++- guide/src/class/object.md | 11 ++ guide/src/migration.md | 34 ++++ newsfragments/4210.added.md | 2 + newsfragments/4210.changed.md | 1 + pyo3-macros-backend/src/attributes.rs | 2 + pyo3-macros-backend/src/pyclass.rs | 242 +++++++++++++++++++++----- pytests/src/enums.rs | 3 +- src/tests/hygiene/pyclass.rs | 3 +- tests/test_declarative_module.rs | 3 +- tests/test_default_impls.rs | 6 +- tests/test_enum.rs | 22 ++- tests/ui/deprecations.rs | 6 + tests/ui/deprecations.stderr | 8 + tests/ui/invalid_pyclass_args.rs | 22 +++ tests/ui/invalid_pyclass_args.stderr | 91 +++++++++- tests/ui/invalid_pyclass_enum.rs | 18 ++ tests/ui/invalid_pyclass_enum.stderr | 74 ++++++++ 19 files changed, 501 insertions(+), 72 deletions(-) create mode 100644 newsfragments/4210.added.md create mode 100644 newsfragments/4210.changed.md diff --git a/guide/pyclass-parameters.md b/guide/pyclass-parameters.md index 9bd0534ea5d..77750e36cdf 100644 --- a/guide/pyclass-parameters.md +++ b/guide/pyclass-parameters.md @@ -5,6 +5,8 @@ | `constructor` | This is currently only allowed on [variants of complex enums][params-constructor]. It allows customization of the generated class constructor for each variant. It uses the same syntax and supports the same options as the `signature` attribute of functions and methods. | | `crate = "some::path"` | Path to import the `pyo3` crate, if it's not accessible at `::pyo3`. | | `dict` | Gives instances of this class an empty `__dict__` to store custom attributes. | +| `eq` | Implements `__eq__` using the `PartialEq` implementation of the underlying Rust datatype. | +| `eq_int` | Implements `__eq__` using `__int__` for simple enums. | | `extends = BaseType` | Use a custom baseclass. Defaults to [`PyAny`][params-1] | | `freelist = N` | Implements a [free list][params-2] of size N. This can improve performance for types that are often created and deleted in quick succession. Profile your code to see whether `freelist` is right for you. | | `frozen` | Declares that your pyclass is immutable. It removes the borrow checker overhead when retrieving a shared reference to the Rust struct, but disables the ability to get a mutable reference. | diff --git a/guide/src/class.md b/guide/src/class.md index 57a5cf6d467..b72cae34e25 100644 --- a/guide/src/class.md +++ b/guide/src/class.md @@ -37,14 +37,16 @@ struct Number(i32); // PyO3 supports unit-only enums (which contain only unit variants) // These simple enums behave similarly to Python's enumerations (enum.Enum) -#[pyclass] +#[pyclass(eq, eq_int)] +#[derive(PartialEq)] enum MyEnum { Variant, OtherVariant = 30, // PyO3 supports custom discriminants. } // PyO3 supports custom discriminants in unit-only enums -#[pyclass] +#[pyclass(eq, eq_int)] +#[derive(PartialEq)] enum HttpResponse { Ok = 200, NotFound = 404, @@ -1053,7 +1055,8 @@ PyO3 adds a class attribute for each variant, so you can access them in Python w ```rust # use pyo3::prelude::*; -#[pyclass] +#[pyclass(eq, eq_int)] +#[derive(PartialEq)] enum MyEnum { Variant, OtherVariant, @@ -1075,7 +1078,8 @@ You can also convert your simple enums into `int`: ```rust # use pyo3::prelude::*; -#[pyclass] +#[pyclass(eq, eq_int)] +#[derive(PartialEq)] enum MyEnum { Variant, OtherVariant = 10, @@ -1087,8 +1091,6 @@ Python::with_gil(|py| { pyo3::py_run!(py, cls x, r#" assert int(cls.Variant) == x assert int(cls.OtherVariant) == 10 - assert cls.OtherVariant == 10 # You can also compare against int. - assert 10 == cls.OtherVariant "#) }) ``` @@ -1097,7 +1099,8 @@ PyO3 also provides `__repr__` for enums: ```rust # use pyo3::prelude::*; -#[pyclass] +#[pyclass(eq, eq_int)] +#[derive(PartialEq)] enum MyEnum{ Variant, OtherVariant, @@ -1117,7 +1120,8 @@ All methods defined by PyO3 can be overridden. For example here's how you overri ```rust # use pyo3::prelude::*; -#[pyclass] +#[pyclass(eq, eq_int)] +#[derive(PartialEq)] enum MyEnum { Answer = 42, } @@ -1139,7 +1143,8 @@ Enums and their variants can also be renamed using `#[pyo3(name)]`. ```rust # use pyo3::prelude::*; -#[pyclass(name = "RenamedEnum")] +#[pyclass(eq, eq_int, name = "RenamedEnum")] +#[derive(PartialEq)] enum MyEnum { #[pyo3(name = "UPPERCASE")] Variant, diff --git a/guide/src/class/object.md b/guide/src/class/object.md index db6cc7d3234..e7a366870b7 100644 --- a/guide/src/class/object.md +++ b/guide/src/class/object.md @@ -226,6 +226,16 @@ impl Number { # } ``` +To implement `__eq__` using the Rust [`PartialEq`] trait implementation, the `eq` option can be used. + +```rust +# use pyo3::prelude::*; +# +#[pyclass(eq)] +#[derive(PartialEq)] +struct Number(i32); +``` + ### Truthyness We'll consider `Number` to be `True` if it is nonzero: @@ -305,3 +315,4 @@ fn my_module(m: &Bound<'_, PyModule>) -> PyResult<()> { [`Hasher`]: https://doc.rust-lang.org/std/hash/trait.Hasher.html [`DefaultHasher`]: https://doc.rust-lang.org/std/collections/hash_map/struct.DefaultHasher.html [SipHash]: https://en.wikipedia.org/wiki/SipHash +[`PartialEq`]: https://doc.rust-lang.org/stable/std/cmp/trait.PartialEq.html diff --git a/guide/src/migration.md b/guide/src/migration.md index f56db2a5fc7..31e912a6856 100644 --- a/guide/src/migration.md +++ b/guide/src/migration.md @@ -47,6 +47,40 @@ However, take care to note that the behaviour is different from previous version Related to this, we also added a `pyo3_disable_reference_pool` conditional compilation flag which removes the infrastructure necessary to apply delayed reference count decrements implied by `impl Drop for Py`. They do not appear to be a soundness hazard as they should lead to memory leaks in the worst case. However, the global synchronization adds significant overhead to cross the Python-Rust boundary. Enabling this feature will remove these costs and make the `Drop` implementation abort the process if called without the GIL being held instead.
+### Require explicit opt-in for comparison for simple enums +
+Click to expand + +With `pyo3` 0.22 the new `#[pyo3(eq)]` options allows automatic implementation of Python equality using Rust's `PartialEq`. Previously simple enums automatically implemented equality in terms of their discriminants. To make PyO3 more consistent, this automatic equality implementation is deprecated in favour of having opt-ins for all `#[pyclass]` types. Similarly, simple enums supported comparison with integers, which is not covered by Rust's `PartialEq` derive, so has been split out into the `#[pyo3(eq_int)]` attribute. + +To migrate, place a `#[pyo3(eq, eq_int)]` attribute on simple enum classes. + +Before: + +```rust +# #![allow(deprecated, dead_code)] +# use pyo3::prelude::*; +#[pyclass] +enum SimpleEnum { + VariantA, + VariantB = 42, +} +``` + +After: + +```rust +# #![allow(dead_code)] +# use pyo3::prelude::*; +#[pyclass(eq, eq_int)] +#[derive(PartialEq)] +enum SimpleEnum { + VariantA, + VariantB = 42, +} +``` +
+ ## from 0.20.* to 0.21
Click to expand diff --git a/newsfragments/4210.added.md b/newsfragments/4210.added.md new file mode 100644 index 00000000000..dae8cd8dabb --- /dev/null +++ b/newsfragments/4210.added.md @@ -0,0 +1,2 @@ +Added `#[pyclass(eq)]` option to generate `__eq__` based on `PartialEq`. +Added `#[pyclass(eq_int)]` for simple enums to implement equality based on their discriminants. \ No newline at end of file diff --git a/newsfragments/4210.changed.md b/newsfragments/4210.changed.md new file mode 100644 index 00000000000..a69e3c3a37e --- /dev/null +++ b/newsfragments/4210.changed.md @@ -0,0 +1 @@ +Deprecate implicit integer comparision for simple enums in favor of `#[pyclass(eq_int)]`. \ No newline at end of file diff --git a/pyo3-macros-backend/src/attributes.rs b/pyo3-macros-backend/src/attributes.rs index d9c805aa3fa..3bccf0ae3ee 100644 --- a/pyo3-macros-backend/src/attributes.rs +++ b/pyo3-macros-backend/src/attributes.rs @@ -14,6 +14,8 @@ pub mod kw { syn::custom_keyword!(cancel_handle); syn::custom_keyword!(constructor); syn::custom_keyword!(dict); + syn::custom_keyword!(eq); + syn::custom_keyword!(eq_int); syn::custom_keyword!(extends); syn::custom_keyword!(freelist); syn::custom_keyword!(from_py_with); diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index 47c52c84518..5838f7c5f5c 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -18,7 +18,7 @@ use crate::utils::Ctx; use crate::utils::{self, apply_renaming_rule, PythonDoc}; use crate::PyFunctionOptions; use proc_macro2::{Ident, Span, TokenStream}; -use quote::{format_ident, quote}; +use quote::{format_ident, quote, quote_spanned}; use syn::ext::IdentExt; use syn::parse::{Parse, ParseStream}; use syn::punctuated::Punctuated; @@ -59,6 +59,8 @@ impl PyClassArgs { pub struct PyClassPyO3Options { pub krate: Option, pub dict: Option, + pub eq: Option, + pub eq_int: Option, pub extends: Option, pub get_all: Option, pub freelist: Option, @@ -77,6 +79,8 @@ pub struct PyClassPyO3Options { enum PyClassPyO3Option { Crate(CrateAttribute), Dict(kw::dict), + Eq(kw::eq), + EqInt(kw::eq_int), Extends(ExtendsAttribute), Freelist(FreelistAttribute), Frozen(kw::frozen), @@ -99,6 +103,10 @@ impl Parse for PyClassPyO3Option { input.parse().map(PyClassPyO3Option::Crate) } else if lookahead.peek(kw::dict) { input.parse().map(PyClassPyO3Option::Dict) + } else if lookahead.peek(kw::eq) { + input.parse().map(PyClassPyO3Option::Eq) + } else if lookahead.peek(kw::eq_int) { + input.parse().map(PyClassPyO3Option::EqInt) } else if lookahead.peek(kw::extends) { input.parse().map(PyClassPyO3Option::Extends) } else if lookahead.peek(attributes::kw::freelist) { @@ -166,6 +174,8 @@ impl PyClassPyO3Options { match option { PyClassPyO3Option::Crate(krate) => set_option!(krate), PyClassPyO3Option::Dict(dict) => set_option!(dict), + PyClassPyO3Option::Eq(eq) => set_option!(eq), + PyClassPyO3Option::EqInt(eq_int) => set_option!(eq_int), PyClassPyO3Option::Extends(extends) => set_option!(extends), PyClassPyO3Option::Freelist(freelist) => set_option!(freelist), PyClassPyO3Option::Frozen(frozen) => set_option!(frozen), @@ -350,6 +360,12 @@ fn impl_class( let Ctx { pyo3_path } = ctx; let pytypeinfo_impl = impl_pytypeinfo(cls, args, None, ctx); + let (default_richcmp, default_richcmp_slot) = + pyclass_richcmp(&args.options, &syn::parse_quote!(#cls), ctx)?; + + let mut slots = Vec::new(); + slots.extend(default_richcmp_slot); + let py_class_impl = PyClassImplsBuilder::new( cls, args, @@ -361,7 +377,7 @@ fn impl_class( field_options, ctx, )?, - vec![], + slots, ) .doc(doc) .impl_all(ctx)?; @@ -372,6 +388,12 @@ fn impl_class( #pytypeinfo_impl #py_class_impl + + #[doc(hidden)] + #[allow(non_snake_case)] + impl #cls { + #default_richcmp + } }) } @@ -723,7 +745,6 @@ fn impl_simple_enum( methods_type: PyClassMethodsType, ctx: &Ctx, ) -> Result { - let Ctx { pyo3_path } = ctx; let cls = simple_enum.ident; let ty: syn::Type = syn::parse_quote!(#cls); let variants = simple_enum.variants; @@ -775,50 +796,11 @@ fn impl_simple_enum( (int_impl, int_slot) }; - let (default_richcmp, default_richcmp_slot) = { - let mut richcmp_impl: syn::ImplItemFn = syn::parse_quote! { - fn __pyo3__richcmp__( - &self, - py: #pyo3_path::Python, - other: &#pyo3_path::Bound<'_, #pyo3_path::PyAny>, - op: #pyo3_path::basic::CompareOp - ) -> #pyo3_path::PyResult<#pyo3_path::PyObject> { - use #pyo3_path::conversion::ToPyObject; - use #pyo3_path::types::PyAnyMethods; - use ::core::result::Result::*; - match op { - #pyo3_path::basic::CompareOp::Eq => { - let self_val = self.__pyo3__int__(); - if let Ok(i) = other.extract::<#repr_type>() { - return Ok((self_val == i).to_object(py)); - } - if let Ok(other) = other.extract::<#pyo3_path::PyRef>() { - return Ok((self_val == other.__pyo3__int__()).to_object(py)); - } - - return Ok(py.NotImplemented()); - } - #pyo3_path::basic::CompareOp::Ne => { - let self_val = self.__pyo3__int__(); - if let Ok(i) = other.extract::<#repr_type>() { - return Ok((self_val != i).to_object(py)); - } - if let Ok(other) = other.extract::<#pyo3_path::PyRef>() { - return Ok((self_val != other.__pyo3__int__()).to_object(py)); - } + let (default_richcmp, default_richcmp_slot) = + pyclass_richcmp_simple_enum(&args.options, &ty, repr_type, ctx); - return Ok(py.NotImplemented()); - } - _ => Ok(py.NotImplemented()), - } - } - }; - let richcmp_slot = - generate_default_protocol_slot(&ty, &mut richcmp_impl, &__RICHCMP__, ctx).unwrap(); - (richcmp_impl, richcmp_slot) - }; - - let default_slots = vec![default_repr_slot, default_int_slot, default_richcmp_slot]; + let mut default_slots = vec![default_repr_slot, default_int_slot]; + default_slots.extend(default_richcmp_slot); let pyclass_impls = PyClassImplsBuilder::new( cls, @@ -857,6 +839,8 @@ fn impl_complex_enum( ctx: &Ctx, ) -> Result { let Ctx { pyo3_path } = ctx; + let cls = complex_enum.ident; + let ty: syn::Type = syn::parse_quote!(#cls); // Need to rig the enum PyClass options let args = { @@ -873,7 +857,10 @@ fn impl_complex_enum( let variants = complex_enum.variants; let pytypeinfo = impl_pytypeinfo(cls, &args, None, ctx); - let default_slots = vec![]; + let (default_richcmp, default_richcmp_slot) = pyclass_richcmp(&args.options, &ty, ctx)?; + + let mut default_slots = vec![]; + default_slots.extend(default_richcmp_slot); let impl_builder = PyClassImplsBuilder::new( cls, @@ -978,7 +965,9 @@ fn impl_complex_enum( #[doc(hidden)] #[allow(non_snake_case)] - impl #cls {} + impl #cls { + #default_richcmp + } #(#variant_cls_zsts)* @@ -1276,6 +1265,23 @@ fn gen_complex_enum_variant_class_ident(enum_: &syn::Ident, variant: &syn::Ident format_ident!("{}_{}", enum_, variant) } +fn generate_protocol_slot( + cls: &syn::Type, + method: &mut syn::ImplItemFn, + slot: &SlotDef, + name: &str, + ctx: &Ctx, +) -> syn::Result { + let spec = FnSpec::parse( + &mut method.sig, + &mut Vec::new(), + PyFunctionOptions::default(), + ctx, + ) + .unwrap(); + slot.generate_type_slot(&syn::parse_quote!(#cls), &spec, name, ctx) +} + fn generate_default_protocol_slot( cls: &syn::Type, method: &mut syn::ImplItemFn, @@ -1637,6 +1643,146 @@ fn impl_pytypeinfo( } } +fn pyclass_richcmp_arms(options: &PyClassPyO3Options, ctx: &Ctx) -> TokenStream { + let Ctx { pyo3_path } = ctx; + + let eq_arms = options + .eq + .map(|eq| { + quote_spanned! { eq.span() => + #pyo3_path::pyclass::CompareOp::Eq => { + ::std::result::Result::Ok(#pyo3_path::conversion::IntoPy::into_py(self_val == other, py)) + }, + #pyo3_path::pyclass::CompareOp::Ne => { + ::std::result::Result::Ok(#pyo3_path::conversion::IntoPy::into_py(self_val != other, py)) + }, + } + }) + .unwrap_or_default(); + + // TODO: `ord` can be integrated here (#4202) + #[allow(clippy::let_and_return)] + eq_arms +} + +fn pyclass_richcmp_simple_enum( + options: &PyClassPyO3Options, + cls: &syn::Type, + repr_type: &syn::Ident, + ctx: &Ctx, +) -> (Option, Option) { + let Ctx { pyo3_path } = ctx; + + let arms = pyclass_richcmp_arms(options, ctx); + + let deprecation = options + .eq_int + .map(|_| TokenStream::new()) + .unwrap_or_else(|| { + quote! { + #[deprecated( + since = "0.22.0", + note = "Implicit equality for simple enums is deprecated. Use `#[pyclass(eq, eq_int)` to keep the current behavior." + )] + const DEPRECATION: () = (); + const _: () = DEPRECATION; + } + }); + + let mut options = options.clone(); + options.eq_int = Some(parse_quote!(eq_int)); + + if options.eq.is_none() && options.eq_int.is_none() { + return (None, None); + } + + let eq = options.eq.map(|eq| { + quote_spanned! { eq.span() => + let self_val = self; + if let ::std::result::Result::Ok(other) = #pyo3_path::types::PyAnyMethods::downcast::(other) { + let other = &*other.borrow(); + return match op { + #arms + _ => ::std::result::Result::Ok(py.NotImplemented()) + } + } + } + }); + + let eq_int = options.eq_int.map(|eq_int| { + quote_spanned! { eq_int.span() => + let self_val = self.__pyo3__int__(); + if let ::std::result::Result::Ok(other) = #pyo3_path::types::PyAnyMethods::extract::<#repr_type>(other).or_else(|_| { + #pyo3_path::types::PyAnyMethods::downcast::(other).map(|o| o.borrow().__pyo3__int__()) + }) { + return match op { + #arms + _ => ::std::result::Result::Ok(py.NotImplemented()) + } + } + } + }); + + let mut richcmp_impl = parse_quote! { + fn __pyo3__generated____richcmp__( + &self, + py: #pyo3_path::Python, + other: &#pyo3_path::Bound<'_, #pyo3_path::PyAny>, + op: #pyo3_path::pyclass::CompareOp + ) -> #pyo3_path::PyResult<#pyo3_path::PyObject> { + #deprecation + + #eq + + #eq_int + + ::std::result::Result::Ok(py.NotImplemented()) + } + }; + let richcmp_slot = if options.eq.is_some() { + generate_protocol_slot(cls, &mut richcmp_impl, &__RICHCMP__, "__richcmp__", ctx).unwrap() + } else { + generate_default_protocol_slot(cls, &mut richcmp_impl, &__RICHCMP__, ctx).unwrap() + }; + (Some(richcmp_impl), Some(richcmp_slot)) +} + +fn pyclass_richcmp( + options: &PyClassPyO3Options, + cls: &syn::Type, + ctx: &Ctx, +) -> Result<(Option, Option)> { + let Ctx { pyo3_path } = ctx; + if let Some(eq_int) = options.eq_int { + bail_spanned!(eq_int.span() => "`eq_int` can only be used on simple enums.") + } + + let arms = pyclass_richcmp_arms(options, ctx); + if options.eq.is_some() { + let mut richcmp_impl = parse_quote! { + fn __pyo3__generated____richcmp__( + &self, + py: #pyo3_path::Python, + other: &#pyo3_path::Bound<'_, #pyo3_path::PyAny>, + op: #pyo3_path::pyclass::CompareOp + ) -> #pyo3_path::PyResult<#pyo3_path::PyObject> { + let self_val = self; + let other = &*#pyo3_path::types::PyAnyMethods::downcast::(other)?.borrow(); + match op { + #arms + _ => ::std::result::Result::Ok(py.NotImplemented()) + } + } + }; + let richcmp_slot = + generate_protocol_slot(cls, &mut richcmp_impl, &__RICHCMP__, "__richcmp__", ctx) + .unwrap(); + Ok((Some(richcmp_impl), Some(richcmp_slot))) + } else { + Ok((None, None)) + } +} + /// Implements most traits used by `#[pyclass]`. /// /// Specifically, it implements traits that only depend on class name, diff --git a/pytests/src/enums.rs b/pytests/src/enums.rs index 964f0d431c3..80d7550e1ec 100644 --- a/pytests/src/enums.rs +++ b/pytests/src/enums.rs @@ -18,7 +18,8 @@ pub fn enums(m: &Bound<'_, PyModule>) -> PyResult<()> { Ok(()) } -#[pyclass] +#[pyclass(eq, eq_int)] +#[derive(PartialEq)] pub enum SimpleEnum { Sunday, Monday, diff --git a/src/tests/hygiene/pyclass.rs b/src/tests/hygiene/pyclass.rs index 34b30a8c6f4..27a6b388769 100644 --- a/src/tests/hygiene/pyclass.rs +++ b/src/tests/hygiene/pyclass.rs @@ -29,8 +29,9 @@ pub struct Bar { c: ::std::option::Option>, } -#[crate::pyclass] +#[crate::pyclass(eq, eq_int)] #[pyo3(crate = "crate")] +#[derive(PartialEq)] pub enum Enum { Var0, } diff --git a/tests/test_declarative_module.rs b/tests/test_declarative_module.rs index 2e46f4a64d1..820cf63806d 100644 --- a/tests/test_declarative_module.rs +++ b/tests/test_declarative_module.rs @@ -89,7 +89,8 @@ mod declarative_module { } } - #[pyclass] + #[pyclass(eq, eq_int)] + #[derive(PartialEq)] enum Enum { A, B, diff --git a/tests/test_default_impls.rs b/tests/test_default_impls.rs index 526f88e8f82..670772369f8 100644 --- a/tests/test_default_impls.rs +++ b/tests/test_default_impls.rs @@ -6,7 +6,8 @@ use pyo3::prelude::*; mod common; // Test default generated __repr__. -#[pyclass] +#[pyclass(eq, eq_int)] +#[derive(PartialEq)] enum TestDefaultRepr { Var, } @@ -23,7 +24,8 @@ fn test_default_slot_exists() { }) } -#[pyclass] +#[pyclass(eq, eq_int)] +#[derive(PartialEq)] enum OverrideSlot { Var, } diff --git a/tests/test_enum.rs b/tests/test_enum.rs index 63492b8d3cd..148520dd771 100644 --- a/tests/test_enum.rs +++ b/tests/test_enum.rs @@ -6,7 +6,7 @@ use pyo3::py_run; #[path = "../src/tests/common.rs"] mod common; -#[pyclass] +#[pyclass(eq, eq_int)] #[derive(Debug, PartialEq, Eq, Clone)] pub enum MyEnum { Variant, @@ -73,7 +73,8 @@ fn test_enum_eq_incomparable() { }) } -#[pyclass] +#[pyclass(eq, eq_int)] +#[derive(Debug, PartialEq, Eq, Clone)] enum CustomDiscriminant { One = 1, Two = 2, @@ -121,7 +122,8 @@ fn test_enum_compare_int() { }) } -#[pyclass] +#[pyclass(eq, eq_int)] +#[derive(Debug, PartialEq, Eq, Clone)] #[repr(u8)] enum SmallEnum { V = 1, @@ -135,7 +137,8 @@ fn test_enum_compare_int_no_throw_when_overflow() { }) } -#[pyclass] +#[pyclass(eq, eq_int)] +#[derive(Debug, PartialEq, Eq, Clone)] #[repr(usize)] #[allow(clippy::enum_clike_unportable_variant)] enum BigEnum { @@ -147,12 +150,14 @@ fn test_big_enum_no_overflow() { Python::with_gil(|py| { let usize_max = usize::MAX; let v = Py::new(py, BigEnum::V).unwrap(); + py_assert!(py, usize_max v, "v == usize_max"); py_assert!(py, usize_max v, "int(v) == usize_max"); }) } -#[pyclass] +#[pyclass(eq, eq_int)] +#[derive(Debug, PartialEq, Eq, Clone)] #[repr(u16, align(8))] enum TestReprParse { V, @@ -163,7 +168,7 @@ fn test_repr_parse() { assert_eq!(std::mem::align_of::(), 8); } -#[pyclass(name = "MyEnum")] +#[pyclass(eq, eq_int, name = "MyEnum")] #[derive(Debug, PartialEq, Eq, Clone)] pub enum RenameEnum { Variant, @@ -177,7 +182,7 @@ fn test_rename_enum_repr_correct() { }) } -#[pyclass] +#[pyclass(eq, eq_int)] #[derive(Debug, PartialEq, Eq, Clone)] pub enum RenameVariantEnum { #[pyo3(name = "VARIANT")] @@ -192,7 +197,8 @@ fn test_rename_variant_repr_correct() { }) } -#[pyclass(rename_all = "SCREAMING_SNAKE_CASE")] +#[pyclass(eq, eq_int, rename_all = "SCREAMING_SNAKE_CASE")] +#[derive(Debug, PartialEq, Eq, Clone)] #[allow(clippy::enum_variant_names)] enum RenameAllVariantsEnum { VariantOne, diff --git a/tests/ui/deprecations.rs b/tests/ui/deprecations.rs index fc9e8687cae..dbd0f8aa462 100644 --- a/tests/ui/deprecations.rs +++ b/tests/ui/deprecations.rs @@ -196,3 +196,9 @@ fn test_wrap_pyfunction(py: Python<'_>, m: &Bound<'_, PyModule>) { let _ = wrap_pyfunction_bound!(double, py); let _ = wrap_pyfunction_bound!(double)(py); } + +#[pyclass] +pub enum SimpleEnumWithoutEq { + VariamtA, + VariantB, +} diff --git a/tests/ui/deprecations.stderr b/tests/ui/deprecations.stderr index d014a06bbcc..e08139863d1 100644 --- a/tests/ui/deprecations.stderr +++ b/tests/ui/deprecations.stderr @@ -34,6 +34,14 @@ error: use of deprecated constant `__pyfunction_pyfunction_option_4::SIGNATURE`: 138 | fn pyfunction_option_4( | ^^^^^^^^^^^^^^^^^^^ +error: use of deprecated constant `SimpleEnumWithoutEq::__pyo3__generated____richcmp__::DEPRECATION`: Implicit equality for simple enums is deprecated. Use `#[pyclass(eq, eq_int)` to keep the current behavior. + --> tests/ui/deprecations.rs:200:1 + | +200 | #[pyclass] + | ^^^^^^^^^^ + | + = note: this error originates in the attribute macro `pyclass` (in Nightly builds, run with -Z macro-backtrace for more info) + error: use of deprecated struct `pyo3::PyCell`: `PyCell` was merged into `Bound`, use that instead; see the migration guide for more info --> tests/ui/deprecations.rs:23:30 | diff --git a/tests/ui/invalid_pyclass_args.rs b/tests/ui/invalid_pyclass_args.rs index fac21db078c..6e359f6130e 100644 --- a/tests/ui/invalid_pyclass_args.rs +++ b/tests/ui/invalid_pyclass_args.rs @@ -30,4 +30,26 @@ struct InvalidArg {} #[pyclass(mapping, sequence)] struct CannotBeMappingAndSequence {} +#[pyclass(eq)] +struct EqOptRequiresEq {} + +#[pyclass(eq)] +#[derive(PartialEq)] +struct EqOptAndManualRichCmp {} + +#[pymethods] +impl EqOptAndManualRichCmp { + fn __richcmp__( + &self, + _py: Python, + _other: Bound<'_, PyAny>, + _op: pyo3::pyclass::CompareOp, + ) -> PyResult { + todo!() + } +} + +#[pyclass(eq_int)] +struct NoEqInt {} + fn main() {} diff --git a/tests/ui/invalid_pyclass_args.stderr b/tests/ui/invalid_pyclass_args.stderr index 5b2bd24dd3a..72da82385e7 100644 --- a/tests/ui/invalid_pyclass_args.stderr +++ b/tests/ui/invalid_pyclass_args.stderr @@ -1,4 +1,4 @@ -error: expected one of: `crate`, `dict`, `extends`, `freelist`, `frozen`, `get_all`, `mapping`, `module`, `name`, `rename_all`, `sequence`, `set_all`, `subclass`, `unsendable`, `weakref` +error: expected one of: `crate`, `dict`, `eq`, `eq_int`, `extends`, `freelist`, `frozen`, `get_all`, `mapping`, `module`, `name`, `rename_all`, `sequence`, `set_all`, `subclass`, `unsendable`, `weakref` --> tests/ui/invalid_pyclass_args.rs:3:11 | 3 | #[pyclass(extend=pyo3::types::PyDict)] @@ -46,7 +46,7 @@ error: expected string literal 24 | #[pyclass(module = my_module)] | ^^^^^^^^^ -error: expected one of: `crate`, `dict`, `extends`, `freelist`, `frozen`, `get_all`, `mapping`, `module`, `name`, `rename_all`, `sequence`, `set_all`, `subclass`, `unsendable`, `weakref` +error: expected one of: `crate`, `dict`, `eq`, `eq_int`, `extends`, `freelist`, `frozen`, `get_all`, `mapping`, `module`, `name`, `rename_all`, `sequence`, `set_all`, `subclass`, `unsendable`, `weakref` --> tests/ui/invalid_pyclass_args.rs:27:11 | 27 | #[pyclass(weakrev)] @@ -57,3 +57,90 @@ error: a `#[pyclass]` cannot be both a `mapping` and a `sequence` | 31 | struct CannotBeMappingAndSequence {} | ^^^^^^^^^^^^^^^^^^^^^^^^^^ + +error: `eq_int` can only be used on simple enums. + --> tests/ui/invalid_pyclass_args.rs:52:11 + | +52 | #[pyclass(eq_int)] + | ^^^^^^ + +error[E0592]: duplicate definitions with name `__pymethod___richcmp____` + --> tests/ui/invalid_pyclass_args.rs:36:1 + | +36 | #[pyclass(eq)] + | ^^^^^^^^^^^^^^ duplicate definitions for `__pymethod___richcmp____` +... +40 | #[pymethods] + | ------------ other definition for `__pymethod___richcmp____` + | + = note: this error originates in the attribute macro `pyclass` (in Nightly builds, run with -Z macro-backtrace for more info) + +error[E0369]: binary operation `==` cannot be applied to type `&EqOptRequiresEq` + --> tests/ui/invalid_pyclass_args.rs:33:11 + | +33 | #[pyclass(eq)] + | ^^ + | +note: an implementation of `PartialEq` might be missing for `EqOptRequiresEq` + --> tests/ui/invalid_pyclass_args.rs:34:1 + | +34 | struct EqOptRequiresEq {} + | ^^^^^^^^^^^^^^^^^^^^^^ must implement `PartialEq` +help: consider annotating `EqOptRequiresEq` with `#[derive(PartialEq)]` + | +34 + #[derive(PartialEq)] +35 | struct EqOptRequiresEq {} + | + +error[E0369]: binary operation `!=` cannot be applied to type `&EqOptRequiresEq` + --> tests/ui/invalid_pyclass_args.rs:33:11 + | +33 | #[pyclass(eq)] + | ^^ + | +note: an implementation of `PartialEq` might be missing for `EqOptRequiresEq` + --> tests/ui/invalid_pyclass_args.rs:34:1 + | +34 | struct EqOptRequiresEq {} + | ^^^^^^^^^^^^^^^^^^^^^^ must implement `PartialEq` +help: consider annotating `EqOptRequiresEq` with `#[derive(PartialEq)]` + | +34 + #[derive(PartialEq)] +35 | struct EqOptRequiresEq {} + | + +error[E0034]: multiple applicable items in scope + --> tests/ui/invalid_pyclass_args.rs:36:1 + | +36 | #[pyclass(eq)] + | ^^^^^^^^^^^^^^ multiple `__pymethod___richcmp____` found + | +note: candidate #1 is defined in an impl for the type `EqOptAndManualRichCmp` + --> tests/ui/invalid_pyclass_args.rs:36:1 + | +36 | #[pyclass(eq)] + | ^^^^^^^^^^^^^^ +note: candidate #2 is defined in an impl for the type `EqOptAndManualRichCmp` + --> tests/ui/invalid_pyclass_args.rs:40:1 + | +40 | #[pymethods] + | ^^^^^^^^^^^^ + = note: this error originates in the attribute macro `pyclass` which comes from the expansion of the attribute macro `pymethods` (in Nightly builds, run with -Z macro-backtrace for more info) + +error[E0034]: multiple applicable items in scope + --> tests/ui/invalid_pyclass_args.rs:40:1 + | +40 | #[pymethods] + | ^^^^^^^^^^^^ multiple `__pymethod___richcmp____` found + | +note: candidate #1 is defined in an impl for the type `EqOptAndManualRichCmp` + --> tests/ui/invalid_pyclass_args.rs:36:1 + | +36 | #[pyclass(eq)] + | ^^^^^^^^^^^^^^ +note: candidate #2 is defined in an impl for the type `EqOptAndManualRichCmp` + --> tests/ui/invalid_pyclass_args.rs:40:1 + | +40 | #[pymethods] + | ^^^^^^^^^^^^ + = note: this error originates in the attribute macro `pymethods` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/tests/ui/invalid_pyclass_enum.rs b/tests/ui/invalid_pyclass_enum.rs index e98010fea32..3c6f08da653 100644 --- a/tests/ui/invalid_pyclass_enum.rs +++ b/tests/ui/invalid_pyclass_enum.rs @@ -28,4 +28,22 @@ enum SimpleNoSignature { B, } +#[pyclass(eq, eq_int)] +enum SimpleEqOptRequiresPartialEq { + A, + B, +} + +#[pyclass(eq)] +enum ComplexEqOptRequiresPartialEq { + A(i32), + B { msg: String }, +} + +#[pyclass(eq_int)] +enum NoEqInt { + A(i32), + B { msg: String }, +} + fn main() {} diff --git a/tests/ui/invalid_pyclass_enum.stderr b/tests/ui/invalid_pyclass_enum.stderr index 7e3b6ffa425..551d920eae3 100644 --- a/tests/ui/invalid_pyclass_enum.stderr +++ b/tests/ui/invalid_pyclass_enum.stderr @@ -29,3 +29,77 @@ error: `constructor` can't be used on a simple enum variant | 26 | #[pyo3(constructor = (a, b))] | ^^^^^^^^^^^ + +error: `eq_int` can only be used on simple enums. + --> tests/ui/invalid_pyclass_enum.rs:43:11 + | +43 | #[pyclass(eq_int)] + | ^^^^^^ + +error[E0369]: binary operation `==` cannot be applied to type `&SimpleEqOptRequiresPartialEq` + --> tests/ui/invalid_pyclass_enum.rs:31:11 + | +31 | #[pyclass(eq, eq_int)] + | ^^ + | +note: an implementation of `PartialEq` might be missing for `SimpleEqOptRequiresPartialEq` + --> tests/ui/invalid_pyclass_enum.rs:32:1 + | +32 | enum SimpleEqOptRequiresPartialEq { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ must implement `PartialEq` +help: consider annotating `SimpleEqOptRequiresPartialEq` with `#[derive(PartialEq)]` + | +32 + #[derive(PartialEq)] +33 | enum SimpleEqOptRequiresPartialEq { + | + +error[E0369]: binary operation `!=` cannot be applied to type `&SimpleEqOptRequiresPartialEq` + --> tests/ui/invalid_pyclass_enum.rs:31:11 + | +31 | #[pyclass(eq, eq_int)] + | ^^ + | +note: an implementation of `PartialEq` might be missing for `SimpleEqOptRequiresPartialEq` + --> tests/ui/invalid_pyclass_enum.rs:32:1 + | +32 | enum SimpleEqOptRequiresPartialEq { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ must implement `PartialEq` +help: consider annotating `SimpleEqOptRequiresPartialEq` with `#[derive(PartialEq)]` + | +32 + #[derive(PartialEq)] +33 | enum SimpleEqOptRequiresPartialEq { + | + +error[E0369]: binary operation `==` cannot be applied to type `&ComplexEqOptRequiresPartialEq` + --> tests/ui/invalid_pyclass_enum.rs:37:11 + | +37 | #[pyclass(eq)] + | ^^ + | +note: an implementation of `PartialEq` might be missing for `ComplexEqOptRequiresPartialEq` + --> tests/ui/invalid_pyclass_enum.rs:38:1 + | +38 | enum ComplexEqOptRequiresPartialEq { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ must implement `PartialEq` +help: consider annotating `ComplexEqOptRequiresPartialEq` with `#[derive(PartialEq)]` + | +38 + #[derive(PartialEq)] +39 | enum ComplexEqOptRequiresPartialEq { + | + +error[E0369]: binary operation `!=` cannot be applied to type `&ComplexEqOptRequiresPartialEq` + --> tests/ui/invalid_pyclass_enum.rs:37:11 + | +37 | #[pyclass(eq)] + | ^^ + | +note: an implementation of `PartialEq` might be missing for `ComplexEqOptRequiresPartialEq` + --> tests/ui/invalid_pyclass_enum.rs:38:1 + | +38 | enum ComplexEqOptRequiresPartialEq { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ must implement `PartialEq` +help: consider annotating `ComplexEqOptRequiresPartialEq` with `#[derive(PartialEq)]` + | +38 + #[derive(PartialEq)] +39 | enum ComplexEqOptRequiresPartialEq { + | From 25c1db4767fdefbf41b3ccb065bff73ca89fa4e3 Mon Sep 17 00:00:00 2001 From: Jasper van Brakel <36795178+SuperJappie08@users.noreply.github.com> Date: Fri, 31 May 2024 21:13:50 +0200 Subject: [PATCH 092/495] Add weakref Python types (#3835) * Add vscode folder to gitignore * Initial work on PyWeakRef (weakref.ReferenceType) * Add documentation for PyWeakRef::upgrade * Add missing docs for PyWeakRef * Add PyWeakProxy * Add PyWeakCallableProxy * Add PyWeakRefMethods::upgrade_exact and prevent unnecessary panicing * Change PyWeakRefMethods to exclude infeasible errors. As a result of the checks made about the objectpointers in PyO3, all errors in PyWeakref_GetObject are already caught. Therefor there is no need to check for these errors in the non-ffi layer. * Add towncrier changes * Update weakref type doctests to use `py.run_bound` * Fix to adhere to MSRV * Make weakref tests independent from macros feature * Change Weakref tests * Remove forgotten Debug marcos * Processed .gitignore and PyResultExt feedback * Change new methods of weakref types to remove dangling pointers * Change to reflect deprecation of PyErr::value for PyErr::value_bound * Change Tests so different class name in older python versions is accounted for * Change formatting * Make tests ABI3 compatible * Prevent the use of PyClass in test for weakref under abi3 Python 3.7 and 3.8 * Disable weakref types when targeting PyPy * Remove needless borrow from CallableProxy test * Add Borrowed variants of upgrade and upgrade exact to trait * Added tests for weakref borrow_upgrade methods * Change PyWeakRefMethods method names to be more consistent * Change weakref constructors to take PyAny for main target * Add track_caller to all panicing weakref methods * Add PyWeakRefMethods::upgrade*_as_unchecked * Fix PyWeakProxy and PyWeakCallableProxy Documentation * Replace deprecated wrap_pyfunction with bound equivalent * Add (Generic) PyWeakref Type * Reworked Proxy types into one (PyWeakrefProxy) * Rename PyWeakRef to PyWeakrefReference * Change PyWeakrefReference to only use type pointer when it exists * Remove `#[track_caller]` annotations for now * Make the gil-refs function feature dependent * Remove unused AsPyPointer import * Change docs links to PyNone to not include private module * Fix string based examples * Change tests to work for Python 3.13 * Fix cargo clippy for Python 3.13 --------- Co-authored-by: David Hewitt --- newsfragments/3835.added.md | 1 + src/prelude.rs | 1 + src/types/mod.rs | 2 + src/types/weakref/anyref.rs | 1752 ++++++++++++++++++++++++++++++++ src/types/weakref/mod.rs | 7 + src/types/weakref/proxy.rs | 1688 ++++++++++++++++++++++++++++++ src/types/weakref/reference.rs | 1119 ++++++++++++++++++++ 7 files changed, 4570 insertions(+) create mode 100644 newsfragments/3835.added.md create mode 100644 src/types/weakref/anyref.rs create mode 100644 src/types/weakref/mod.rs create mode 100644 src/types/weakref/proxy.rs create mode 100644 src/types/weakref/reference.rs diff --git a/newsfragments/3835.added.md b/newsfragments/3835.added.md new file mode 100644 index 00000000000..2970a4c8db4 --- /dev/null +++ b/newsfragments/3835.added.md @@ -0,0 +1 @@ +Add `PyWeakref`, `PyWeakrefReference` and `PyWeakrefProxy`. diff --git a/src/prelude.rs b/src/prelude.rs index 4052f7c2d0b..6a0657c8a98 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -49,3 +49,4 @@ pub use crate::types::string::PyStringMethods; pub use crate::types::traceback::PyTracebackMethods; pub use crate::types::tuple::PyTupleMethods; pub use crate::types::typeobject::PyTypeMethods; +pub use crate::types::weakref::PyWeakrefMethods; diff --git a/src/types/mod.rs b/src/types/mod.rs index 12dabda7463..d74c7bc234c 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -47,6 +47,7 @@ pub use self::string::{PyString, PyString as PyUnicode, PyStringMethods}; pub use self::traceback::{PyTraceback, PyTracebackMethods}; pub use self::tuple::{PyTuple, PyTupleMethods}; pub use self::typeobject::{PyType, PyTypeMethods}; +pub use self::weakref::{PyWeakref, PyWeakrefMethods, PyWeakrefProxy, PyWeakrefReference}; /// Iteration over Python collections. /// @@ -365,3 +366,4 @@ pub(crate) mod string; pub(crate) mod traceback; pub(crate) mod tuple; pub(crate) mod typeobject; +pub(crate) mod weakref; diff --git a/src/types/weakref/anyref.rs b/src/types/weakref/anyref.rs new file mode 100644 index 00000000000..82e16293e62 --- /dev/null +++ b/src/types/weakref/anyref.rs @@ -0,0 +1,1752 @@ +use crate::err::{DowncastError, PyResult}; +use crate::ffi_ptr_ext::FfiPtrExt; +use crate::type_object::{PyTypeCheck, PyTypeInfo}; +use crate::types::any::{PyAny, PyAnyMethods}; +use crate::{ffi, Borrowed, Bound}; + +#[cfg(feature = "gil-refs")] +use crate::PyNativeType; + +/// Represents any Python `weakref` reference. +/// +/// In Python this is created by calling `weakref.ref` or `weakref.proxy`. +#[repr(transparent)] +pub struct PyWeakref(PyAny); + +pyobject_native_type_named!(PyWeakref); +pyobject_native_type_extract!(PyWeakref); + +// TODO: We known the layout but this cannot be implemented, due to the lack of public typeobject pointers +// #[cfg(not(Py_LIMITED_API))] +// pyobject_native_type_sized!(PyWeakref, ffi::PyWeakReference); + +impl PyTypeCheck for PyWeakref { + const NAME: &'static str = "weakref"; + + fn type_check(object: &Bound<'_, PyAny>) -> bool { + unsafe { ffi::PyWeakref_Check(object.as_ptr()) > 0 } + } +} + +#[cfg(feature = "gil-refs")] +impl PyWeakref { + // TODO: MAYBE ADD CREATION METHODS OR EASY CASTING?; + + /// Upgrade the weakref to a direct object reference. + /// + /// It is named `upgrade` to be inline with [rust's `Weak::upgrade`](std::rc::Weak::upgrade). + /// In Python it would be equivalent to [`PyWeakref_GetObject`] or retrieving the Object from Python. + /// + /// # Example + #[cfg_attr( + not(all(feature = "macros", not(all(Py_LIMITED_API, not(Py_3_9))))), + doc = "```rust,ignore" + )] + #[cfg_attr( + all(feature = "macros", not(all(Py_LIMITED_API, not(Py_3_9)))), + doc = "```rust" + )] + /// use pyo3::prelude::*; + /// use pyo3::types::{PyWeakref, PyWeakrefProxy}; + /// + /// #[pyclass(weakref)] + /// struct Foo { /* fields omitted */ } + /// + /// #[pymethods] + /// impl Foo { + /// fn get_data(&self) -> (&str, u32) { + /// ("Dave", 10) + /// } + /// } + /// + /// fn parse_data(reference: Borrowed<'_, '_, PyWeakref>) -> PyResult { + /// if let Some(data_src) = reference.upgrade_as::()? { + /// let data = data_src.borrow(); + /// let (name, score) = data.get_data(); + /// Ok(format!("Processing '{}': score = {}", name, score)) + /// } else { + /// Ok("The supplied data reference is nolonger relavent.".to_owned()) + /// } + /// } + /// + /// # fn main() -> PyResult<()> { + /// Python::with_gil(|py| { + /// let data = Bound::new(py, Foo{})?; + /// let proxy = PyWeakrefProxy::new_bound(&data)?; // Retrieve this as an PyMethods argument. + /// let reference = proxy.downcast::()?; + /// + /// assert_eq!( + /// parse_data(reference.as_borrowed())?, + /// "Processing 'Dave': score = 10" + /// ); + /// + /// drop(data); + /// + /// assert_eq!( + /// parse_data(reference.as_borrowed())?, + /// "The supplied data reference is nolonger relavent." + /// ); + /// + /// Ok(()) + /// }) + /// # } + /// ``` + /// + /// # Panics + /// This function panics is the current object is invalid. + /// If used propperly this is never the case. (NonNull and actually a weakref type) + /// + /// [`PyWeakref_GetObject`]: https://docs.python.org/3/c-api/weakref.html#c.PyWeakref_GetObject + pub fn upgrade_as(&self) -> PyResult> + where + T: PyTypeCheck, + { + Ok(self + .as_borrowed() + .upgrade_as::()? + .map(Bound::into_gil_ref)) + } + + /// Upgrade the weakref to a direct object reference unchecked. The type of the recovered object is not checked before downcasting, this could lead to unexpected behavior. Use only when absolutely certain the type can be guaranteed. The `weakref` may still return `None`. + /// + /// It is named `upgrade` to be inline with [rust's `Weak::upgrade`](std::rc::Weak::upgrade). + /// In Python it would be equivalent to [`PyWeakref_GetObject`] or retrieving the Object from Python. + /// + /// # Safety + /// Callers must ensure that the type is valid or risk type confusion. + /// The `weakref` is still allowed to be `None`, if the referenced object has been cleaned up. + /// + /// # Example + #[cfg_attr( + not(all(feature = "macros", not(all(Py_LIMITED_API, not(Py_3_9))))), + doc = "```rust,ignore" + )] + #[cfg_attr( + all(feature = "macros", not(all(Py_LIMITED_API, not(Py_3_9)))), + doc = "```rust" + )] + /// use pyo3::prelude::*; + /// use pyo3::types::{PyWeakref, PyWeakrefProxy}; + /// + /// #[pyclass(weakref)] + /// struct Foo { /* fields omitted */ } + /// + /// #[pymethods] + /// impl Foo { + /// fn get_data(&self) -> (&str, u32) { + /// ("Dave", 10) + /// } + /// } + /// + /// fn parse_data(reference: Borrowed<'_, '_, PyWeakref>) -> String { + /// if let Some(data_src) = unsafe { reference.upgrade_as_unchecked::() } { + /// let data = data_src.borrow(); + /// let (name, score) = data.get_data(); + /// format!("Processing '{}': score = {}", name, score) + /// } else { + /// "The supplied data reference is nolonger relavent.".to_owned() + /// } + /// } + /// + /// # fn main() -> PyResult<()> { + /// Python::with_gil(|py| { + /// let data = Bound::new(py, Foo{})?; + /// let proxy = PyWeakrefProxy::new_bound(&data)?; // Retrieve this as an PyMethods argument. + /// let reference = proxy.downcast::()?; + /// + /// assert_eq!( + /// parse_data(reference.as_borrowed()), + /// "Processing 'Dave': score = 10" + /// ); + /// + /// drop(data); + /// + /// assert_eq!( + /// parse_data(reference.as_borrowed()), + /// "The supplied data reference is nolonger relavent." + /// ); + /// + /// Ok(()) + /// }) + /// # } + /// ``` + /// + /// # Panics + /// This function panics is the current object is invalid. + /// If used propperly this is never the case. (NonNull and actually a weakref type) + /// + /// [`PyWeakref_GetObject`]: https://docs.python.org/3/c-api/weakref.html#c.PyWeakref_GetObject + pub unsafe fn upgrade_as_unchecked(&self) -> Option<&T::AsRefTarget> + where + T: PyTypeCheck, + { + self.as_borrowed() + .upgrade_as_unchecked::() + .map(Bound::into_gil_ref) + } + + /// Upgrade the weakref to an exact direct object reference. + /// + /// It is named `upgrade` to be inline with [rust's `Weak::upgrade`](std::rc::Weak::upgrade). + /// In Python it would be equivalent to [`PyWeakref_GetObject`] or retrieving the Object from Python. + /// + /// # Example + #[cfg_attr( + not(all(feature = "macros", not(all(Py_LIMITED_API, not(Py_3_9))))), + doc = "```rust,ignore" + )] + #[cfg_attr( + all(feature = "macros", not(all(Py_LIMITED_API, not(Py_3_9)))), + doc = "```rust" + )] + /// use pyo3::prelude::*; + /// use pyo3::types::{PyWeakref, PyWeakrefProxy}; + /// + /// #[pyclass(weakref)] + /// struct Foo { /* fields omitted */ } + /// + /// #[pymethods] + /// impl Foo { + /// fn get_data(&self) -> (&str, u32) { + /// ("Dave", 10) + /// } + /// } + /// + /// fn parse_data(reference: Borrowed<'_, '_, PyWeakref>) -> PyResult { + /// if let Some(data_src) = reference.upgrade_as_exact::()? { + /// let data = data_src.borrow(); + /// let (name, score) = data.get_data(); + /// Ok(format!("Processing '{}': score = {}", name, score)) + /// } else { + /// Ok("The supplied data reference is nolonger relavent.".to_owned()) + /// } + /// } + /// + /// # fn main() -> PyResult<()> { + /// Python::with_gil(|py| { + /// let data = Bound::new(py, Foo{})?; + /// let proxy = PyWeakrefProxy::new_bound(&data)?; // Retrieve this as an PyMethods argument. + /// let reference = proxy.downcast::()?; + /// + /// assert_eq!( + /// parse_data(reference.as_borrowed())?, + /// "Processing 'Dave': score = 10" + /// ); + /// + /// drop(data); + /// + /// assert_eq!( + /// parse_data(reference.as_borrowed())?, + /// "The supplied data reference is nolonger relavent." + /// ); + /// + /// Ok(()) + /// }) + /// # } + /// ``` + /// + /// # Panics + /// This function panics is the current object is invalid. + /// If used propperly this is never the case. (NonNull and actually a weakref type) + /// + /// [`PyWeakref_GetObject`]: https://docs.python.org/3/c-api/weakref.html#c.PyWeakref_GetObject + pub fn upgrade_as_exact(&self) -> PyResult> + where + T: PyTypeInfo, + { + Ok(self + .as_borrowed() + .upgrade_as_exact::()? + .map(Bound::into_gil_ref)) + } + + /// Upgrade the weakref to a [`PyAny`] reference to the target if possible. + /// + /// It is named `upgrade` to be inline with [rust's `Weak::upgrade`](std::rc::Weak::upgrade). + /// This function returns `Some(&'py PyAny)` if the reference still exists, otherwise `None` will be returned. + /// + /// This function gets the optional target of this [`PyWeakref`] (Any Python `weakref` weakreference). + /// It produces similair results as using [`PyWeakref_GetObject`] in the C api or retrieving the Object from Python. + /// + /// # Example + #[cfg_attr( + not(all(feature = "macros", not(all(Py_LIMITED_API, not(Py_3_9))))), + doc = "```rust,ignore" + )] + #[cfg_attr( + all(feature = "macros", not(all(Py_LIMITED_API, not(Py_3_9)))), + doc = "```rust" + )] + /// use pyo3::prelude::*; + /// use pyo3::types::{PyWeakref, PyWeakrefProxy}; + /// + /// #[pyclass(weakref)] + /// struct Foo { /* fields omitted */ } + /// + /// fn parse_data(reference: Borrowed<'_, '_, PyWeakref>) -> PyResult { + /// if let Some(object) = reference.upgrade() { + /// Ok(format!("The object '{}' refered by this reference still exists.", object.getattr("__class__")?.getattr("__qualname__")?)) + /// } else { + /// Ok("The object, which this reference refered to, no longer exists".to_owned()) + /// } + /// } + /// + /// # fn main() -> PyResult<()> { + /// Python::with_gil(|py| { + /// let data = Bound::new(py, Foo{})?; + /// let proxy = PyWeakrefProxy::new_bound(&data)?; // Retrieve this as an PyMethods argument. + /// let reference = proxy.downcast::()?; + /// + /// assert_eq!( + /// parse_data(reference.as_borrowed())?, + /// "The object 'Foo' refered by this reference still exists." + /// ); + /// + /// drop(data); + /// + /// assert_eq!( + /// parse_data(reference.as_borrowed())?, + /// "The object, which this reference refered to, no longer exists" + /// ); + /// + /// Ok(()) + /// }) + /// # } + /// ``` + /// + /// # Panics + /// This function panics is the current object is invalid. + /// If used propperly this is never the case. (NonNull and actually a weakref type) + /// + /// [`PyWeakref_GetObject`]: https://docs.python.org/3/c-api/weakref.html#c.PyWeakref_GetObject + pub fn upgrade(&self) -> Option<&'_ PyAny> { + self.as_borrowed().upgrade().map(Bound::into_gil_ref) + } + + /// Retrieve to a object pointed to by the weakref. + /// + /// This function returns `&'py PyAny`, which is either the object if it still exists, otherwise it will refer to [`PyNone`](crate::types::PyNone). + /// + /// This function gets the optional target of this [`PyWeakref`] (Any Python `weakref` weakreference). + /// It produces similair results as using [`PyWeakref_GetObject`] in the C api or retrieving the Object from Python. + /// + /// # Example + #[cfg_attr( + not(all(feature = "macros", not(all(Py_LIMITED_API, not(Py_3_9))))), + doc = "```rust,ignore" + )] + #[cfg_attr( + all(feature = "macros", not(all(Py_LIMITED_API, not(Py_3_9)))), + doc = "```rust" + )] + /// use pyo3::prelude::*; + /// use pyo3::types::{PyWeakref, PyWeakrefProxy}; + /// + /// #[pyclass(weakref)] + /// struct Foo { /* fields omitted */ } + /// + /// fn get_class(reference: Borrowed<'_, '_, PyWeakref>) -> PyResult { + /// reference + /// .get_object() + /// .getattr("__class__")? + /// .repr() + /// .map(|repr| repr.to_string()) + /// } + /// + /// # fn main() -> PyResult<()> { + /// Python::with_gil(|py| { + /// let object = Bound::new(py, Foo{})?; + /// let proxy = PyWeakrefProxy::new_bound(&object)?; // Retrieve this as an PyMethods argument. + /// let reference = proxy.downcast::()?; + /// + /// assert_eq!( + /// get_class(reference.as_borrowed())?, + /// "" + /// ); + /// + /// drop(object); + /// + /// assert_eq!(get_class(reference.as_borrowed())?, ""); + /// + /// Ok(()) + /// }) + /// # } + /// ``` + /// + /// # Panics + /// This function panics is the current object is invalid. + /// If used propperly this is never the case. (NonNull and actually a weakref type) + /// + /// [`PyWeakref_GetObject`]: https://docs.python.org/3/c-api/weakref.html#c.PyWeakref_GetObject + pub fn get_object(&self) -> &'_ PyAny { + self.as_borrowed().get_object().into_gil_ref() + } +} + +/// Implementation of functionality for [`PyWeakref`]. +/// +/// These methods are defined for the `Bound<'py, PyWeakref>` smart pointer, so to use method call +/// syntax these methods are separated into a trait, because stable Rust does not yet support +/// `arbitrary_self_types`. +#[doc(alias = "PyWeakref")] +pub trait PyWeakrefMethods<'py> { + /// Upgrade the weakref to a direct Bound object reference. + /// + /// It is named `upgrade` to be inline with [rust's `Weak::upgrade`](std::rc::Weak::upgrade). + /// In Python it would be equivalent to [`PyWeakref_GetObject`]. + /// + /// # Example + #[cfg_attr( + not(all(feature = "macros", not(all(Py_LIMITED_API, not(Py_3_9))))), + doc = "```rust,ignore" + )] + #[cfg_attr( + all(feature = "macros", not(all(Py_LIMITED_API, not(Py_3_9)))), + doc = "```rust" + )] + /// use pyo3::prelude::*; + /// use pyo3::types::PyWeakrefReference; + /// + /// #[pyclass(weakref)] + /// struct Foo { /* fields omitted */ } + /// + /// #[pymethods] + /// impl Foo { + /// fn get_data(&self) -> (&str, u32) { + /// ("Dave", 10) + /// } + /// } + /// + /// fn parse_data(reference: Borrowed<'_, '_, PyWeakrefReference>) -> PyResult { + /// if let Some(data_src) = reference.upgrade_as::()? { + /// let data = data_src.borrow(); + /// let (name, score) = data.get_data(); + /// Ok(format!("Processing '{}': score = {}", name, score)) + /// } else { + /// Ok("The supplied data reference is nolonger relavent.".to_owned()) + /// } + /// } + /// + /// # fn main() -> PyResult<()> { + /// Python::with_gil(|py| { + /// let data = Bound::new(py, Foo{})?; + /// let reference = PyWeakrefReference::new_bound(&data)?; + /// + /// assert_eq!( + /// parse_data(reference.as_borrowed())?, + /// "Processing 'Dave': score = 10" + /// ); + /// + /// drop(data); + /// + /// assert_eq!( + /// parse_data(reference.as_borrowed())?, + /// "The supplied data reference is nolonger relavent." + /// ); + /// + /// Ok(()) + /// }) + /// # } + /// ``` + /// + /// # Panics + /// This function panics is the current object is invalid. + /// If used propperly this is never the case. (NonNull and actually a weakref type) + /// + /// [`PyWeakref_GetObject`]: https://docs.python.org/3/c-api/weakref.html#c.PyWeakref_GetObject + /// [`weakref.ReferenceType`]: https://docs.python.org/3/library/weakref.html#weakref.ReferenceType + /// [`weakref.ref`]: https://docs.python.org/3/library/weakref.html#weakref.ref + fn upgrade_as(&self) -> PyResult>> + where + T: PyTypeCheck, + { + self.upgrade() + .map(Bound::downcast_into::) + .transpose() + .map_err(Into::into) + } + + /// Upgrade the weakref to a Borrowed object reference. + /// + /// It is named `upgrade_borrowed` to be inline with [rust's `Weak::upgrade`](std::rc::Weak::upgrade). + /// In Python it would be equivalent to [`PyWeakref_GetObject`]. + /// + /// # Example + #[cfg_attr( + not(all(feature = "macros", not(all(Py_LIMITED_API, not(Py_3_9))))), + doc = "```rust,ignore" + )] + #[cfg_attr( + all(feature = "macros", not(all(Py_LIMITED_API, not(Py_3_9)))), + doc = "```rust" + )] + /// use pyo3::prelude::*; + /// use pyo3::types::PyWeakrefReference; + /// + /// #[pyclass(weakref)] + /// struct Foo { /* fields omitted */ } + /// + /// #[pymethods] + /// impl Foo { + /// fn get_data(&self) -> (&str, u32) { + /// ("Dave", 10) + /// } + /// } + /// + /// fn parse_data(reference: Borrowed<'_, '_, PyWeakrefReference>) -> PyResult { + /// if let Some(data_src) = reference.upgrade_borrowed_as::()? { + /// let data = data_src.borrow(); + /// let (name, score) = data.get_data(); + /// Ok(format!("Processing '{}': score = {}", name, score)) + /// } else { + /// Ok("The supplied data reference is nolonger relavent.".to_owned()) + /// } + /// } + /// + /// # fn main() -> PyResult<()> { + /// Python::with_gil(|py| { + /// let data = Bound::new(py, Foo{})?; + /// let reference = PyWeakrefReference::new_bound(&data)?; + /// + /// assert_eq!( + /// parse_data(reference.as_borrowed())?, + /// "Processing 'Dave': score = 10" + /// ); + /// + /// drop(data); + /// + /// assert_eq!( + /// parse_data(reference.as_borrowed())?, + /// "The supplied data reference is nolonger relavent." + /// ); + /// + /// Ok(()) + /// }) + /// # } + /// ``` + /// + /// # Panics + /// This function panics is the current object is invalid. + /// If used propperly this is never the case. (NonNull and actually a weakref type) + /// + /// [`PyWeakref_GetObject`]: https://docs.python.org/3/c-api/weakref.html#c.PyWeakref_GetObject + /// [`weakref.ReferenceType`]: https://docs.python.org/3/library/weakref.html#weakref.ReferenceType + /// [`weakref.ref`]: https://docs.python.org/3/library/weakref.html#weakref.ref? + fn upgrade_borrowed_as<'a, T>(&'a self) -> PyResult>> + where + T: PyTypeCheck, + 'py: 'a, + { + // TODO: Replace when Borrowed::downcast exists + match self.upgrade_borrowed() { + None => Ok(None), + Some(object) if T::type_check(&object) => { + Ok(Some(unsafe { object.downcast_unchecked() })) + } + Some(object) => Err(DowncastError::new(&object, T::NAME).into()), + } + } + + /// Upgrade the weakref to a direct Bound object reference unchecked. The type of the recovered object is not checked before downcasting, this could lead to unexpected behavior. Use only when absolutely certain the type can be guaranteed. The `weakref` may still return `None`. + /// + /// It is named `upgrade` to be inline with [rust's `Weak::upgrade`](std::rc::Weak::upgrade). + /// In Python it would be equivalent to [`PyWeakref_GetObject`]. + /// + /// # Safety + /// Callers must ensure that the type is valid or risk type confusion. + /// The `weakref` is still allowed to be `None`, if the referenced object has been cleaned up. + /// + /// # Example + #[cfg_attr( + not(all(feature = "macros", not(all(Py_LIMITED_API, not(Py_3_9))))), + doc = "```rust,ignore" + )] + #[cfg_attr( + all(feature = "macros", not(all(Py_LIMITED_API, not(Py_3_9)))), + doc = "```rust" + )] + /// use pyo3::prelude::*; + /// use pyo3::types::PyWeakrefReference; + /// + /// #[pyclass(weakref)] + /// struct Foo { /* fields omitted */ } + /// + /// #[pymethods] + /// impl Foo { + /// fn get_data(&self) -> (&str, u32) { + /// ("Dave", 10) + /// } + /// } + /// + /// fn parse_data(reference: Borrowed<'_, '_, PyWeakrefReference>) -> String { + /// if let Some(data_src) = unsafe { reference.upgrade_as_unchecked::() } { + /// let data = data_src.borrow(); + /// let (name, score) = data.get_data(); + /// format!("Processing '{}': score = {}", name, score) + /// } else { + /// "The supplied data reference is nolonger relavent.".to_owned() + /// } + /// } + /// + /// # fn main() -> PyResult<()> { + /// Python::with_gil(|py| { + /// let data = Bound::new(py, Foo{})?; + /// let reference = PyWeakrefReference::new_bound(&data)?; + /// + /// assert_eq!( + /// parse_data(reference.as_borrowed()), + /// "Processing 'Dave': score = 10" + /// ); + /// + /// drop(data); + /// + /// assert_eq!( + /// parse_data(reference.as_borrowed()), + /// "The supplied data reference is nolonger relavent." + /// ); + /// + /// Ok(()) + /// }) + /// # } + /// ``` + /// + /// # Panics + /// This function panics is the current object is invalid. + /// If used propperly this is never the case. (NonNull and actually a weakref type) + /// + /// [`PyWeakref_GetObject`]: https://docs.python.org/3/c-api/weakref.html#c.PyWeakref_GetObject + /// [`weakref.ReferenceType`]: https://docs.python.org/3/library/weakref.html#weakref.ReferenceType + /// [`weakref.ref`]: https://docs.python.org/3/library/weakref.html#weakref.ref + unsafe fn upgrade_as_unchecked(&self) -> Option> { + Some(self.upgrade()?.downcast_into_unchecked()) + } + + /// Upgrade the weakref to a Borrowed object reference unchecked. The type of the recovered object is not checked before downcasting, this could lead to unexpected behavior. Use only when absolutely certain the type can be guaranteed. The `weakref` may still return `None`. + /// + /// It is named `upgrade_borrowed` to be inline with [rust's `Weak::upgrade`](std::rc::Weak::upgrade). + /// In Python it would be equivalent to [`PyWeakref_GetObject`]. + /// + /// # Safety + /// Callers must ensure that the type is valid or risk type confusion. + /// The `weakref` is still allowed to be `None`, if the referenced object has been cleaned up. + /// + /// # Example + #[cfg_attr( + not(all(feature = "macros", not(all(Py_LIMITED_API, not(Py_3_9))))), + doc = "```rust,ignore" + )] + #[cfg_attr( + all(feature = "macros", not(all(Py_LIMITED_API, not(Py_3_9)))), + doc = "```rust" + )] + /// use pyo3::prelude::*; + /// use pyo3::types::PyWeakrefReference; + /// + /// #[pyclass(weakref)] + /// struct Foo { /* fields omitted */ } + /// + /// #[pymethods] + /// impl Foo { + /// fn get_data(&self) -> (&str, u32) { + /// ("Dave", 10) + /// } + /// } + /// + /// fn parse_data(reference: Borrowed<'_, '_, PyWeakrefReference>) -> String { + /// if let Some(data_src) = unsafe { reference.upgrade_borrowed_as_unchecked::() } { + /// let data = data_src.borrow(); + /// let (name, score) = data.get_data(); + /// format!("Processing '{}': score = {}", name, score) + /// } else { + /// "The supplied data reference is nolonger relavent.".to_owned() + /// } + /// } + /// + /// # fn main() -> PyResult<()> { + /// Python::with_gil(|py| { + /// let data = Bound::new(py, Foo{})?; + /// let reference = PyWeakrefReference::new_bound(&data)?; + /// + /// assert_eq!( + /// parse_data(reference.as_borrowed()), + /// "Processing 'Dave': score = 10" + /// ); + /// + /// drop(data); + /// + /// assert_eq!( + /// parse_data(reference.as_borrowed()), + /// "The supplied data reference is nolonger relavent." + /// ); + /// + /// Ok(()) + /// }) + /// # } + /// ``` + /// + /// # Panics + /// This function panics is the current object is invalid. + /// If used propperly this is never the case. (NonNull and actually a weakref type) + /// + /// [`PyWeakref_GetObject`]: https://docs.python.org/3/c-api/weakref.html#c.PyWeakref_GetObject + /// [`weakref.ReferenceType`]: https://docs.python.org/3/library/weakref.html#weakref.ReferenceType + /// [`weakref.ref`]: https://docs.python.org/3/library/weakref.html#weakref.ref? + unsafe fn upgrade_borrowed_as_unchecked<'a, T>(&'a self) -> Option> + where + 'py: 'a, + { + Some(self.upgrade_borrowed()?.downcast_unchecked()) + } + + /// Upgrade the weakref to a exact direct Bound object reference. + /// + /// It is named `upgrade` to be inline with [rust's `Weak::upgrade`](std::rc::Weak::upgrade). + /// In Python it would be equivalent to [`PyWeakref_GetObject`]. + /// + /// # Example + #[cfg_attr( + not(all(feature = "macros", not(all(Py_LIMITED_API, not(Py_3_9))))), + doc = "```rust,ignore" + )] + #[cfg_attr( + all(feature = "macros", not(all(Py_LIMITED_API, not(Py_3_9)))), + doc = "```rust" + )] + /// use pyo3::prelude::*; + /// use pyo3::types::PyWeakrefReference; + /// + /// #[pyclass(weakref)] + /// struct Foo { /* fields omitted */ } + /// + /// #[pymethods] + /// impl Foo { + /// fn get_data(&self) -> (&str, u32) { + /// ("Dave", 10) + /// } + /// } + /// + /// fn parse_data(reference: Borrowed<'_, '_, PyWeakrefReference>) -> PyResult { + /// if let Some(data_src) = reference.upgrade_as_exact::()? { + /// let data = data_src.borrow(); + /// let (name, score) = data.get_data(); + /// Ok(format!("Processing '{}': score = {}", name, score)) + /// } else { + /// Ok("The supplied data reference is nolonger relavent.".to_owned()) + /// } + /// } + /// + /// # fn main() -> PyResult<()> { + /// Python::with_gil(|py| { + /// let data = Bound::new(py, Foo{})?; + /// let reference = PyWeakrefReference::new_bound(&data)?; + /// + /// assert_eq!( + /// parse_data(reference.as_borrowed())?, + /// "Processing 'Dave': score = 10" + /// ); + /// + /// drop(data); + /// + /// assert_eq!( + /// parse_data(reference.as_borrowed())?, + /// "The supplied data reference is nolonger relavent." + /// ); + /// + /// Ok(()) + /// }) + /// # } + /// ``` + /// + /// # Panics + /// This function panics is the current object is invalid. + /// If used propperly this is never the case. (NonNull and actually a weakref type) + /// + /// [`PyWeakref_GetObject`]: https://docs.python.org/3/c-api/weakref.html#c.PyWeakref_GetObject + /// [`weakref.ReferenceType`]: https://docs.python.org/3/library/weakref.html#weakref.ReferenceType + /// [`weakref.ref`]: https://docs.python.org/3/library/weakref.html#weakref.ref + fn upgrade_as_exact(&self) -> PyResult>> + where + T: PyTypeInfo, + { + self.upgrade() + .map(Bound::downcast_into_exact) + .transpose() + .map_err(Into::into) + } + + /// Upgrade the weakref to a exact Borrowed object reference. + /// + /// It is named `upgrade_borrowed` to be inline with [rust's `Weak::upgrade`](std::rc::Weak::upgrade). + /// In Python it would be equivalent to [`PyWeakref_GetObject`]. + /// + /// # Example + #[cfg_attr( + not(all(feature = "macros", not(all(Py_LIMITED_API, not(Py_3_9))))), + doc = "```rust,ignore" + )] + #[cfg_attr( + all(feature = "macros", not(all(Py_LIMITED_API, not(Py_3_9)))), + doc = "```rust" + )] + /// use pyo3::prelude::*; + /// use pyo3::types::PyWeakrefReference; + /// + /// #[pyclass(weakref)] + /// struct Foo { /* fields omitted */ } + /// + /// #[pymethods] + /// impl Foo { + /// fn get_data(&self) -> (&str, u32) { + /// ("Dave", 10) + /// } + /// } + /// + /// fn parse_data(reference: Borrowed<'_, '_, PyWeakrefReference>) -> PyResult { + /// if let Some(data_src) = reference.upgrade_borrowed_as_exact::()? { + /// let data = data_src.borrow(); + /// let (name, score) = data.get_data(); + /// Ok(format!("Processing '{}': score = {}", name, score)) + /// } else { + /// Ok("The supplied data reference is nolonger relavent.".to_owned()) + /// } + /// } + /// + /// # fn main() -> PyResult<()> { + /// Python::with_gil(|py| { + /// let data = Bound::new(py, Foo{})?; + /// let reference = PyWeakrefReference::new_bound(&data)?; + /// + /// assert_eq!( + /// parse_data(reference.as_borrowed())?, + /// "Processing 'Dave': score = 10" + /// ); + /// + /// drop(data); + /// + /// assert_eq!( + /// parse_data(reference.as_borrowed())?, + /// "The supplied data reference is nolonger relavent." + /// ); + /// + /// Ok(()) + /// }) + /// # } + /// ``` + /// + /// # Panics + /// This function panics is the current object is invalid. + /// If used propperly this is never the case. (NonNull and actually a weakref type) + /// + /// [`PyWeakref_GetObject`]: https://docs.python.org/3/c-api/weakref.html#c.PyWeakref_GetObject + /// [`weakref.ReferenceType`]: https://docs.python.org/3/library/weakref.html#weakref.ReferenceType + /// [`weakref.ref`]: https://docs.python.org/3/library/weakref.html#weakref.ref? + fn upgrade_borrowed_as_exact<'a, T>(&'a self) -> PyResult>> + where + T: PyTypeInfo, + 'py: 'a, + { + // TODO: Replace when Borrowed::downcast_exact exists + match self.upgrade_borrowed() { + None => Ok(None), + Some(object) if object.is_exact_instance_of::() => { + Ok(Some(unsafe { object.downcast_unchecked() })) + } + Some(object) => Err(DowncastError::new(&object, T::NAME).into()), + } + } + + /// Upgrade the weakref to a Bound [`PyAny`] reference to the target object if possible. + /// + /// It is named `upgrade` to be inline with [rust's `Weak::upgrade`](std::rc::Weak::upgrade). + /// This function returns `Some(Bound<'py, PyAny>)` if the reference still exists, otherwise `None` will be returned. + /// + /// This function gets the optional target of this [`weakref.ReferenceType`] (result of calling [`weakref.ref`]). + /// It produces similair results to using [`PyWeakref_GetObject`] in the C api. + /// + /// # Example + #[cfg_attr( + not(all(feature = "macros", not(all(Py_LIMITED_API, not(Py_3_9))))), + doc = "```rust,ignore" + )] + #[cfg_attr( + all(feature = "macros", not(all(Py_LIMITED_API, not(Py_3_9)))), + doc = "```rust" + )] + /// use pyo3::prelude::*; + /// use pyo3::types::PyWeakrefReference; + /// + /// #[pyclass(weakref)] + /// struct Foo { /* fields omitted */ } + /// + /// fn parse_data(reference: Borrowed<'_, '_, PyWeakrefReference>) -> PyResult { + /// if let Some(object) = reference.upgrade() { + /// Ok(format!("The object '{}' refered by this reference still exists.", object.getattr("__class__")?.getattr("__qualname__")?)) + /// } else { + /// Ok("The object, which this reference refered to, no longer exists".to_owned()) + /// } + /// } + /// + /// # fn main() -> PyResult<()> { + /// Python::with_gil(|py| { + /// let data = Bound::new(py, Foo{})?; + /// let reference = PyWeakrefReference::new_bound(&data)?; + /// + /// assert_eq!( + /// parse_data(reference.as_borrowed())?, + /// "The object 'Foo' refered by this reference still exists." + /// ); + /// + /// drop(data); + /// + /// assert_eq!( + /// parse_data(reference.as_borrowed())?, + /// "The object, which this reference refered to, no longer exists" + /// ); + /// + /// Ok(()) + /// }) + /// # } + /// ``` + /// + /// # Panics + /// This function panics is the current object is invalid. + /// If used propperly this is never the case. (NonNull and actually a weakref type) + /// + /// [`PyWeakref_GetObject`]: https://docs.python.org/3/c-api/weakref.html#c.PyWeakref_GetObject + /// [`weakref.ReferenceType`]: https://docs.python.org/3/library/weakref.html#weakref.ReferenceType + /// [`weakref.ref`]: https://docs.python.org/3/library/weakref.html#weakref.ref + fn upgrade(&self) -> Option> { + let object = self.get_object(); + + if object.is_none() { + None + } else { + Some(object) + } + } + + /// Upgrade the weakref to a Borrowed [`PyAny`] reference to the target object if possible. + /// + /// It is named `upgrade_borrowed` to be inline with [rust's `Weak::upgrade`](std::rc::Weak::upgrade). + /// This function returns `Some(Borrowed<'_, 'py, PyAny>)` if the reference still exists, otherwise `None` will be returned. + /// + /// This function gets the optional target of this [`weakref.ReferenceType`] (result of calling [`weakref.ref`]). + /// It produces similair results to using [`PyWeakref_GetObject`] in the C api. + /// + /// # Example + #[cfg_attr( + not(all(feature = "macros", not(all(Py_LIMITED_API, not(Py_3_9))))), + doc = "```rust,ignore" + )] + #[cfg_attr( + all(feature = "macros", not(all(Py_LIMITED_API, not(Py_3_9)))), + doc = "```rust" + )] + /// use pyo3::prelude::*; + /// use pyo3::types::PyWeakrefReference; + /// + /// #[pyclass(weakref)] + /// struct Foo { /* fields omitted */ } + /// + /// fn parse_data(reference: Borrowed<'_, '_, PyWeakrefReference>) -> PyResult { + /// if let Some(object) = reference.upgrade_borrowed() { + /// Ok(format!("The object '{}' refered by this reference still exists.", object.getattr("__class__")?.getattr("__qualname__")?)) + /// } else { + /// Ok("The object, which this reference refered to, no longer exists".to_owned()) + /// } + /// } + /// + /// # fn main() -> PyResult<()> { + /// Python::with_gil(|py| { + /// let data = Bound::new(py, Foo{})?; + /// let reference = PyWeakrefReference::new_bound(&data)?; + /// + /// assert_eq!( + /// parse_data(reference.as_borrowed())?, + /// "The object 'Foo' refered by this reference still exists." + /// ); + /// + /// drop(data); + /// + /// assert_eq!( + /// parse_data(reference.as_borrowed())?, + /// "The object, which this reference refered to, no longer exists" + /// ); + /// + /// Ok(()) + /// }) + /// # } + /// ``` + /// + /// # Panics + /// This function panics is the current object is invalid. + /// If used propperly this is never the case. (NonNull and actually a weakref type) + /// + /// [`PyWeakref_GetObject`]: https://docs.python.org/3/c-api/weakref.html#c.PyWeakref_GetObject + /// [`weakref.ReferenceType`]: https://docs.python.org/3/library/weakref.html#weakref.ReferenceType + /// [`weakref.ref`]: https://docs.python.org/3/library/weakref.html#weakref.ref + fn upgrade_borrowed<'a>(&'a self) -> Option> + where + 'py: 'a, + { + let object = self.get_object_borrowed(); + + if object.is_none() { + None + } else { + Some(object) + } + } + + /// Retrieve to a Bound object pointed to by the weakref. + /// + /// This function returns `Bound<'py, PyAny>`, which is either the object if it still exists, otherwise it will refer to [`PyNone`](crate::types::PyNone). + /// + /// This function gets the optional target of this [`weakref.ReferenceType`] (result of calling [`weakref.ref`]). + /// It produces similair results to using [`PyWeakref_GetObject`] in the C api. + /// + /// # Example + #[cfg_attr( + not(all(feature = "macros", not(all(Py_LIMITED_API, not(Py_3_9))))), + doc = "```rust,ignore" + )] + #[cfg_attr( + all(feature = "macros", not(all(Py_LIMITED_API, not(Py_3_9)))), + doc = "```rust" + )] + /// use pyo3::prelude::*; + /// use pyo3::types::PyWeakrefReference; + /// + /// #[pyclass(weakref)] + /// struct Foo { /* fields omitted */ } + /// + /// fn get_class(reference: Borrowed<'_, '_, PyWeakrefReference>) -> PyResult { + /// reference + /// .get_object() + /// .getattr("__class__")? + /// .repr() + /// .map(|repr| repr.to_string()) + /// } + /// + /// # fn main() -> PyResult<()> { + /// Python::with_gil(|py| { + /// let object = Bound::new(py, Foo{})?; + /// let reference = PyWeakrefReference::new_bound(&object)?; + /// + /// assert_eq!( + /// get_class(reference.as_borrowed())?, + /// "" + /// ); + /// + /// drop(object); + /// + /// assert_eq!(get_class(reference.as_borrowed())?, ""); + /// + /// Ok(()) + /// }) + /// # } + /// ``` + /// + /// # Panics + /// This function panics is the current object is invalid. + /// If used propperly this is never the case. (NonNull and actually a weakref type) + /// + /// [`PyWeakref_GetObject`]: https://docs.python.org/3/c-api/weakref.html#c.PyWeakref_GetObject + /// [`weakref.ReferenceType`]: https://docs.python.org/3/library/weakref.html#weakref.ReferenceType + /// [`weakref.ref`]: https://docs.python.org/3/library/weakref.html#weakref.ref + fn get_object(&self) -> Bound<'py, PyAny> { + // PyWeakref_GetObject does some error checking, however we ensure the passed object is Non-Null and a Weakref type. + self.get_object_borrowed().to_owned() + } + + /// Retrieve to a Borrowed object pointed to by the weakref. + /// + /// This function returns `Borrowed<'py, PyAny>`, which is either the object if it still exists, otherwise it will refer to [`PyNone`](crate::types::PyNone). + /// + /// This function gets the optional target of this [`weakref.ReferenceType`] (result of calling [`weakref.ref`]). + /// It produces similair results to using [`PyWeakref_GetObject`] in the C api. + /// + /// # Example + #[cfg_attr( + not(all(feature = "macros", not(all(Py_LIMITED_API, not(Py_3_9))))), + doc = "```rust,ignore" + )] + #[cfg_attr( + all(feature = "macros", not(all(Py_LIMITED_API, not(Py_3_9)))), + doc = "```rust" + )] + /// use pyo3::prelude::*; + /// use pyo3::types::PyWeakrefReference; + /// + /// #[pyclass(weakref)] + /// struct Foo { /* fields omitted */ } + /// + /// fn get_class(reference: Borrowed<'_, '_, PyWeakrefReference>) -> PyResult { + /// reference + /// .get_object_borrowed() + /// .getattr("__class__")? + /// .repr() + /// .map(|repr| repr.to_string()) + /// } + /// + /// # fn main() -> PyResult<()> { + /// Python::with_gil(|py| { + /// let object = Bound::new(py, Foo{})?; + /// let reference = PyWeakrefReference::new_bound(&object)?; + /// + /// assert_eq!( + /// get_class(reference.as_borrowed())?, + /// "" + /// ); + /// + /// drop(object); + /// + /// assert_eq!(get_class(reference.as_borrowed())?, ""); + /// + /// Ok(()) + /// }) + /// # } + /// ``` + /// + /// # Panics + /// This function panics is the current object is invalid. + /// If used propperly this is never the case. (NonNull and actually a weakref type) + /// + /// [`PyWeakref_GetObject`]: https://docs.python.org/3/c-api/weakref.html#c.PyWeakref_GetObject + /// [`weakref.ReferenceType`]: https://docs.python.org/3/library/weakref.html#weakref.ReferenceType + /// [`weakref.ref`]: https://docs.python.org/3/library/weakref.html#weakref.ref + #[track_caller] + // TODO: This function is the reason every function tracks caller, however it only panics when the weakref object is not actually a weakreference type. So is it this neccessary? + fn get_object_borrowed(&self) -> Borrowed<'_, 'py, PyAny>; +} + +impl<'py> PyWeakrefMethods<'py> for Bound<'py, PyWeakref> { + fn get_object_borrowed(&self) -> Borrowed<'_, 'py, PyAny> { + // PyWeakref_GetObject does some error checking, however we ensure the passed object is Non-Null and a Weakref type. + unsafe { ffi::PyWeakref_GetObject(self.as_ptr()).assume_borrowed_or_err(self.py()) } + .expect("The 'weakref' weak reference instance should be valid (non-null and actually a weakref reference)") + } +} + +#[cfg(test)] +mod tests { + use crate::types::any::{PyAny, PyAnyMethods}; + use crate::types::weakref::{PyWeakref, PyWeakrefMethods, PyWeakrefProxy, PyWeakrefReference}; + use crate::{Bound, PyResult, Python}; + + fn new_reference<'py>(object: &Bound<'py, PyAny>) -> PyResult> { + let reference = PyWeakrefReference::new_bound(object)?; + reference.into_any().downcast_into().map_err(Into::into) + } + + fn new_proxy<'py>(object: &Bound<'py, PyAny>) -> PyResult> { + let reference = PyWeakrefProxy::new_bound(object)?; + reference.into_any().downcast_into().map_err(Into::into) + } + + mod python_class { + use super::*; + use crate::{py_result_ext::PyResultExt, types::PyType}; + + fn get_type(py: Python<'_>) -> PyResult> { + py.run_bound("class A:\n pass\n", None, None)?; + py.eval_bound("A", None, None).downcast_into::() + } + + #[test] + fn test_weakref_upgrade_as() -> PyResult<()> { + fn inner( + create_reference: impl for<'py> FnOnce( + &Bound<'py, PyAny>, + ) + -> PyResult>, + ) -> PyResult<()> { + Python::with_gil(|py| { + let class = get_type(py)?; + let object = class.call0()?; + let reference = create_reference(&object)?; + + { + // This test is a bit weird but ok. + let obj = reference.upgrade_as::(); + + assert!(obj.is_ok()); + let obj = obj.unwrap(); + + assert!(obj.is_some()); + assert!(obj.map_or(false, |obj| obj.as_ptr() == object.as_ptr() + && obj.is_exact_instance(&class))); + } + + drop(object); + + { + // This test is a bit weird but ok. + let obj = reference.upgrade_as::(); + + assert!(obj.is_ok()); + let obj = obj.unwrap(); + + assert!(obj.is_none()); + } + + Ok(()) + }) + } + + inner(new_reference)?; + inner(new_proxy) + } + + #[test] + fn test_weakref_upgrade_borrowed_as() -> PyResult<()> { + fn inner( + create_reference: impl for<'py> FnOnce( + &Bound<'py, PyAny>, + ) + -> PyResult>, + ) -> PyResult<()> { + Python::with_gil(|py| { + let class = get_type(py)?; + let object = class.call0()?; + let reference = create_reference(&object)?; + + { + // This test is a bit weird but ok. + let obj = reference.upgrade_borrowed_as::(); + + assert!(obj.is_ok()); + let obj = obj.unwrap(); + + assert!(obj.is_some()); + assert!(obj.map_or(false, |obj| obj.as_ptr() == object.as_ptr() + && obj.is_exact_instance(&class))); + } + + drop(object); + + { + // This test is a bit weird but ok. + let obj = reference.upgrade_borrowed_as::(); + + assert!(obj.is_ok()); + let obj = obj.unwrap(); + + assert!(obj.is_none()); + } + + Ok(()) + }) + } + + inner(new_reference)?; + inner(new_proxy) + } + + #[test] + fn test_weakref_upgrade_as_unchecked() -> PyResult<()> { + fn inner( + create_reference: impl for<'py> FnOnce( + &Bound<'py, PyAny>, + ) + -> PyResult>, + ) -> PyResult<()> { + Python::with_gil(|py| { + let class = get_type(py)?; + let object = class.call0()?; + let reference = create_reference(&object)?; + + { + // This test is a bit weird but ok. + let obj = unsafe { reference.upgrade_as_unchecked::() }; + + assert!(obj.is_some()); + assert!(obj.map_or(false, |obj| obj.as_ptr() == object.as_ptr() + && obj.is_exact_instance(&class))); + } + + drop(object); + + { + // This test is a bit weird but ok. + let obj = unsafe { reference.upgrade_as_unchecked::() }; + + assert!(obj.is_none()); + } + + Ok(()) + }) + } + + inner(new_reference)?; + inner(new_proxy) + } + + #[test] + fn test_weakref_upgrade_borrowed_as_unchecked() -> PyResult<()> { + fn inner( + create_reference: impl for<'py> FnOnce( + &Bound<'py, PyAny>, + ) + -> PyResult>, + ) -> PyResult<()> { + Python::with_gil(|py| { + let class = get_type(py)?; + let object = class.call0()?; + let reference = create_reference(&object)?; + + { + // This test is a bit weird but ok. + let obj = unsafe { reference.upgrade_borrowed_as_unchecked::() }; + + assert!(obj.is_some()); + assert!(obj.map_or(false, |obj| obj.as_ptr() == object.as_ptr() + && obj.is_exact_instance(&class))); + } + + drop(object); + + { + // This test is a bit weird but ok. + let obj = unsafe { reference.upgrade_borrowed_as_unchecked::() }; + + assert!(obj.is_none()); + } + + Ok(()) + }) + } + + inner(new_reference)?; + inner(new_proxy) + } + + #[test] + fn test_weakref_upgrade() -> PyResult<()> { + fn inner( + create_reference: impl for<'py> FnOnce( + &Bound<'py, PyAny>, + ) + -> PyResult>, + call_retrievable: bool, + ) -> PyResult<()> { + let not_call_retrievable = !call_retrievable; + + Python::with_gil(|py| { + let class = get_type(py)?; + let object = class.call0()?; + let reference = create_reference(&object)?; + + assert!(not_call_retrievable || reference.call0()?.is(&object)); + assert!(reference.upgrade().is_some()); + assert!(reference.upgrade().map_or(false, |obj| obj.is(&object))); + + drop(object); + + assert!(not_call_retrievable || reference.call0()?.is_none()); + assert!(reference.upgrade().is_none()); + + Ok(()) + }) + } + + inner(new_reference, true)?; + inner(new_proxy, false) + } + + #[test] + fn test_weakref_upgrade_borrowed() -> PyResult<()> { + fn inner( + create_reference: impl for<'py> FnOnce( + &Bound<'py, PyAny>, + ) + -> PyResult>, + call_retrievable: bool, + ) -> PyResult<()> { + let not_call_retrievable = !call_retrievable; + + Python::with_gil(|py| { + let class = get_type(py)?; + let object = class.call0()?; + let reference = create_reference(&object)?; + + assert!(not_call_retrievable || reference.call0()?.is(&object)); + assert!(reference.upgrade_borrowed().is_some()); + assert!(reference + .upgrade_borrowed() + .map_or(false, |obj| obj.is(&object))); + + drop(object); + + assert!(not_call_retrievable || reference.call0()?.is_none()); + assert!(reference.upgrade_borrowed().is_none()); + + Ok(()) + }) + } + + inner(new_reference, true)?; + inner(new_proxy, false) + } + + #[test] + fn test_weakref_get_object() -> PyResult<()> { + fn inner( + create_reference: impl for<'py> FnOnce( + &Bound<'py, PyAny>, + ) + -> PyResult>, + call_retrievable: bool, + ) -> PyResult<()> { + let not_call_retrievable = !call_retrievable; + + Python::with_gil(|py| { + let class = get_type(py)?; + let object = class.call0()?; + let reference = create_reference(&object)?; + + assert!(not_call_retrievable || reference.call0()?.is(&object)); + assert!(reference.get_object().is(&object)); + + drop(object); + + assert!(not_call_retrievable || reference.call0()?.is(&reference.get_object())); + assert!(not_call_retrievable || reference.call0()?.is_none()); + assert!(reference.get_object().is_none()); + + Ok(()) + }) + } + + inner(new_reference, true)?; + inner(new_proxy, false) + } + + #[test] + fn test_weakref_get_object_borrowed() -> PyResult<()> { + fn inner( + create_reference: impl for<'py> FnOnce( + &Bound<'py, PyAny>, + ) + -> PyResult>, + call_retrievable: bool, + ) -> PyResult<()> { + let not_call_retrievable = !call_retrievable; + + Python::with_gil(|py| { + let class = get_type(py)?; + let object = class.call0()?; + let reference = create_reference(&object)?; + + assert!(not_call_retrievable || reference.call0()?.is(&object)); + assert!(reference.get_object_borrowed().is(&object)); + + drop(object); + + assert!(not_call_retrievable || reference.call0()?.is_none()); + assert!(reference.get_object_borrowed().is_none()); + + Ok(()) + }) + } + + inner(new_reference, true)?; + inner(new_proxy, false) + } + } + + // under 'abi3-py37' and 'abi3-py38' PyClass cannot be weakreferencable. + #[cfg(all(feature = "macros", not(all(Py_LIMITED_API, not(Py_3_9)))))] + mod pyo3_pyclass { + use super::*; + use crate::{pyclass, Py}; + + #[pyclass(weakref, crate = "crate")] + struct WeakrefablePyClass {} + + #[test] + fn test_weakref_upgrade_as() -> PyResult<()> { + fn inner( + create_reference: impl for<'py> FnOnce( + &Bound<'py, PyAny>, + ) + -> PyResult>, + ) -> PyResult<()> { + Python::with_gil(|py| { + let object = Py::new(py, WeakrefablePyClass {})?; + let reference = create_reference(object.bind(py))?; + + { + let obj = reference.upgrade_as::(); + + assert!(obj.is_ok()); + let obj = obj.unwrap(); + + assert!(obj.is_some()); + assert!(obj.map_or(false, |obj| obj.as_ptr() == object.as_ptr())); + } + + drop(object); + + { + let obj = reference.upgrade_as::(); + + assert!(obj.is_ok()); + let obj = obj.unwrap(); + + assert!(obj.is_none()); + } + + Ok(()) + }) + } + + inner(new_reference)?; + inner(new_proxy) + } + + #[test] + fn test_weakref_upgrade_borrowed_as() -> PyResult<()> { + fn inner( + create_reference: impl for<'py> FnOnce( + &Bound<'py, PyAny>, + ) + -> PyResult>, + ) -> PyResult<()> { + Python::with_gil(|py| { + let object = Py::new(py, WeakrefablePyClass {})?; + let reference = create_reference(object.bind(py))?; + + { + let obj = reference.upgrade_borrowed_as::(); + + assert!(obj.is_ok()); + let obj = obj.unwrap(); + + assert!(obj.is_some()); + assert!(obj.map_or(false, |obj| obj.as_ptr() == object.as_ptr())); + } + + drop(object); + + { + let obj = reference.upgrade_borrowed_as::(); + + assert!(obj.is_ok()); + let obj = obj.unwrap(); + + assert!(obj.is_none()); + } + + Ok(()) + }) + } + + inner(new_reference)?; + inner(new_proxy) + } + + #[test] + fn test_weakref_upgrade_as_unchecked() -> PyResult<()> { + fn inner( + create_reference: impl for<'py> FnOnce( + &Bound<'py, PyAny>, + ) + -> PyResult>, + ) -> PyResult<()> { + Python::with_gil(|py| { + let object = Py::new(py, WeakrefablePyClass {})?; + let reference = create_reference(object.bind(py))?; + + { + let obj = unsafe { reference.upgrade_as_unchecked::() }; + + assert!(obj.is_some()); + assert!(obj.map_or(false, |obj| obj.as_ptr() == object.as_ptr())); + } + + drop(object); + + { + let obj = unsafe { reference.upgrade_as_unchecked::() }; + + assert!(obj.is_none()); + } + + Ok(()) + }) + } + + inner(new_reference)?; + inner(new_proxy) + } + + #[test] + fn test_weakref_upgrade_borrowed_as_unchecked() -> PyResult<()> { + fn inner( + create_reference: impl for<'py> FnOnce( + &Bound<'py, PyAny>, + ) + -> PyResult>, + ) -> PyResult<()> { + Python::with_gil(|py| { + let object = Py::new(py, WeakrefablePyClass {})?; + let reference = create_reference(object.bind(py))?; + + { + let obj = unsafe { + reference.upgrade_borrowed_as_unchecked::() + }; + + assert!(obj.is_some()); + assert!(obj.map_or(false, |obj| obj.as_ptr() == object.as_ptr())); + } + + drop(object); + + { + let obj = unsafe { + reference.upgrade_borrowed_as_unchecked::() + }; + + assert!(obj.is_none()); + } + + Ok(()) + }) + } + + inner(new_reference)?; + inner(new_proxy) + } + + #[test] + fn test_weakref_upgrade() -> PyResult<()> { + fn inner( + create_reference: impl for<'py> FnOnce( + &Bound<'py, PyAny>, + ) + -> PyResult>, + call_retrievable: bool, + ) -> PyResult<()> { + let not_call_retrievable = !call_retrievable; + + Python::with_gil(|py| { + let object = Py::new(py, WeakrefablePyClass {})?; + let reference = create_reference(object.bind(py))?; + + assert!(not_call_retrievable || reference.call0()?.is(&object)); + assert!(reference.upgrade().is_some()); + assert!(reference.upgrade().map_or(false, |obj| obj.is(&object))); + + drop(object); + + assert!(not_call_retrievable || reference.call0()?.is_none()); + assert!(reference.upgrade().is_none()); + + Ok(()) + }) + } + + inner(new_reference, true)?; + inner(new_proxy, false) + } + + #[test] + fn test_weakref_upgrade_borrowed() -> PyResult<()> { + fn inner( + create_reference: impl for<'py> FnOnce( + &Bound<'py, PyAny>, + ) + -> PyResult>, + call_retrievable: bool, + ) -> PyResult<()> { + let not_call_retrievable = !call_retrievable; + + Python::with_gil(|py| { + let object = Py::new(py, WeakrefablePyClass {})?; + let reference = create_reference(object.bind(py))?; + + assert!(not_call_retrievable || reference.call0()?.is(&object)); + assert!(reference.upgrade_borrowed().is_some()); + assert!(reference + .upgrade_borrowed() + .map_or(false, |obj| obj.is(&object))); + + drop(object); + + assert!(not_call_retrievable || reference.call0()?.is_none()); + assert!(reference.upgrade_borrowed().is_none()); + + Ok(()) + }) + } + + inner(new_reference, true)?; + inner(new_proxy, false) + } + + #[test] + fn test_weakref_get_object() -> PyResult<()> { + fn inner( + create_reference: impl for<'py> FnOnce( + &Bound<'py, PyAny>, + ) + -> PyResult>, + call_retrievable: bool, + ) -> PyResult<()> { + let not_call_retrievable = !call_retrievable; + + Python::with_gil(|py| { + let object = Py::new(py, WeakrefablePyClass {})?; + let reference = create_reference(object.bind(py))?; + + assert!(not_call_retrievable || reference.call0()?.is(&object)); + assert!(reference.get_object().is(&object)); + + drop(object); + + assert!(not_call_retrievable || reference.call0()?.is(&reference.get_object())); + assert!(not_call_retrievable || reference.call0()?.is_none()); + assert!(reference.get_object().is_none()); + + Ok(()) + }) + } + + inner(new_reference, true)?; + inner(new_proxy, false) + } + + #[test] + fn test_weakref_get_object_borrowed() -> PyResult<()> { + fn inner( + create_reference: impl for<'py> FnOnce( + &Bound<'py, PyAny>, + ) + -> PyResult>, + call_retrievable: bool, + ) -> PyResult<()> { + let not_call_retrievable = !call_retrievable; + + Python::with_gil(|py| { + let object = Py::new(py, WeakrefablePyClass {})?; + let reference = create_reference(object.bind(py))?; + + assert!(not_call_retrievable || reference.call0()?.is(&object)); + assert!(reference.get_object_borrowed().is(&object)); + + drop(object); + + assert!(not_call_retrievable || reference.call0()?.is_none()); + assert!(reference.get_object_borrowed().is_none()); + + Ok(()) + }) + } + + inner(new_reference, true)?; + inner(new_proxy, false) + } + } +} diff --git a/src/types/weakref/mod.rs b/src/types/weakref/mod.rs new file mode 100644 index 00000000000..49d4e515b12 --- /dev/null +++ b/src/types/weakref/mod.rs @@ -0,0 +1,7 @@ +pub use anyref::{PyWeakref, PyWeakrefMethods}; +pub use proxy::PyWeakrefProxy; +pub use reference::PyWeakrefReference; + +pub(crate) mod anyref; +pub(crate) mod proxy; +pub(crate) mod reference; diff --git a/src/types/weakref/proxy.rs b/src/types/weakref/proxy.rs new file mode 100644 index 00000000000..71334488b54 --- /dev/null +++ b/src/types/weakref/proxy.rs @@ -0,0 +1,1688 @@ +use crate::err::PyResult; +use crate::ffi_ptr_ext::FfiPtrExt; +use crate::py_result_ext::PyResultExt; +use crate::type_object::PyTypeCheck; +use crate::types::any::PyAny; +use crate::{ffi, Borrowed, Bound, ToPyObject}; + +#[cfg(feature = "gil-refs")] +use crate::{type_object::PyTypeInfo, PyNativeType}; + +use super::PyWeakrefMethods; + +/// Represents any Python `weakref` Proxy type. +/// +/// In Python this is created by calling `weakref.proxy`. +/// This is either a `weakref.ProxyType` or a `weakref.CallableProxyType` (`weakref.ProxyTypes`). +#[repr(transparent)] +pub struct PyWeakrefProxy(PyAny); + +pyobject_native_type_named!(PyWeakrefProxy); +pyobject_native_type_extract!(PyWeakrefProxy); + +// TODO: We known the layout but this cannot be implemented, due to the lack of public typeobject pointers. And it is 2 distinct types +// #[cfg(not(Py_LIMITED_API))] +// pyobject_native_type_sized!(PyWeakrefProxy, ffi::PyWeakReference); + +impl PyTypeCheck for PyWeakrefProxy { + const NAME: &'static str = "weakref.ProxyTypes"; + + fn type_check(object: &Bound<'_, PyAny>) -> bool { + unsafe { ffi::PyWeakref_CheckProxy(object.as_ptr()) > 0 } + } +} + +/// TODO: UPDATE DOCS +impl PyWeakrefProxy { + /// Constructs a new Weak Reference (`weakref.proxy`/`weakref.ProxyType`/`weakref.CallableProxyType`) for the given object. + /// + /// Returns a `TypeError` if `object` is not weak referenceable (Most native types and PyClasses without `weakref` flag). + /// + /// # Examples + #[cfg_attr( + not(all(feature = "macros", not(all(Py_LIMITED_API, not(Py_3_9))))), + doc = "```rust,ignore" + )] + #[cfg_attr( + all(feature = "macros", not(all(Py_LIMITED_API, not(Py_3_9)))), + doc = "```rust" + )] + /// use pyo3::prelude::*; + /// use pyo3::types::PyWeakrefProxy; + /// + /// #[pyclass(weakref)] + /// struct Foo { /* fields omitted */ } + /// + /// # fn main() -> PyResult<()> { + /// Python::with_gil(|py| { + /// let foo = Bound::new(py, Foo {})?; + /// let weakref = PyWeakrefProxy::new_bound(&foo)?; + /// assert!( + /// // In normal situations where a direct `Bound<'py, Foo>` is required use `upgrade::` + /// weakref.upgrade() + /// .map_or(false, |obj| obj.is(&foo)) + /// ); + /// + /// let weakref2 = PyWeakrefProxy::new_bound(&foo)?; + /// assert!(weakref.is(&weakref2)); + /// + /// drop(foo); + /// + /// assert!(weakref.upgrade().is_none()); + /// Ok(()) + /// }) + /// # } + /// ``` + #[inline] + pub fn new_bound<'py>(object: &Bound<'py, PyAny>) -> PyResult> { + // TODO: Is this inner pattern still necessary Here? + fn inner<'py>(object: &Bound<'py, PyAny>) -> PyResult> { + unsafe { + Bound::from_owned_ptr_or_err( + object.py(), + ffi::PyWeakref_NewProxy(object.as_ptr(), ffi::Py_None()), + ) + .downcast_into_unchecked() + } + } + + inner(object) + } + + /// Constructs a new Weak Reference (`weakref.proxy`/`weakref.ProxyType`/`weakref.CallableProxyType`) for the given object with a callback. + /// + /// Returns a `TypeError` if `object` is not weak referenceable (Most native types and PyClasses without `weakref` flag) or if the `callback` is not callable or None. + /// + /// # Examples + #[cfg_attr( + not(all(feature = "macros", not(all(Py_LIMITED_API, not(Py_3_9))))), + doc = "```rust,ignore" + )] + #[cfg_attr( + all(feature = "macros", not(all(Py_LIMITED_API, not(Py_3_9)))), + doc = "```rust" + )] + /// use pyo3::prelude::*; + /// use pyo3::types::PyWeakrefProxy; + /// + /// #[pyclass(weakref)] + /// struct Foo { /* fields omitted */ } + /// + /// #[pyfunction] + /// fn callback(wref: Bound<'_, PyWeakrefProxy>) -> PyResult<()> { + /// let py = wref.py(); + /// assert!(wref.upgrade_as::()?.is_none()); + /// py.run_bound("counter = 1", None, None) + /// } + /// + /// # fn main() -> PyResult<()> { + /// Python::with_gil(|py| { + /// py.run_bound("counter = 0", None, None)?; + /// assert_eq!(py.eval_bound("counter", None, None)?.extract::()?, 0); + /// let foo = Bound::new(py, Foo{})?; + /// + /// // This is fine. + /// let weakref = PyWeakrefProxy::new_bound_with(&foo, py.None())?; + /// assert!(weakref.upgrade_as::()?.is_some()); + /// assert!( + /// // In normal situations where a direct `Bound<'py, Foo>` is required use `upgrade::` + /// weakref.upgrade() + /// .map_or(false, |obj| obj.is(&foo)) + /// ); + /// assert_eq!(py.eval_bound("counter", None, None)?.extract::()?, 0); + /// + /// let weakref2 = PyWeakrefProxy::new_bound_with(&foo, wrap_pyfunction_bound!(callback, py)?)?; + /// assert!(!weakref.is(&weakref2)); // Not the same weakref + /// assert!(weakref.eq(&weakref2)?); // But Equal, since they point to the same object + /// + /// drop(foo); + /// + /// assert!(weakref.upgrade_as::()?.is_none()); + /// assert_eq!(py.eval_bound("counter", None, None)?.extract::()?, 1); + /// Ok(()) + /// }) + /// # } + /// ``` + #[inline] + pub fn new_bound_with<'py, C>( + object: &Bound<'py, PyAny>, + callback: C, + ) -> PyResult> + where + C: ToPyObject, + { + fn inner<'py>( + object: &Bound<'py, PyAny>, + callback: Bound<'py, PyAny>, + ) -> PyResult> { + unsafe { + Bound::from_owned_ptr_or_err( + object.py(), + ffi::PyWeakref_NewProxy(object.as_ptr(), callback.as_ptr()), + ) + .downcast_into_unchecked() + } + } + + let py = object.py(); + inner(object, callback.to_object(py).into_bound(py)) + } +} + +/// TODO: UPDATE DOCS +#[cfg(feature = "gil-refs")] +impl PyWeakrefProxy { + /// Deprecated form of [`PyWeakrefProxy::new_bound`]. + #[inline] + #[deprecated( + since = "0.21.0", + note = "`PyWeakrefProxy::new` will be replaced by `PyWeakrefProxy::new_bound` in a future PyO3 version" + )] + pub fn new(object: &T) -> PyResult<&PyWeakrefProxy> + where + T: PyNativeType, + { + Self::new_bound(object.as_borrowed().as_any()).map(Bound::into_gil_ref) + } + + /// Deprecated form of [`PyWeakrefProxy::new_bound_with`]. + #[inline] + #[deprecated( + since = "0.21.0", + note = "`PyWeakrefProxy::new_with` will be replaced by `PyWeakrefProxy::new_bound_with` in a future PyO3 version" + )] + pub fn new_with(object: &T, callback: C) -> PyResult<&PyWeakrefProxy> + where + T: PyNativeType, + C: ToPyObject, + { + Self::new_bound_with(object.as_borrowed().as_any(), callback).map(Bound::into_gil_ref) + } + + /// Upgrade the weakref to a direct object reference. + /// + /// It is named `upgrade` to be inline with [rust's `Weak::upgrade`](std::rc::Weak::upgrade). + /// In Python it would be equivalent to [`PyWeakref_GetObject`]. + /// + /// # Example + #[cfg_attr( + not(all(feature = "macros", not(all(Py_LIMITED_API, not(Py_3_9))))), + doc = "```rust,ignore" + )] + #[cfg_attr( + all(feature = "macros", not(all(Py_LIMITED_API, not(Py_3_9)))), + doc = "```rust" + )] + /// use pyo3::prelude::*; + /// use pyo3::types::PyWeakrefProxy; + /// + /// #[pyclass(weakref)] + /// struct Foo { /* fields omitted */ } + /// + /// #[pymethods] + /// impl Foo { + /// fn get_data(&self) -> (&str, u32) { + /// ("Dave", 10) + /// } + /// } + /// + /// fn parse_data(reference: Borrowed<'_, '_, PyWeakrefProxy>) -> PyResult { + /// if let Some(data_src) = reference.upgrade_as::()? { + /// let data = data_src.borrow(); + /// let (name, score) = data.get_data(); + /// Ok(format!("Processing '{}': score = {}", name, score)) + /// } else { + /// Ok("The supplied data reference is nolonger relavent.".to_owned()) + /// } + /// } + /// + /// # fn main() -> PyResult<()> { + /// Python::with_gil(|py| { + /// let data = Bound::new(py, Foo{})?; + /// let reference = PyWeakrefProxy::new_bound(&data)?; + /// + /// assert_eq!( + /// parse_data(reference.as_borrowed())?, + /// "Processing 'Dave': score = 10" + /// ); + /// + /// drop(data); + /// + /// assert_eq!( + /// parse_data(reference.as_borrowed())?, + /// "The supplied data reference is nolonger relavent." + /// ); + /// + /// Ok(()) + /// }) + /// # } + /// ``` + /// + /// # Panics + /// This function panics is the current object is invalid. + /// If used propperly this is never the case. (NonNull and actually a weakref type) + /// + /// [`PyWeakref_GetObject`]: https://docs.python.org/3/c-api/weakref.html#c.PyWeakref_GetObject + /// [`weakref.ProxyType`]: https://docs.python.org/3/library/weakref.html#weakref.ProxyType + /// [`weakref.proxy`]: https://docs.python.org/3/library/weakref.html#weakref.proxy + pub fn upgrade_as(&self) -> PyResult> + where + T: PyTypeCheck, + { + Ok(self + .as_borrowed() + .upgrade_as::()? + .map(Bound::into_gil_ref)) + } + + /// Upgrade the weakref to a direct object reference unchecked. The type of the recovered object is not checked before downcasting, this could lead to unexpected behavior. Use only when absolutely certain the type can be guaranteed. The `weakref` may still return `None`. + /// + /// It is named `upgrade` to be inline with [rust's `Weak::upgrade`](std::rc::Weak::upgrade). + /// In Python it would be equivalent to [`PyWeakref_GetObject`]. + /// + /// # Safety + /// Callers must ensure that the type is valid or risk type confusion. + /// The `weakref` is still allowed to be `None`, if the referenced object has been cleaned up. + /// + /// # Example + #[cfg_attr( + not(all(feature = "macros", not(all(Py_LIMITED_API, not(Py_3_9))))), + doc = "```rust,ignore" + )] + #[cfg_attr( + all(feature = "macros", not(all(Py_LIMITED_API, not(Py_3_9)))), + doc = "```rust" + )] + /// use pyo3::prelude::*; + /// use pyo3::types::PyWeakrefProxy; + /// + /// #[pyclass(weakref)] + /// struct Foo { /* fields omitted */ } + /// + /// #[pymethods] + /// impl Foo { + /// fn get_data(&self) -> (&str, u32) { + /// ("Dave", 10) + /// } + /// } + /// + /// fn parse_data(reference: Borrowed<'_, '_, PyWeakrefProxy>) -> String { + /// if let Some(data_src) = unsafe { reference.upgrade_as_unchecked::() } { + /// let data = data_src.borrow(); + /// let (name, score) = data.get_data(); + /// format!("Processing '{}': score = {}", name, score) + /// } else { + /// "The supplied data reference is nolonger relavent.".to_owned() + /// } + /// } + /// + /// # fn main() -> PyResult<()> { + /// Python::with_gil(|py| { + /// let data = Bound::new(py, Foo{})?; + /// let reference = PyWeakrefProxy::new_bound(&data)?; + /// + /// assert_eq!( + /// parse_data(reference.as_borrowed()), + /// "Processing 'Dave': score = 10" + /// ); + /// + /// drop(data); + /// + /// assert_eq!( + /// parse_data(reference.as_borrowed()), + /// "The supplied data reference is nolonger relavent." + /// ); + /// + /// Ok(()) + /// }) + /// # } + /// ``` + /// + /// # Panics + /// This function panics is the current object is invalid. + /// If used propperly this is never the case. (NonNull and actually a weakref type) + /// + /// [`PyWeakref_GetObject`]: https://docs.python.org/3/c-api/weakref.html#c.PyWeakref_GetObject + /// [`weakref.ProxyType`]: https://docs.python.org/3/library/weakref.html#weakref.ProxyType + /// [`weakref.proxy`]: https://docs.python.org/3/library/weakref.html#weakref.proxy + pub unsafe fn upgrade_as_unchecked(&self) -> Option<&T::AsRefTarget> + where + T: PyTypeCheck, + { + self.as_borrowed() + .upgrade_as_unchecked::() + .map(Bound::into_gil_ref) + } + + /// Upgrade the weakref to an exact direct object reference. + /// + /// It is named `upgrade` to be inline with [rust's `Weak::upgrade`](std::rc::Weak::upgrade). + /// In Python it would be equivalent to [`PyWeakref_GetObject`]. + /// + /// # Example + #[cfg_attr( + not(all(feature = "macros", not(all(Py_LIMITED_API, not(Py_3_9))))), + doc = "```rust,ignore" + )] + #[cfg_attr( + all(feature = "macros", not(all(Py_LIMITED_API, not(Py_3_9)))), + doc = "```rust" + )] + /// use pyo3::prelude::*; + /// use pyo3::types::PyWeakrefProxy; + /// + /// #[pyclass(weakref)] + /// struct Foo { /* fields omitted */ } + /// + /// #[pymethods] + /// impl Foo { + /// fn get_data(&self) -> (&str, u32) { + /// ("Dave", 10) + /// } + /// } + /// + /// fn parse_data(reference: Borrowed<'_, '_, PyWeakrefProxy>) -> PyResult { + /// if let Some(data_src) = reference.upgrade_as_exact::()? { + /// let data = data_src.borrow(); + /// let (name, score) = data.get_data(); + /// Ok(format!("Processing '{}': score = {}", name, score)) + /// } else { + /// Ok("The supplied data reference is nolonger relavent.".to_owned()) + /// } + /// } + /// + /// # fn main() -> PyResult<()> { + /// Python::with_gil(|py| { + /// let data = Bound::new(py, Foo{})?; + /// let reference = PyWeakrefProxy::new_bound(&data)?; + /// + /// assert_eq!( + /// parse_data(reference.as_borrowed())?, + /// "Processing 'Dave': score = 10" + /// ); + /// + /// drop(data); + /// + /// assert_eq!( + /// parse_data(reference.as_borrowed())?, + /// "The supplied data reference is nolonger relavent." + /// ); + /// + /// Ok(()) + /// }) + /// # } + /// ``` + /// + /// # Panics + /// This function panics is the current object is invalid. + /// If used propperly this is never the case. (NonNull and actually a weakref type) + /// + /// [`PyWeakref_GetObject`]: https://docs.python.org/3/c-api/weakref.html#c.PyWeakref_GetObject + /// [`weakref.ProxyType`]: https://docs.python.org/3/library/weakref.html#weakref.ProxyType + /// [`weakref.proxy`]: https://docs.python.org/3/library/weakref.html#weakref.proxy + pub fn upgrade_as_exact(&self) -> PyResult> + where + T: PyTypeInfo, + { + Ok(self + .as_borrowed() + .upgrade_as_exact::()? + .map(Bound::into_gil_ref)) + } + + /// Upgrade the weakref to a [`PyAny`] reference to the target if possible. + /// + /// It is named `upgrade` to be inline with [rust's `Weak::upgrade`](std::rc::Weak::upgrade). + /// This function returns `Some(&'py PyAny)` if the reference still exists, otherwise `None` will be returned. + /// + /// This function gets the optional target of this [`weakref.ProxyType`] (or [`weakref.CallableProxyType`], result of calling [`weakref.proxy`]). + /// It produces similair results using [`PyWeakref_GetObject`] in the C api. + /// + /// # Example + #[cfg_attr( + not(all(feature = "macros", not(all(Py_LIMITED_API, not(Py_3_9))))), + doc = "```rust,ignore" + )] + #[cfg_attr( + all(feature = "macros", not(all(Py_LIMITED_API, not(Py_3_9)))), + doc = "```rust" + )] + /// use pyo3::prelude::*; + /// use pyo3::types::PyWeakrefProxy; + /// + /// #[pyclass(weakref)] + /// struct Foo { /* fields omitted */ } + /// + /// fn parse_data(reference: Borrowed<'_, '_, PyWeakrefProxy>) -> PyResult { + /// if let Some(object) = reference.upgrade() { + /// Ok(format!("The object '{}' refered by this reference still exists.", object.getattr("__class__")?.getattr("__qualname__")?)) + /// } else { + /// Ok("The object, which this reference refered to, no longer exists".to_owned()) + /// } + /// } + /// + /// # fn main() -> PyResult<()> { + /// Python::with_gil(|py| { + /// let data = Bound::new(py, Foo{})?; + /// let reference = PyWeakrefProxy::new_bound(&data)?; + /// + /// assert_eq!( + /// parse_data(reference.as_borrowed())?, + /// "The object 'Foo' refered by this reference still exists." + /// ); + /// + /// drop(data); + /// + /// assert_eq!( + /// parse_data(reference.as_borrowed())?, + /// "The object, which this reference refered to, no longer exists" + /// ); + /// + /// Ok(()) + /// }) + /// # } + /// ``` + /// + /// # Panics + /// This function panics is the current object is invalid. + /// If used propperly this is never the case. (NonNull and actually a weakref type) + /// + /// [`PyWeakref_GetObject`]: https://docs.python.org/3/c-api/weakref.html#c.PyWeakref_GetObject + /// [`weakref.ProxyType`]: https://docs.python.org/3/library/weakref.html#weakref.ProxyType + /// [`weakref.CallableProxyType`]: https://docs.python.org/3/library/weakref.html#weakref.CallableProxyType + /// [`weakref.proxy`]: https://docs.python.org/3/library/weakref.html#weakref.proxy + pub fn upgrade(&self) -> Option<&'_ PyAny> { + self.as_borrowed().upgrade().map(Bound::into_gil_ref) + } + + /// Retrieve to a object pointed to by the weakref. + /// + /// This function returns `&'py PyAny`, which is either the object if it still exists, otherwise it will refer to [`PyNone`](crate::types::PyNone). + /// + /// This function gets the optional target of this [`weakref.ProxyType`] (or [`weakref.CallableProxyType`], result of calling [`weakref.proxy`]). + /// It produces similair results using [`PyWeakref_GetObject`] in the C api. + /// + /// # Example + #[cfg_attr( + not(all(feature = "macros", not(all(Py_LIMITED_API, not(Py_3_9))))), + doc = "```rust,ignore" + )] + #[cfg_attr( + all(feature = "macros", not(all(Py_LIMITED_API, not(Py_3_9)))), + doc = "```rust" + )] + /// use pyo3::prelude::*; + /// use pyo3::types::PyWeakrefProxy; + /// + /// #[pyclass(weakref)] + /// struct Foo { /* fields omitted */ } + /// + /// fn get_class(reference: Borrowed<'_, '_, PyWeakrefProxy>) -> PyResult { + /// reference + /// .get_object() + /// .getattr("__class__")? + /// .repr() + /// .map(|repr| repr.to_string()) + /// } + /// + /// # fn main() -> PyResult<()> { + /// Python::with_gil(|py| { + /// let object = Bound::new(py, Foo{})?; + /// let reference = PyWeakrefProxy::new_bound(&object)?; + /// + /// assert_eq!( + /// get_class(reference.as_borrowed())?, + /// "" + /// ); + /// + /// drop(object); + /// + /// assert_eq!(get_class(reference.as_borrowed())?, ""); + /// + /// Ok(()) + /// }) + /// # } + /// ``` + /// + /// # Panics + /// This function panics is the current object is invalid. + /// If used propperly this is never the case. (NonNull and actually a weakref type) + /// + /// [`PyWeakref_GetObject`]: https://docs.python.org/3/c-api/weakref.html#c.PyWeakref_GetObject + /// [`weakref.ProxyType`]: https://docs.python.org/3/library/weakref.html#weakref.ProxyType + /// [`weakref.CallableProxyType`]: https://docs.python.org/3/library/weakref.html#weakref.CallableProxyType + /// [`weakref.proxy`]: https://docs.python.org/3/library/weakref.html#weakref.proxy + pub fn get_object(&self) -> &'_ PyAny { + self.as_borrowed().get_object().into_gil_ref() + } +} + +impl<'py> PyWeakrefMethods<'py> for Bound<'py, PyWeakrefProxy> { + fn get_object_borrowed(&self) -> Borrowed<'_, 'py, PyAny> { + // PyWeakref_GetObject does some error checking, however we ensure the passed object is Non-Null and a Weakref type. + unsafe { ffi::PyWeakref_GetObject(self.as_ptr()).assume_borrowed_or_err(self.py()) } + .expect("The 'weakref.ProxyType' (or `weakref.CallableProxyType`) instance should be valid (non-null and actually a weakref reference)") + } +} + +#[cfg(test)] +mod tests { + use crate::exceptions::{PyAttributeError, PyReferenceError, PyTypeError}; + use crate::types::any::{PyAny, PyAnyMethods}; + use crate::types::weakref::{PyWeakrefMethods, PyWeakrefProxy}; + use crate::{Bound, PyResult, Python}; + + #[cfg(all(Py_3_13, not(Py_LIMITED_API)))] + const DEADREF_FIX: Option<&str> = None; + #[cfg(all(not(Py_3_13), not(Py_LIMITED_API)))] + const DEADREF_FIX: Option<&str> = Some("NoneType"); + + #[cfg(not(Py_LIMITED_API))] + fn check_repr( + reference: &Bound<'_, PyWeakrefProxy>, + object: &Bound<'_, PyAny>, + class: Option<&str>, + ) -> PyResult<()> { + let repr = reference.repr()?.to_string(); + + #[cfg(Py_3_13)] + let (first_part, second_part) = repr.split_once(';').unwrap(); + #[cfg(not(Py_3_13))] + let (first_part, second_part) = repr.split_once(" to ").unwrap(); + + { + let (msg, addr) = first_part.split_once("0x").unwrap(); + + assert_eq!(msg, ") -> PyResult> { + py.run_bound("class A:\n pass\n", None, None)?; + py.eval_bound("A", None, None).downcast_into::() + } + + #[test] + fn test_weakref_proxy_behavior() -> PyResult<()> { + Python::with_gil(|py| { + let class = get_type(py)?; + let object = class.call0()?; + let reference = PyWeakrefProxy::new_bound(&object)?; + + assert!(!reference.is(&object)); + assert!(reference.get_object().is(&object)); + + #[cfg(not(Py_LIMITED_API))] + assert_eq!( + reference.get_type().to_string(), + format!("", CLASS_NAME) + ); + + assert_eq!( + reference.getattr("__class__")?.to_string(), + "" + ); + #[cfg(not(Py_LIMITED_API))] + check_repr(&reference, &object, Some("A"))?; + + assert!(reference + .getattr("__callback__") + .err() + .map_or(false, |err| err.is_instance_of::(py))); + + assert!(reference.call0().err().map_or(false, |err| { + let result = err.is_instance_of::(py); + #[cfg(not(Py_LIMITED_API))] + let result = result + & (err.value_bound(py).to_string() + == format!("{} object is not callable", CLASS_NAME)); + result + })); + + drop(object); + + assert!(reference.get_object().is_none()); + assert!(reference + .getattr("__class__") + .err() + .map_or(false, |err| err.is_instance_of::(py))); + #[cfg(not(Py_LIMITED_API))] + check_repr(&reference, py.None().bind(py), None)?; + + assert!(reference + .getattr("__callback__") + .err() + .map_or(false, |err| err.is_instance_of::(py))); + + assert!(reference.call0().err().map_or(false, |err| { + let result = err.is_instance_of::(py); + #[cfg(not(Py_LIMITED_API))] + let result = result + & (err.value_bound(py).to_string() + == format!("{} object is not callable", CLASS_NAME)); + result + })); + + Ok(()) + }) + } + + #[test] + fn test_weakref_upgrade_as() -> PyResult<()> { + Python::with_gil(|py| { + let class = get_type(py)?; + let object = class.call0()?; + let reference = PyWeakrefProxy::new_bound(&object)?; + + { + // This test is a bit weird but ok. + let obj = reference.upgrade_as::(); + + assert!(obj.is_ok()); + let obj = obj.unwrap(); + + assert!(obj.is_some()); + assert!(obj.map_or(false, |obj| obj.as_ptr() == object.as_ptr() + && obj.is_exact_instance(&class))); + } + + drop(object); + + { + // This test is a bit weird but ok. + let obj = reference.upgrade_as::(); + + assert!(obj.is_ok()); + let obj = obj.unwrap(); + + assert!(obj.is_none()); + } + + Ok(()) + }) + } + + #[test] + fn test_weakref_upgrade_borrowed_as() -> PyResult<()> { + Python::with_gil(|py| { + let class = get_type(py)?; + let object = class.call0()?; + let reference = PyWeakrefProxy::new_bound(&object)?; + + { + // This test is a bit weird but ok. + let obj = reference.upgrade_borrowed_as::(); + + assert!(obj.is_ok()); + let obj = obj.unwrap(); + + assert!(obj.is_some()); + assert!(obj.map_or(false, |obj| obj.as_ptr() == object.as_ptr() + && obj.is_exact_instance(&class))); + } + + drop(object); + + { + // This test is a bit weird but ok. + let obj = reference.upgrade_borrowed_as::(); + + assert!(obj.is_ok()); + let obj = obj.unwrap(); + + assert!(obj.is_none()); + } + + Ok(()) + }) + } + + #[test] + fn test_weakref_upgrade_as_unchecked() -> PyResult<()> { + Python::with_gil(|py| { + let class = get_type(py)?; + let object = class.call0()?; + let reference = PyWeakrefProxy::new_bound(&object)?; + + { + // This test is a bit weird but ok. + let obj = unsafe { reference.upgrade_as_unchecked::() }; + + assert!(obj.is_some()); + assert!(obj.map_or(false, |obj| obj.as_ptr() == object.as_ptr() + && obj.is_exact_instance(&class))); + } + + drop(object); + + { + // This test is a bit weird but ok. + let obj = unsafe { reference.upgrade_as_unchecked::() }; + + assert!(obj.is_none()); + } + + Ok(()) + }) + } + + #[test] + fn test_weakref_upgrade_borrowed_as_unchecked() -> PyResult<()> { + Python::with_gil(|py| { + let class = get_type(py)?; + let object = class.call0()?; + let reference = PyWeakrefProxy::new_bound(&object)?; + + { + // This test is a bit weird but ok. + let obj = unsafe { reference.upgrade_borrowed_as_unchecked::() }; + + assert!(obj.is_some()); + assert!(obj.map_or(false, |obj| obj.as_ptr() == object.as_ptr() + && obj.is_exact_instance(&class))); + } + + drop(object); + + { + // This test is a bit weird but ok. + let obj = unsafe { reference.upgrade_borrowed_as_unchecked::() }; + + assert!(obj.is_none()); + } + + Ok(()) + }) + } + + #[test] + fn test_weakref_upgrade() -> PyResult<()> { + Python::with_gil(|py| { + let class = get_type(py)?; + let object = class.call0()?; + let reference = PyWeakrefProxy::new_bound(&object)?; + + assert!(reference.upgrade().is_some()); + assert!(reference.upgrade().map_or(false, |obj| obj.is(&object))); + + drop(object); + + assert!(reference.upgrade().is_none()); + + Ok(()) + }) + } + + #[test] + fn test_weakref_upgrade_borrowed() -> PyResult<()> { + Python::with_gil(|py| { + let class = get_type(py)?; + let object = class.call0()?; + let reference = PyWeakrefProxy::new_bound(&object)?; + + assert!(reference.upgrade_borrowed().is_some()); + assert!(reference + .upgrade_borrowed() + .map_or(false, |obj| obj.is(&object))); + + drop(object); + + assert!(reference.upgrade_borrowed().is_none()); + + Ok(()) + }) + } + + #[test] + fn test_weakref_get_object() -> PyResult<()> { + Python::with_gil(|py| { + let class = get_type(py)?; + let object = class.call0()?; + let reference = PyWeakrefProxy::new_bound(&object)?; + + assert!(reference.get_object().is(&object)); + + drop(object); + + assert!(reference.get_object().is_none()); + + Ok(()) + }) + } + + #[test] + fn test_weakref_get_object_borrowed() -> PyResult<()> { + Python::with_gil(|py| { + let class = get_type(py)?; + let object = class.call0()?; + let reference = PyWeakrefProxy::new_bound(&object)?; + + assert!(reference.get_object_borrowed().is(&object)); + + drop(object); + + assert!(reference.get_object_borrowed().is_none()); + + Ok(()) + }) + } + } + + // under 'abi3-py37' and 'abi3-py38' PyClass cannot be weakreferencable. + #[cfg(all(feature = "macros", not(all(Py_LIMITED_API, not(Py_3_9)))))] + mod pyo3_pyclass { + use super::*; + use crate::{pyclass, Py}; + + #[pyclass(weakref, crate = "crate")] + struct WeakrefablePyClass {} + + #[test] + fn test_weakref_proxy_behavior() -> PyResult<()> { + Python::with_gil(|py| { + let object: Bound<'_, WeakrefablePyClass> = + Bound::new(py, WeakrefablePyClass {})?; + let reference = PyWeakrefProxy::new_bound(&object)?; + + assert!(!reference.is(&object)); + assert!(reference.get_object().is(&object)); + #[cfg(not(Py_LIMITED_API))] + assert_eq!( + reference.get_type().to_string(), + format!("", CLASS_NAME) + ); + + assert_eq!( + reference.getattr("__class__")?.to_string(), + "" + ); + #[cfg(not(Py_LIMITED_API))] + check_repr(&reference, object.as_any(), Some("WeakrefablePyClass"))?; + + assert!(reference + .getattr("__callback__") + .err() + .map_or(false, |err| err.is_instance_of::(py))); + + assert!(reference.call0().err().map_or(false, |err| { + let result = err.is_instance_of::(py); + #[cfg(not(Py_LIMITED_API))] + let result = result + & (err.value_bound(py).to_string() + == format!("{} object is not callable", CLASS_NAME)); + result + })); + + drop(object); + + assert!(reference.get_object().is_none()); + assert!(reference + .getattr("__class__") + .err() + .map_or(false, |err| err.is_instance_of::(py))); + #[cfg(not(Py_LIMITED_API))] + check_repr(&reference, py.None().bind(py), None)?; + + assert!(reference + .getattr("__callback__") + .err() + .map_or(false, |err| err.is_instance_of::(py))); + + assert!(reference.call0().err().map_or(false, |err| { + let result = err.is_instance_of::(py); + #[cfg(not(Py_LIMITED_API))] + let result = result + & (err.value_bound(py).to_string() + == format!("{} object is not callable", CLASS_NAME)); + result + })); + + Ok(()) + }) + } + + #[test] + fn test_weakref_upgrade_as() -> PyResult<()> { + Python::with_gil(|py| { + let object = Py::new(py, WeakrefablePyClass {})?; + let reference = PyWeakrefProxy::new_bound(object.bind(py))?; + + { + let obj = reference.upgrade_as::(); + + assert!(obj.is_ok()); + let obj = obj.unwrap(); + + assert!(obj.is_some()); + assert!(obj.map_or(false, |obj| obj.as_ptr() == object.as_ptr())); + } + + drop(object); + + { + let obj = reference.upgrade_as::(); + + assert!(obj.is_ok()); + let obj = obj.unwrap(); + + assert!(obj.is_none()); + } + + Ok(()) + }) + } + + #[test] + fn test_weakref_upgrade_borrowed_as() -> PyResult<()> { + Python::with_gil(|py| { + let object = Py::new(py, WeakrefablePyClass {})?; + let reference = PyWeakrefProxy::new_bound(object.bind(py))?; + + { + let obj = reference.upgrade_borrowed_as::(); + + assert!(obj.is_ok()); + let obj = obj.unwrap(); + + assert!(obj.is_some()); + assert!(obj.map_or(false, |obj| obj.as_ptr() == object.as_ptr())); + } + + drop(object); + + { + let obj = reference.upgrade_borrowed_as::(); + + assert!(obj.is_ok()); + let obj = obj.unwrap(); + + assert!(obj.is_none()); + } + + Ok(()) + }) + } + + #[test] + fn test_weakref_upgrade_as_unchecked() -> PyResult<()> { + Python::with_gil(|py| { + let object = Py::new(py, WeakrefablePyClass {})?; + let reference = PyWeakrefProxy::new_bound(object.bind(py))?; + + { + let obj = unsafe { reference.upgrade_as_unchecked::() }; + + assert!(obj.is_some()); + assert!(obj.map_or(false, |obj| obj.as_ptr() == object.as_ptr())); + } + + drop(object); + + { + let obj = unsafe { reference.upgrade_as_unchecked::() }; + + assert!(obj.is_none()); + } + + Ok(()) + }) + } + + #[test] + fn test_weakref_upgrade_borrowed_as_unchecked() -> PyResult<()> { + Python::with_gil(|py| { + let object = Py::new(py, WeakrefablePyClass {})?; + let reference = PyWeakrefProxy::new_bound(object.bind(py))?; + + { + let obj = unsafe { + reference.upgrade_borrowed_as_unchecked::() + }; + + assert!(obj.is_some()); + assert!(obj.map_or(false, |obj| obj.as_ptr() == object.as_ptr())); + } + + drop(object); + + { + let obj = unsafe { + reference.upgrade_borrowed_as_unchecked::() + }; + + assert!(obj.is_none()); + } + + Ok(()) + }) + } + + #[test] + fn test_weakref_upgrade() -> PyResult<()> { + Python::with_gil(|py| { + let object = Py::new(py, WeakrefablePyClass {})?; + let reference = PyWeakrefProxy::new_bound(object.bind(py))?; + + assert!(reference.upgrade().is_some()); + assert!(reference.upgrade().map_or(false, |obj| obj.is(&object))); + + drop(object); + + assert!(reference.upgrade().is_none()); + + Ok(()) + }) + } + + #[test] + fn test_weakref_upgrade_borrowed() -> PyResult<()> { + Python::with_gil(|py| { + let object = Py::new(py, WeakrefablePyClass {})?; + let reference = PyWeakrefProxy::new_bound(object.bind(py))?; + + assert!(reference.upgrade_borrowed().is_some()); + assert!(reference + .upgrade_borrowed() + .map_or(false, |obj| obj.is(&object))); + + drop(object); + + assert!(reference.upgrade_borrowed().is_none()); + + Ok(()) + }) + } + + #[test] + fn test_weakref_get_object() -> PyResult<()> { + Python::with_gil(|py| { + let object = Py::new(py, WeakrefablePyClass {})?; + let reference = PyWeakrefProxy::new_bound(object.bind(py))?; + + assert!(reference.get_object().is(&object)); + + drop(object); + + assert!(reference.get_object().is_none()); + + Ok(()) + }) + } + + #[test] + fn test_weakref_get_object_borrowed() -> PyResult<()> { + Python::with_gil(|py| { + let object = Py::new(py, WeakrefablePyClass {})?; + let reference = PyWeakrefProxy::new_bound(object.bind(py))?; + + assert!(reference.get_object_borrowed().is(&object)); + + drop(object); + + assert!(reference.get_object_borrowed().is_none()); + + Ok(()) + }) + } + } + } + + mod callable_proxy { + use super::*; + + #[cfg(all(not(Py_LIMITED_API), Py_3_10))] + const CLASS_NAME: &str = ""; + #[cfg(all(not(Py_LIMITED_API), not(Py_3_10)))] + const CLASS_NAME: &str = ""; + + mod python_class { + use super::*; + use crate::{py_result_ext::PyResultExt, types::PyType}; + + fn get_type(py: Python<'_>) -> PyResult> { + py.run_bound( + "class A:\n def __call__(self):\n return 'This class is callable!'\n", + None, + None, + )?; + py.eval_bound("A", None, None).downcast_into::() + } + + #[test] + fn test_weakref_proxy_behavior() -> PyResult<()> { + Python::with_gil(|py| { + let class = get_type(py)?; + let object = class.call0()?; + let reference = PyWeakrefProxy::new_bound(&object)?; + + assert!(!reference.is(&object)); + assert!(reference.get_object().is(&object)); + #[cfg(not(Py_LIMITED_API))] + assert_eq!(reference.get_type().to_string(), CLASS_NAME); + + assert_eq!( + reference.getattr("__class__")?.to_string(), + "" + ); + #[cfg(not(Py_LIMITED_API))] + check_repr(&reference, &object, Some("A"))?; + + assert!(reference + .getattr("__callback__") + .err() + .map_or(false, |err| err.is_instance_of::(py))); + + assert_eq!(reference.call0()?.to_string(), "This class is callable!"); + + drop(object); + + assert!(reference.get_object().is_none()); + assert!(reference + .getattr("__class__") + .err() + .map_or(false, |err| err.is_instance_of::(py))); + #[cfg(not(Py_LIMITED_API))] + check_repr(&reference, py.None().bind(py), None)?; + + assert!(reference + .getattr("__callback__") + .err() + .map_or(false, |err| err.is_instance_of::(py))); + + assert!(reference + .call0() + .err() + .map_or(false, |err| err.is_instance_of::(py) + & (err.value_bound(py).to_string() + == "weakly-referenced object no longer exists"))); + + Ok(()) + }) + } + + #[test] + fn test_weakref_upgrade_as() -> PyResult<()> { + Python::with_gil(|py| { + let class = get_type(py)?; + let object = class.call0()?; + let reference = PyWeakrefProxy::new_bound(&object)?; + + { + // This test is a bit weird but ok. + let obj = reference.upgrade_as::(); + + assert!(obj.is_ok()); + let obj = obj.unwrap(); + + assert!(obj.is_some()); + assert!(obj.map_or(false, |obj| obj.as_ptr() == object.as_ptr() + && obj.is_exact_instance(&class))); + } + + drop(object); + + { + // This test is a bit weird but ok. + let obj = reference.upgrade_as::(); + + assert!(obj.is_ok()); + let obj = obj.unwrap(); + + assert!(obj.is_none()); + } + + Ok(()) + }) + } + + #[test] + fn test_weakref_upgrade_borrowed_as() -> PyResult<()> { + Python::with_gil(|py| { + let class = get_type(py)?; + let object = class.call0()?; + let reference = PyWeakrefProxy::new_bound(&object)?; + + { + // This test is a bit weird but ok. + let obj = reference.upgrade_borrowed_as::(); + + assert!(obj.is_ok()); + let obj = obj.unwrap(); + + assert!(obj.is_some()); + assert!(obj.map_or(false, |obj| obj.as_ptr() == object.as_ptr() + && obj.is_exact_instance(&class))); + } + + drop(object); + + { + // This test is a bit weird but ok. + let obj = reference.upgrade_borrowed_as::(); + + assert!(obj.is_ok()); + let obj = obj.unwrap(); + + assert!(obj.is_none()); + } + + Ok(()) + }) + } + + #[test] + fn test_weakref_upgrade_as_unchecked() -> PyResult<()> { + Python::with_gil(|py| { + let class = get_type(py)?; + let object = class.call0()?; + let reference = PyWeakrefProxy::new_bound(&object)?; + + { + // This test is a bit weird but ok. + let obj = unsafe { reference.upgrade_as_unchecked::() }; + + assert!(obj.is_some()); + assert!(obj.map_or(false, |obj| obj.as_ptr() == object.as_ptr() + && obj.is_exact_instance(&class))); + } + + drop(object); + + { + // This test is a bit weird but ok. + let obj = unsafe { reference.upgrade_as_unchecked::() }; + + assert!(obj.is_none()); + } + + Ok(()) + }) + } + + #[test] + fn test_weakref_upgrade_borrowed_as_unchecked() -> PyResult<()> { + Python::with_gil(|py| { + let class = get_type(py)?; + let object = class.call0()?; + let reference = PyWeakrefProxy::new_bound(&object)?; + + { + // This test is a bit weird but ok. + let obj = unsafe { reference.upgrade_borrowed_as_unchecked::() }; + + assert!(obj.is_some()); + assert!(obj.map_or(false, |obj| obj.as_ptr() == object.as_ptr() + && obj.is_exact_instance(&class))); + } + + drop(object); + + { + // This test is a bit weird but ok. + let obj = unsafe { reference.upgrade_borrowed_as_unchecked::() }; + + assert!(obj.is_none()); + } + + Ok(()) + }) + } + + #[test] + fn test_weakref_upgrade() -> PyResult<()> { + Python::with_gil(|py| { + let class = get_type(py)?; + let object = class.call0()?; + let reference = PyWeakrefProxy::new_bound(&object)?; + + assert!(reference.upgrade().is_some()); + assert!(reference.upgrade().map_or(false, |obj| obj.is(&object))); + + drop(object); + + assert!(reference.upgrade().is_none()); + + Ok(()) + }) + } + + #[test] + fn test_weakref_upgrade_borrowed() -> PyResult<()> { + Python::with_gil(|py| { + let class = get_type(py)?; + let object = class.call0()?; + let reference = PyWeakrefProxy::new_bound(&object)?; + + assert!(reference.upgrade_borrowed().is_some()); + assert!(reference + .upgrade_borrowed() + .map_or(false, |obj| obj.is(&object))); + + drop(object); + + assert!(reference.upgrade_borrowed().is_none()); + + Ok(()) + }) + } + + #[test] + fn test_weakref_get_object() -> PyResult<()> { + Python::with_gil(|py| { + let class = get_type(py)?; + let object = class.call0()?; + let reference = PyWeakrefProxy::new_bound(&object)?; + + assert!(reference.get_object().is(&object)); + + drop(object); + + assert!(reference.get_object().is_none()); + + Ok(()) + }) + } + + #[test] + fn test_weakref_get_object_borrowed() -> PyResult<()> { + Python::with_gil(|py| { + let class = get_type(py)?; + let object = class.call0()?; + let reference = PyWeakrefProxy::new_bound(&object)?; + + assert!(reference.get_object_borrowed().is(&object)); + + drop(object); + + assert!(reference.get_object_borrowed().is_none()); + + Ok(()) + }) + } + } + + // under 'abi3-py37' and 'abi3-py38' PyClass cannot be weakreferencable. + #[cfg(all(feature = "macros", not(all(Py_LIMITED_API, not(Py_3_9)))))] + mod pyo3_pyclass { + use super::*; + use crate::{pyclass, pymethods, Py}; + + #[pyclass(weakref, crate = "crate")] + struct WeakrefablePyClass {} + + #[pymethods(crate = "crate")] + impl WeakrefablePyClass { + fn __call__(&self) -> &str { + "This class is callable!" + } + } + + #[test] + fn test_weakref_proxy_behavior() -> PyResult<()> { + Python::with_gil(|py| { + let object: Bound<'_, WeakrefablePyClass> = + Bound::new(py, WeakrefablePyClass {})?; + let reference = PyWeakrefProxy::new_bound(&object)?; + + assert!(!reference.is(&object)); + assert!(reference.get_object().is(&object)); + #[cfg(not(Py_LIMITED_API))] + assert_eq!(reference.get_type().to_string(), CLASS_NAME); + + assert_eq!( + reference.getattr("__class__")?.to_string(), + "" + ); + #[cfg(not(Py_LIMITED_API))] + check_repr(&reference, object.as_any(), Some("WeakrefablePyClass"))?; + + assert!(reference + .getattr("__callback__") + .err() + .map_or(false, |err| err.is_instance_of::(py))); + + assert_eq!(reference.call0()?.to_string(), "This class is callable!"); + + drop(object); + + assert!(reference.get_object().is_none()); + assert!(reference + .getattr("__class__") + .err() + .map_or(false, |err| err.is_instance_of::(py))); + #[cfg(not(Py_LIMITED_API))] + check_repr(&reference, py.None().bind(py), None)?; + + assert!(reference + .getattr("__callback__") + .err() + .map_or(false, |err| err.is_instance_of::(py))); + + assert!(reference + .call0() + .err() + .map_or(false, |err| err.is_instance_of::(py) + & (err.value_bound(py).to_string() + == "weakly-referenced object no longer exists"))); + + Ok(()) + }) + } + + #[test] + fn test_weakref_upgrade_as() -> PyResult<()> { + Python::with_gil(|py| { + let object = Py::new(py, WeakrefablePyClass {})?; + let reference = PyWeakrefProxy::new_bound(object.bind(py))?; + + { + let obj = reference.upgrade_as::(); + + assert!(obj.is_ok()); + let obj = obj.unwrap(); + + assert!(obj.is_some()); + assert!(obj.map_or(false, |obj| obj.as_ptr() == object.as_ptr())); + } + + drop(object); + + { + let obj = reference.upgrade_as::(); + + assert!(obj.is_ok()); + let obj = obj.unwrap(); + + assert!(obj.is_none()); + } + + Ok(()) + }) + } + #[test] + fn test_weakref_upgrade_borrowed_as() -> PyResult<()> { + Python::with_gil(|py| { + let object = Py::new(py, WeakrefablePyClass {})?; + let reference = PyWeakrefProxy::new_bound(object.bind(py))?; + + { + let obj = reference.upgrade_borrowed_as::(); + + assert!(obj.is_ok()); + let obj = obj.unwrap(); + + assert!(obj.is_some()); + assert!(obj.map_or(false, |obj| obj.as_ptr() == object.as_ptr())); + } + + drop(object); + + { + let obj = reference.upgrade_borrowed_as::(); + + assert!(obj.is_ok()); + let obj = obj.unwrap(); + + assert!(obj.is_none()); + } + + Ok(()) + }) + } + + #[test] + fn test_weakref_upgrade_as_unchecked() -> PyResult<()> { + Python::with_gil(|py| { + let object = Py::new(py, WeakrefablePyClass {})?; + let reference = PyWeakrefProxy::new_bound(object.bind(py))?; + + { + let obj = unsafe { reference.upgrade_as_unchecked::() }; + + assert!(obj.is_some()); + assert!(obj.map_or(false, |obj| obj.as_ptr() == object.as_ptr())); + } + + drop(object); + + { + let obj = unsafe { reference.upgrade_as_unchecked::() }; + + assert!(obj.is_none()); + } + + Ok(()) + }) + } + #[test] + fn test_weakref_upgrade_borrowed_as_unchecked() -> PyResult<()> { + Python::with_gil(|py| { + let object = Py::new(py, WeakrefablePyClass {})?; + let reference = PyWeakrefProxy::new_bound(object.bind(py))?; + + { + let obj = unsafe { + reference.upgrade_borrowed_as_unchecked::() + }; + + assert!(obj.is_some()); + assert!(obj.map_or(false, |obj| obj.as_ptr() == object.as_ptr())); + } + + drop(object); + + { + let obj = unsafe { + reference.upgrade_borrowed_as_unchecked::() + }; + + assert!(obj.is_none()); + } + + Ok(()) + }) + } + + #[test] + fn test_weakref_upgrade() -> PyResult<()> { + Python::with_gil(|py| { + let object = Py::new(py, WeakrefablePyClass {})?; + let reference = PyWeakrefProxy::new_bound(object.bind(py))?; + + assert!(reference.upgrade().is_some()); + assert!(reference.upgrade().map_or(false, |obj| obj.is(&object))); + + drop(object); + + assert!(reference.upgrade().is_none()); + + Ok(()) + }) + } + + #[test] + fn test_weakref_upgrade_borrowed() -> PyResult<()> { + Python::with_gil(|py| { + let object = Py::new(py, WeakrefablePyClass {})?; + let reference = PyWeakrefProxy::new_bound(object.bind(py))?; + + assert!(reference.upgrade_borrowed().is_some()); + assert!(reference + .upgrade_borrowed() + .map_or(false, |obj| obj.is(&object))); + + drop(object); + + assert!(reference.upgrade_borrowed().is_none()); + + Ok(()) + }) + } + + #[test] + fn test_weakref_get_object() -> PyResult<()> { + Python::with_gil(|py| { + let object = Py::new(py, WeakrefablePyClass {})?; + let reference = PyWeakrefProxy::new_bound(object.bind(py))?; + + assert!(reference.get_object().is(&object)); + + drop(object); + + assert!(reference.get_object().is_none()); + + Ok(()) + }) + } + + #[test] + fn test_weakref_get_object_borrowed() -> PyResult<()> { + Python::with_gil(|py| { + let object = Py::new(py, WeakrefablePyClass {})?; + let reference = PyWeakrefProxy::new_bound(object.bind(py))?; + + assert!(reference.get_object_borrowed().is(&object)); + + drop(object); + + assert!(reference.get_object_borrowed().is_none()); + + Ok(()) + }) + } + } + } +} diff --git a/src/types/weakref/reference.rs b/src/types/weakref/reference.rs new file mode 100644 index 00000000000..6cdcde3a7f7 --- /dev/null +++ b/src/types/weakref/reference.rs @@ -0,0 +1,1119 @@ +use crate::err::PyResult; +use crate::ffi_ptr_ext::FfiPtrExt; +use crate::py_result_ext::PyResultExt; +use crate::types::any::PyAny; +use crate::{ffi, Borrowed, Bound, ToPyObject}; + +#[cfg(any(any(PyPy, GraalPy, Py_LIMITED_API), feature = "gil-refs"))] +use crate::type_object::PyTypeCheck; +#[cfg(feature = "gil-refs")] +use crate::{type_object::PyTypeInfo, PyNativeType}; + +use super::PyWeakrefMethods; + +/// Represents a Python `weakref.ReferenceType`. +/// +/// In Python this is created by calling `weakref.ref`. +#[repr(transparent)] +pub struct PyWeakrefReference(PyAny); + +#[cfg(not(any(PyPy, GraalPy, Py_LIMITED_API)))] +pyobject_native_type!( + PyWeakrefReference, + ffi::PyWeakReference, + pyobject_native_static_type_object!(ffi::_PyWeakref_RefType), + #module=Some("weakref"), + #checkfunction=ffi::PyWeakref_CheckRefExact +); + +// When targetting alternative or multiple interpreters, it is better to not use the internal API. +#[cfg(any(PyPy, GraalPy, Py_LIMITED_API))] +pyobject_native_type_named!(PyWeakrefReference); +#[cfg(any(PyPy, GraalPy, Py_LIMITED_API))] +pyobject_native_type_extract!(PyWeakrefReference); + +#[cfg(any(PyPy, GraalPy, Py_LIMITED_API))] +impl PyTypeCheck for PyWeakrefReference { + const NAME: &'static str = "weakref.ReferenceType"; + + fn type_check(object: &Bound<'_, PyAny>) -> bool { + unsafe { ffi::PyWeakref_CheckRef(object.as_ptr()) > 0 } + } +} + +impl PyWeakrefReference { + /// Constructs a new Weak Reference (`weakref.ref`/`weakref.ReferenceType`) for the given object. + /// + /// Returns a `TypeError` if `object` is not weak referenceable (Most native types and PyClasses without `weakref` flag). + /// + /// # Examples + #[cfg_attr( + not(all(feature = "macros", not(all(Py_LIMITED_API, not(Py_3_9))))), + doc = "```rust,ignore" + )] + #[cfg_attr( + all(feature = "macros", not(all(Py_LIMITED_API, not(Py_3_9)))), + doc = "```rust" + )] + /// use pyo3::prelude::*; + /// use pyo3::types::PyWeakrefReference; + /// + /// #[pyclass(weakref)] + /// struct Foo { /* fields omitted */ } + /// + /// # fn main() -> PyResult<()> { + /// Python::with_gil(|py| { + /// let foo = Bound::new(py, Foo {})?; + /// let weakref = PyWeakrefReference::new_bound(&foo)?; + /// assert!( + /// // In normal situations where a direct `Bound<'py, Foo>` is required use `upgrade::` + /// weakref.upgrade() + /// .map_or(false, |obj| obj.is(&foo)) + /// ); + /// + /// let weakref2 = PyWeakrefReference::new_bound(&foo)?; + /// assert!(weakref.is(&weakref2)); + /// + /// drop(foo); + /// + /// assert!(weakref.upgrade().is_none()); + /// Ok(()) + /// }) + /// # } + /// ``` + pub fn new_bound<'py>(object: &Bound<'py, PyAny>) -> PyResult> { + // TODO: Is this inner pattern still necessary Here? + fn inner<'py>(object: &Bound<'py, PyAny>) -> PyResult> { + unsafe { + Bound::from_owned_ptr_or_err( + object.py(), + ffi::PyWeakref_NewRef(object.as_ptr(), ffi::Py_None()), + ) + .downcast_into_unchecked() + } + } + + inner(object) + } + + /// Constructs a new Weak Reference (`weakref.ref`/`weakref.ReferenceType`) for the given object with a callback. + /// + /// Returns a `TypeError` if `object` is not weak referenceable (Most native types and PyClasses without `weakref` flag) or if the `callback` is not callable or None. + /// + /// # Examples + #[cfg_attr( + not(all(feature = "macros", not(all(Py_LIMITED_API, not(Py_3_9))))), + doc = "```rust,ignore" + )] + #[cfg_attr( + all(feature = "macros", not(all(Py_LIMITED_API, not(Py_3_9)))), + doc = "```rust" + )] + /// use pyo3::prelude::*; + /// use pyo3::types::PyWeakrefReference; + /// + /// #[pyclass(weakref)] + /// struct Foo { /* fields omitted */ } + /// + /// #[pyfunction] + /// fn callback(wref: Bound<'_, PyWeakrefReference>) -> PyResult<()> { + /// let py = wref.py(); + /// assert!(wref.upgrade_as::()?.is_none()); + /// py.run_bound("counter = 1", None, None) + /// } + /// + /// # fn main() -> PyResult<()> { + /// Python::with_gil(|py| { + /// py.run_bound("counter = 0", None, None)?; + /// assert_eq!(py.eval_bound("counter", None, None)?.extract::()?, 0); + /// let foo = Bound::new(py, Foo{})?; + /// + /// // This is fine. + /// let weakref = PyWeakrefReference::new_bound_with(&foo, py.None())?; + /// assert!(weakref.upgrade_as::()?.is_some()); + /// assert!( + /// // In normal situations where a direct `Bound<'py, Foo>` is required use `upgrade::` + /// weakref.upgrade() + /// .map_or(false, |obj| obj.is(&foo)) + /// ); + /// assert_eq!(py.eval_bound("counter", None, None)?.extract::()?, 0); + /// + /// let weakref2 = PyWeakrefReference::new_bound_with(&foo, wrap_pyfunction_bound!(callback, py)?)?; + /// assert!(!weakref.is(&weakref2)); // Not the same weakref + /// assert!(weakref.eq(&weakref2)?); // But Equal, since they point to the same object + /// + /// drop(foo); + /// + /// assert!(weakref.upgrade_as::()?.is_none()); + /// assert_eq!(py.eval_bound("counter", None, None)?.extract::()?, 1); + /// Ok(()) + /// }) + /// # } + /// ``` + pub fn new_bound_with<'py, C>( + object: &Bound<'py, PyAny>, + callback: C, + ) -> PyResult> + where + C: ToPyObject, + { + fn inner<'py>( + object: &Bound<'py, PyAny>, + callback: Bound<'py, PyAny>, + ) -> PyResult> { + unsafe { + Bound::from_owned_ptr_or_err( + object.py(), + ffi::PyWeakref_NewRef(object.as_ptr(), callback.as_ptr()), + ) + .downcast_into_unchecked() + } + } + + let py = object.py(); + inner(object, callback.to_object(py).into_bound(py)) + } +} + +#[cfg(feature = "gil-refs")] +impl PyWeakrefReference { + /// Deprecated form of [`PyWeakrefReference::new_bound`]. + #[inline] + #[deprecated( + since = "0.21.0", + note = "`PyWeakrefReference::new` will be replaced by `PyWeakrefReference::new_bound` in a future PyO3 version" + )] + pub fn new(object: &T) -> PyResult<&PyWeakrefReference> + where + T: PyNativeType, + { + Self::new_bound(object.as_borrowed().as_any()).map(Bound::into_gil_ref) + } + + /// Deprecated form of [`PyWeakrefReference::new_bound_with`]. + #[inline] + #[deprecated( + since = "0.21.0", + note = "`PyWeakrefReference::new_with` will be replaced by `PyWeakrefReference::new_bound_with` in a future PyO3 version" + )] + pub fn new_with(object: &T, callback: C) -> PyResult<&PyWeakrefReference> + where + T: PyNativeType, + C: ToPyObject, + { + Self::new_bound_with(object.as_borrowed().as_any(), callback).map(Bound::into_gil_ref) + } + + /// Upgrade the weakref to a direct object reference. + /// + /// It is named `upgrade` to be inline with [rust's `Weak::upgrade`](std::rc::Weak::upgrade). + /// In Python it would be equivalent to [`PyWeakref_GetObject`] or calling the [`weakref.ReferenceType`] (result of calling [`weakref.ref`]). + /// + /// # Example + #[cfg_attr( + not(all(feature = "macros", not(all(Py_LIMITED_API, not(Py_3_9))))), + doc = "```rust,ignore" + )] + #[cfg_attr( + all(feature = "macros", not(all(Py_LIMITED_API, not(Py_3_9)))), + doc = "```rust" + )] + /// use pyo3::prelude::*; + /// use pyo3::types::PyWeakrefReference; + /// + /// #[pyclass(weakref)] + /// struct Foo { /* fields omitted */ } + /// + /// #[pymethods] + /// impl Foo { + /// fn get_data(&self) -> (&str, u32) { + /// ("Dave", 10) + /// } + /// } + /// + /// fn parse_data(reference: Borrowed<'_, '_, PyWeakrefReference>) -> PyResult { + /// if let Some(data_src) = reference.upgrade_as::()? { + /// let data = data_src.borrow(); + /// let (name, score) = data.get_data(); + /// Ok(format!("Processing '{}': score = {}", name, score)) + /// } else { + /// Ok("The supplied data reference is nolonger relavent.".to_owned()) + /// } + /// } + /// + /// # fn main() -> PyResult<()> { + /// Python::with_gil(|py| { + /// let data = Bound::new(py, Foo{})?; + /// let reference = PyWeakrefReference::new_bound(&data)?; + /// + /// assert_eq!( + /// parse_data(reference.as_borrowed())?, + /// "Processing 'Dave': score = 10" + /// ); + /// + /// drop(data); + /// + /// assert_eq!( + /// parse_data(reference.as_borrowed())?, + /// "The supplied data reference is nolonger relavent." + /// ); + /// + /// Ok(()) + /// }) + /// # } + /// ``` + /// + /// # Panics + /// This function panics is the current object is invalid. + /// If used propperly this is never the case. (NonNull and actually a weakref type) + /// + /// [`PyWeakref_GetObject`]: https://docs.python.org/3/c-api/weakref.html#c.PyWeakref_GetObject + /// [`weakref.ReferenceType`]: https://docs.python.org/3/library/weakref.html#weakref.ReferenceType + /// [`weakref.ref`]: https://docs.python.org/3/library/weakref.html#weakref.ref + pub fn upgrade_as(&self) -> PyResult> + where + T: PyTypeCheck, + { + Ok(self + .as_borrowed() + .upgrade_as::()? + .map(Bound::into_gil_ref)) + } + + /// Upgrade the weakref to a direct object reference unchecked. The type of the recovered object is not checked before downcasting, this could lead to unexpected behavior. Use only when absolutely certain the type can be guaranteed. The `weakref` may still return `None`. + /// + /// It is named `upgrade` to be inline with [rust's `Weak::upgrade`](std::rc::Weak::upgrade). + /// In Python it would be equivalent to [`PyWeakref_GetObject`] or calling the [`weakref.ReferenceType`] (result of calling [`weakref.ref`]). + /// + /// # Safety + /// Callers must ensure that the type is valid or risk type confusion. + /// The `weakref` is still allowed to be `None`, if the referenced object has been cleaned up. + /// + /// # Example + #[cfg_attr( + not(all(feature = "macros", not(all(Py_LIMITED_API, not(Py_3_9))))), + doc = "```rust,ignore" + )] + #[cfg_attr( + all(feature = "macros", not(all(Py_LIMITED_API, not(Py_3_9)))), + doc = "```rust" + )] + /// use pyo3::prelude::*; + /// use pyo3::types::PyWeakrefReference; + /// + /// #[pyclass(weakref)] + /// struct Foo { /* fields omitted */ } + /// + /// #[pymethods] + /// impl Foo { + /// fn get_data(&self) -> (&str, u32) { + /// ("Dave", 10) + /// } + /// } + /// + /// fn parse_data(reference: Borrowed<'_, '_, PyWeakrefReference>) -> String { + /// if let Some(data_src) = unsafe { reference.upgrade_as_unchecked::() } { + /// let data = data_src.borrow(); + /// let (name, score) = data.get_data(); + /// format!("Processing '{}': score = {}", name, score) + /// } else { + /// "The supplied data reference is nolonger relavent.".to_owned() + /// } + /// } + /// + /// # fn main() -> PyResult<()> { + /// Python::with_gil(|py| { + /// let data = Bound::new(py, Foo{})?; + /// let reference = PyWeakrefReference::new_bound(&data)?; + /// + /// assert_eq!( + /// parse_data(reference.as_borrowed()), + /// "Processing 'Dave': score = 10" + /// ); + /// + /// drop(data); + /// + /// assert_eq!( + /// parse_data(reference.as_borrowed()), + /// "The supplied data reference is nolonger relavent." + /// ); + /// + /// Ok(()) + /// }) + /// # } + /// ``` + /// + /// # Panics + /// This function panics is the current object is invalid. + /// If used propperly this is never the case. (NonNull and actually a weakref type) + /// + /// [`PyWeakref_GetObject`]: https://docs.python.org/3/c-api/weakref.html#c.PyWeakref_GetObject + /// [`weakref.ReferenceType`]: https://docs.python.org/3/library/weakref.html#weakref.ReferenceType + /// [`weakref.ref`]: https://docs.python.org/3/library/weakref.html#weakref.ref + pub unsafe fn upgrade_as_unchecked(&self) -> Option<&T::AsRefTarget> + where + T: PyTypeCheck, + { + self.as_borrowed() + .upgrade_as_unchecked::() + .map(Bound::into_gil_ref) + } + + /// Upgrade the weakref to an exact direct object reference. + /// + /// It is named `upgrade` to be inline with [rust's `Weak::upgrade`](std::rc::Weak::upgrade). + /// In Python it would be equivalent to [`PyWeakref_GetObject`] or calling the [`weakref.ReferenceType`] (result of calling [`weakref.ref`]). + /// + /// # Example + #[cfg_attr( + not(all(feature = "macros", not(all(Py_LIMITED_API, not(Py_3_9))))), + doc = "```rust,ignore" + )] + #[cfg_attr( + all(feature = "macros", not(all(Py_LIMITED_API, not(Py_3_9)))), + doc = "```rust" + )] + /// use pyo3::prelude::*; + /// use pyo3::types::PyWeakrefReference; + /// + /// #[pyclass(weakref)] + /// struct Foo { /* fields omitted */ } + /// + /// #[pymethods] + /// impl Foo { + /// fn get_data(&self) -> (&str, u32) { + /// ("Dave", 10) + /// } + /// } + /// + /// fn parse_data(reference: Borrowed<'_, '_, PyWeakrefReference>) -> PyResult { + /// if let Some(data_src) = reference.upgrade_as_exact::()? { + /// let data = data_src.borrow(); + /// let (name, score) = data.get_data(); + /// Ok(format!("Processing '{}': score = {}", name, score)) + /// } else { + /// Ok("The supplied data reference is nolonger relavent.".to_owned()) + /// } + /// } + /// + /// # fn main() -> PyResult<()> { + /// Python::with_gil(|py| { + /// let data = Bound::new(py, Foo{})?; + /// let reference = PyWeakrefReference::new_bound(&data)?; + /// + /// assert_eq!( + /// parse_data(reference.as_borrowed())?, + /// "Processing 'Dave': score = 10" + /// ); + /// + /// drop(data); + /// + /// assert_eq!( + /// parse_data(reference.as_borrowed())?, + /// "The supplied data reference is nolonger relavent." + /// ); + /// + /// Ok(()) + /// }) + /// # } + /// ``` + /// + /// # Panics + /// This function panics is the current object is invalid. + /// If used propperly this is never the case. (NonNull and actually a weakref type) + /// + /// [`PyWeakref_GetObject`]: https://docs.python.org/3/c-api/weakref.html#c.PyWeakref_GetObject + /// [`weakref.ReferenceType`]: https://docs.python.org/3/library/weakref.html#weakref.ReferenceType + /// [`weakref.ref`]: https://docs.python.org/3/library/weakref.html#weakref.ref + pub fn upgrade_as_exact(&self) -> PyResult> + where + T: PyTypeInfo, + { + Ok(self + .as_borrowed() + .upgrade_as_exact::()? + .map(Bound::into_gil_ref)) + } + + /// Upgrade the weakref to a [`PyAny`] reference to the target if possible. + /// + /// It is named `upgrade` to be inline with [rust's `Weak::upgrade`](std::rc::Weak::upgrade). + /// This function returns `Some(&'py PyAny)` if the reference still exists, otherwise `None` will be returned. + /// + /// This function gets the optional target of this [`weakref.ReferenceType`] (result of calling [`weakref.ref`]). + /// It produces similair results to calling the `weakref.ReferenceType` or using [`PyWeakref_GetObject`] in the C api. + /// + /// # Example + #[cfg_attr( + not(all(feature = "macros", not(all(Py_LIMITED_API, not(Py_3_9))))), + doc = "```rust,ignore" + )] + #[cfg_attr( + all(feature = "macros", not(all(Py_LIMITED_API, not(Py_3_9)))), + doc = "```rust" + )] + /// use pyo3::prelude::*; + /// use pyo3::types::PyWeakrefReference; + /// + /// #[pyclass(weakref)] + /// struct Foo { /* fields omitted */ } + /// + /// fn parse_data(reference: Borrowed<'_, '_, PyWeakrefReference>) -> PyResult { + /// if let Some(object) = reference.upgrade() { + /// Ok(format!("The object '{}' refered by this reference still exists.", object.getattr("__class__")?.getattr("__qualname__")?)) + /// } else { + /// Ok("The object, which this reference refered to, no longer exists".to_owned()) + /// } + /// } + /// + /// # fn main() -> PyResult<()> { + /// Python::with_gil(|py| { + /// let data = Bound::new(py, Foo{})?; + /// let reference = PyWeakrefReference::new_bound(&data)?; + /// + /// assert_eq!( + /// parse_data(reference.as_borrowed())?, + /// "The object 'Foo' refered by this reference still exists." + /// ); + /// + /// drop(data); + /// + /// assert_eq!( + /// parse_data(reference.as_borrowed())?, + /// "The object, which this reference refered to, no longer exists" + /// ); + /// + /// Ok(()) + /// }) + /// # } + /// ``` + /// + /// # Panics + /// This function panics is the current object is invalid. + /// If used propperly this is never the case. (NonNull and actually a weakref type) + /// + /// [`PyWeakref_GetObject`]: https://docs.python.org/3/c-api/weakref.html#c.PyWeakref_GetObject + /// [`weakref.ReferenceType`]: https://docs.python.org/3/library/weakref.html#weakref.ReferenceType + /// [`weakref.ref`]: https://docs.python.org/3/library/weakref.html#weakref.ref + pub fn upgrade(&self) -> Option<&'_ PyAny> { + self.as_borrowed().upgrade().map(Bound::into_gil_ref) + } + + /// Retrieve to a object pointed to by the weakref. + /// + /// This function returns `&'py PyAny`, which is either the object if it still exists, otherwise it will refer to [`PyNone`](crate::types::PyNone). + /// + /// This function gets the optional target of this [`weakref.ReferenceType`] (result of calling [`weakref.ref`]). + /// It produces similair results to calling the `weakref.ReferenceType` or using [`PyWeakref_GetObject`] in the C api. + /// + /// # Example + #[cfg_attr( + not(all(feature = "macros", not(all(Py_LIMITED_API, not(Py_3_9))))), + doc = "```rust,ignore" + )] + #[cfg_attr( + all(feature = "macros", not(all(Py_LIMITED_API, not(Py_3_9)))), + doc = "```rust" + )] + /// use pyo3::prelude::*; + /// use pyo3::types::PyWeakrefReference; + /// + /// #[pyclass(weakref)] + /// struct Foo { /* fields omitted */ } + /// + /// fn get_class(reference: Borrowed<'_, '_, PyWeakrefReference>) -> PyResult { + /// reference + /// .get_object() + /// .getattr("__class__")? + /// .repr() + /// .map(|repr| repr.to_string()) + /// } + /// + /// # fn main() -> PyResult<()> { + /// Python::with_gil(|py| { + /// let object = Bound::new(py, Foo{})?; + /// let reference = PyWeakrefReference::new_bound(&object)?; + /// + /// assert_eq!( + /// get_class(reference.as_borrowed())?, + /// "" + /// ); + /// + /// drop(object); + /// + /// assert_eq!(get_class(reference.as_borrowed())?, ""); + /// + /// Ok(()) + /// }) + /// # } + /// ``` + /// + /// # Panics + /// This function panics is the current object is invalid. + /// If used propperly this is never the case. (NonNull and actually a weakref type) + /// + /// [`PyWeakref_GetObject`]: https://docs.python.org/3/c-api/weakref.html#c.PyWeakref_GetObject + /// [`weakref.ReferenceType`]: https://docs.python.org/3/library/weakref.html#weakref.ReferenceType + /// [`weakref.ref`]: https://docs.python.org/3/library/weakref.html#weakref.ref + pub fn get_object(&self) -> &'_ PyAny { + self.as_borrowed().get_object().into_gil_ref() + } +} + +impl<'py> PyWeakrefMethods<'py> for Bound<'py, PyWeakrefReference> { + fn get_object_borrowed(&self) -> Borrowed<'_, 'py, PyAny> { + // PyWeakref_GetObject does some error checking, however we ensure the passed object is Non-Null and a Weakref type. + unsafe { ffi::PyWeakref_GetObject(self.as_ptr()).assume_borrowed_or_err(self.py()) } + .expect("The 'weakref.ReferenceType' instance should be valid (non-null and actually a weakref reference)") + } +} + +#[cfg(test)] +mod tests { + use crate::types::any::{PyAny, PyAnyMethods}; + use crate::types::weakref::{PyWeakrefMethods, PyWeakrefReference}; + use crate::{Bound, PyResult, Python}; + + #[cfg(all(not(Py_LIMITED_API), Py_3_10))] + const CLASS_NAME: &str = ""; + #[cfg(all(not(Py_LIMITED_API), not(Py_3_10)))] + const CLASS_NAME: &str = ""; + + fn check_repr( + reference: &Bound<'_, PyWeakrefReference>, + object: Option<(&Bound<'_, PyAny>, &str)>, + ) -> PyResult<()> { + let repr = reference.repr()?.to_string(); + let (first_part, second_part) = repr.split_once("; ").unwrap(); + + { + let (msg, addr) = first_part.split_once("0x").unwrap(); + + assert_eq!(msg, " { + let (msg, addr) = second_part.split_once("0x").unwrap(); + + // Avoid testing on reprs directly since they the quoting and full path vs class name tends to be changedi undocumented. + assert!(msg.starts_with("to '")); + assert!(msg.contains(class)); + assert!(msg.ends_with("' at ")); + + assert!(addr + .to_lowercase() + .contains(format!("{:x?}", object.as_ptr()).split_at(2).1)); + } + None => { + assert_eq!(second_part, "dead>") + } + } + + Ok(()) + } + + mod python_class { + use super::*; + use crate::{py_result_ext::PyResultExt, types::PyType}; + + fn get_type(py: Python<'_>) -> PyResult> { + py.run_bound("class A:\n pass\n", None, None)?; + py.eval_bound("A", None, None).downcast_into::() + } + + #[test] + fn test_weakref_reference_behavior() -> PyResult<()> { + Python::with_gil(|py| { + let class = get_type(py)?; + let object = class.call0()?; + let reference = PyWeakrefReference::new_bound(&object)?; + + assert!(!reference.is(&object)); + assert!(reference.get_object().is(&object)); + + #[cfg(not(Py_LIMITED_API))] + assert_eq!(reference.get_type().to_string(), CLASS_NAME); + + #[cfg(not(Py_LIMITED_API))] + assert_eq!(reference.getattr("__class__")?.to_string(), CLASS_NAME); + + #[cfg(not(Py_LIMITED_API))] + check_repr(&reference, Some((object.as_any(), "A")))?; + + assert!(reference + .getattr("__callback__") + .map_or(false, |result| result.is_none())); + + assert!(reference.call0()?.is(&object)); + + drop(object); + + assert!(reference.get_object().is_none()); + #[cfg(not(Py_LIMITED_API))] + assert_eq!(reference.getattr("__class__")?.to_string(), CLASS_NAME); + check_repr(&reference, None)?; + + assert!(reference + .getattr("__callback__") + .map_or(false, |result| result.is_none())); + + assert!(reference.call0()?.is_none()); + + Ok(()) + }) + } + + #[test] + fn test_weakref_upgrade_as() -> PyResult<()> { + Python::with_gil(|py| { + let class = get_type(py)?; + let object = class.call0()?; + let reference = PyWeakrefReference::new_bound(&object)?; + + { + // This test is a bit weird but ok. + let obj = reference.upgrade_as::(); + + assert!(obj.is_ok()); + let obj = obj.unwrap(); + + assert!(obj.is_some()); + assert!(obj.map_or(false, |obj| obj.as_ptr() == object.as_ptr() + && obj.is_exact_instance(&class))); + } + + drop(object); + + { + // This test is a bit weird but ok. + let obj = reference.upgrade_as::(); + + assert!(obj.is_ok()); + let obj = obj.unwrap(); + + assert!(obj.is_none()); + } + + Ok(()) + }) + } + + #[test] + fn test_weakref_upgrade_borrowed_as() -> PyResult<()> { + Python::with_gil(|py| { + let class = get_type(py)?; + let object = class.call0()?; + let reference = PyWeakrefReference::new_bound(&object)?; + + { + // This test is a bit weird but ok. + let obj = reference.upgrade_borrowed_as::(); + + assert!(obj.is_ok()); + let obj = obj.unwrap(); + + assert!(obj.is_some()); + assert!(obj.map_or(false, |obj| obj.as_ptr() == object.as_ptr() + && obj.is_exact_instance(&class))); + } + + drop(object); + + { + // This test is a bit weird but ok. + let obj = reference.upgrade_borrowed_as::(); + + assert!(obj.is_ok()); + let obj = obj.unwrap(); + + assert!(obj.is_none()); + } + + Ok(()) + }) + } + + #[test] + fn test_weakref_upgrade_as_unchecked() -> PyResult<()> { + Python::with_gil(|py| { + let class = get_type(py)?; + let object = class.call0()?; + let reference = PyWeakrefReference::new_bound(&object)?; + + { + // This test is a bit weird but ok. + let obj = unsafe { reference.upgrade_as_unchecked::() }; + + assert!(obj.is_some()); + assert!(obj.map_or(false, |obj| obj.as_ptr() == object.as_ptr() + && obj.is_exact_instance(&class))); + } + + drop(object); + + { + // This test is a bit weird but ok. + let obj = unsafe { reference.upgrade_as_unchecked::() }; + + assert!(obj.is_none()); + } + + Ok(()) + }) + } + + #[test] + fn test_weakref_upgrade_borrowed_as_unchecked() -> PyResult<()> { + Python::with_gil(|py| { + let class = get_type(py)?; + let object = class.call0()?; + let reference = PyWeakrefReference::new_bound(&object)?; + + { + // This test is a bit weird but ok. + let obj = unsafe { reference.upgrade_borrowed_as_unchecked::() }; + + assert!(obj.is_some()); + assert!(obj.map_or(false, |obj| obj.as_ptr() == object.as_ptr() + && obj.is_exact_instance(&class))); + } + + drop(object); + + { + // This test is a bit weird but ok. + let obj = unsafe { reference.upgrade_borrowed_as_unchecked::() }; + + assert!(obj.is_none()); + } + + Ok(()) + }) + } + + #[test] + fn test_weakref_upgrade() -> PyResult<()> { + Python::with_gil(|py| { + let class = get_type(py)?; + let object = class.call0()?; + let reference = PyWeakrefReference::new_bound(&object)?; + + assert!(reference.call0()?.is(&object)); + assert!(reference.upgrade().is_some()); + assert!(reference.upgrade().map_or(false, |obj| obj.is(&object))); + + drop(object); + + assert!(reference.call0()?.is_none()); + assert!(reference.upgrade().is_none()); + + Ok(()) + }) + } + + #[test] + fn test_weakref_upgrade_borrowed() -> PyResult<()> { + Python::with_gil(|py| { + let class = get_type(py)?; + let object = class.call0()?; + let reference = PyWeakrefReference::new_bound(&object)?; + + assert!(reference.call0()?.is(&object)); + assert!(reference.upgrade_borrowed().is_some()); + assert!(reference + .upgrade_borrowed() + .map_or(false, |obj| obj.is(&object))); + + drop(object); + + assert!(reference.call0()?.is_none()); + assert!(reference.upgrade_borrowed().is_none()); + + Ok(()) + }) + } + + #[test] + fn test_weakref_get_object() -> PyResult<()> { + Python::with_gil(|py| { + let class = get_type(py)?; + let object = class.call0()?; + let reference = PyWeakrefReference::new_bound(&object)?; + + assert!(reference.call0()?.is(&object)); + assert!(reference.get_object().is(&object)); + + drop(object); + + assert!(reference.call0()?.is(&reference.get_object())); + assert!(reference.call0()?.is_none()); + assert!(reference.get_object().is_none()); + + Ok(()) + }) + } + + #[test] + fn test_weakref_get_object_borrowed() -> PyResult<()> { + Python::with_gil(|py| { + let class = get_type(py)?; + let object = class.call0()?; + let reference = PyWeakrefReference::new_bound(&object)?; + + assert!(reference.call0()?.is(&object)); + assert!(reference.get_object_borrowed().is(&object)); + + drop(object); + + assert!(reference.call0()?.is_none()); + assert!(reference.get_object_borrowed().is_none()); + + Ok(()) + }) + } + } + + // under 'abi3-py37' and 'abi3-py38' PyClass cannot be weakreferencable. + #[cfg(all(feature = "macros", not(all(Py_LIMITED_API, not(Py_3_9)))))] + mod pyo3_pyclass { + use super::*; + use crate::{pyclass, Py}; + + #[pyclass(weakref, crate = "crate")] + struct WeakrefablePyClass {} + + #[test] + fn test_weakref_reference_behavior() -> PyResult<()> { + Python::with_gil(|py| { + let object: Bound<'_, WeakrefablePyClass> = Bound::new(py, WeakrefablePyClass {})?; + let reference = PyWeakrefReference::new_bound(&object)?; + + assert!(!reference.is(&object)); + assert!(reference.get_object().is(&object)); + #[cfg(not(Py_LIMITED_API))] + assert_eq!(reference.get_type().to_string(), CLASS_NAME); + + #[cfg(not(Py_LIMITED_API))] + assert_eq!(reference.getattr("__class__")?.to_string(), CLASS_NAME); + #[cfg(not(Py_LIMITED_API))] + check_repr(&reference, Some((object.as_any(), "WeakrefablePyClass")))?; + + assert!(reference + .getattr("__callback__") + .map_or(false, |result| result.is_none())); + + assert!(reference.call0()?.is(&object)); + + drop(object); + + assert!(reference.get_object().is_none()); + #[cfg(not(Py_LIMITED_API))] + assert_eq!(reference.getattr("__class__")?.to_string(), CLASS_NAME); + check_repr(&reference, None)?; + + assert!(reference + .getattr("__callback__") + .map_or(false, |result| result.is_none())); + + assert!(reference.call0()?.is_none()); + + Ok(()) + }) + } + + #[test] + fn test_weakref_upgrade_as() -> PyResult<()> { + Python::with_gil(|py| { + let object = Py::new(py, WeakrefablePyClass {})?; + let reference = PyWeakrefReference::new_bound(object.bind(py))?; + + { + let obj = reference.upgrade_as::(); + + assert!(obj.is_ok()); + let obj = obj.unwrap(); + + assert!(obj.is_some()); + assert!(obj.map_or(false, |obj| obj.as_ptr() == object.as_ptr())); + } + + drop(object); + + { + let obj = reference.upgrade_as::(); + + assert!(obj.is_ok()); + let obj = obj.unwrap(); + + assert!(obj.is_none()); + } + + Ok(()) + }) + } + + #[test] + fn test_weakref_upgrade_borrowed_as() -> PyResult<()> { + Python::with_gil(|py| { + let object = Py::new(py, WeakrefablePyClass {})?; + let reference = PyWeakrefReference::new_bound(object.bind(py))?; + + { + let obj = reference.upgrade_borrowed_as::(); + + assert!(obj.is_ok()); + let obj = obj.unwrap(); + + assert!(obj.is_some()); + assert!(obj.map_or(false, |obj| obj.as_ptr() == object.as_ptr())); + } + + drop(object); + + { + let obj = reference.upgrade_borrowed_as::(); + + assert!(obj.is_ok()); + let obj = obj.unwrap(); + + assert!(obj.is_none()); + } + + Ok(()) + }) + } + + #[test] + fn test_weakref_upgrade_as_unchecked() -> PyResult<()> { + Python::with_gil(|py| { + let object = Py::new(py, WeakrefablePyClass {})?; + let reference = PyWeakrefReference::new_bound(object.bind(py))?; + + { + let obj = unsafe { reference.upgrade_as_unchecked::() }; + + assert!(obj.is_some()); + assert!(obj.map_or(false, |obj| obj.as_ptr() == object.as_ptr())); + } + + drop(object); + + { + let obj = unsafe { reference.upgrade_as_unchecked::() }; + + assert!(obj.is_none()); + } + + Ok(()) + }) + } + + #[test] + fn test_weakref_upgrade_borrowed_as_unchecked() -> PyResult<()> { + Python::with_gil(|py| { + let object = Py::new(py, WeakrefablePyClass {})?; + let reference = PyWeakrefReference::new_bound(object.bind(py))?; + + { + let obj = + unsafe { reference.upgrade_borrowed_as_unchecked::() }; + + assert!(obj.is_some()); + assert!(obj.map_or(false, |obj| obj.as_ptr() == object.as_ptr())); + } + + drop(object); + + { + let obj = + unsafe { reference.upgrade_borrowed_as_unchecked::() }; + + assert!(obj.is_none()); + } + + Ok(()) + }) + } + + #[test] + fn test_weakref_upgrade() -> PyResult<()> { + Python::with_gil(|py| { + let object = Py::new(py, WeakrefablePyClass {})?; + let reference = PyWeakrefReference::new_bound(object.bind(py))?; + + assert!(reference.call0()?.is(&object)); + assert!(reference.upgrade().is_some()); + assert!(reference.upgrade().map_or(false, |obj| obj.is(&object))); + + drop(object); + + assert!(reference.call0()?.is_none()); + assert!(reference.upgrade().is_none()); + + Ok(()) + }) + } + + #[test] + fn test_weakref_upgrade_borrowed() -> PyResult<()> { + Python::with_gil(|py| { + let object = Py::new(py, WeakrefablePyClass {})?; + let reference = PyWeakrefReference::new_bound(object.bind(py))?; + + assert!(reference.call0()?.is(&object)); + assert!(reference.upgrade_borrowed().is_some()); + assert!(reference + .upgrade_borrowed() + .map_or(false, |obj| obj.is(&object))); + + drop(object); + + assert!(reference.call0()?.is_none()); + assert!(reference.upgrade_borrowed().is_none()); + + Ok(()) + }) + } + + #[test] + fn test_weakref_get_object() -> PyResult<()> { + Python::with_gil(|py| { + let object = Py::new(py, WeakrefablePyClass {})?; + let reference = PyWeakrefReference::new_bound(object.bind(py))?; + + assert!(reference.call0()?.is(&object)); + assert!(reference.get_object().is(&object)); + + drop(object); + + assert!(reference.call0()?.is(&reference.get_object())); + assert!(reference.call0()?.is_none()); + assert!(reference.get_object().is_none()); + + Ok(()) + }) + } + + #[test] + fn test_weakref_get_object_borrowed() -> PyResult<()> { + Python::with_gil(|py| { + let object = Py::new(py, WeakrefablePyClass {})?; + let reference = PyWeakrefReference::new_bound(object.bind(py))?; + + assert!(reference.call0()?.is(&object)); + assert!(reference.get_object_borrowed().is(&object)); + + drop(object); + + assert!(reference.call0()?.is_none()); + assert!(reference.get_object_borrowed().is_none()); + + Ok(()) + }) + } + } +} From a7a5c10b8ab18ecedc95b49425eed373a8a37a76 Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Sat, 1 Jun 2024 16:20:20 +0200 Subject: [PATCH 093/495] add pyclass `hash` option (#4206) * add pyclass `hash` option * add newsfragment * require `frozen` option for `hash` * simplify `hash` without `frozen` error message Co-authored-by: David Hewitt * require `eq` for `hash` * prevent manual `__hash__` with `#pyo3(hash)` * combine error messages --------- Co-authored-by: David Hewitt --- guide/pyclass-parameters.md | 1 + guide/src/class/object.md | 13 +++++ newsfragments/4206.added.md | 1 + pyo3-macros-backend/src/attributes.rs | 1 + pyo3-macros-backend/src/pyclass.rs | 48 ++++++++++++++++- pyo3-macros-backend/src/pymethod.rs | 2 +- pyo3-macros-backend/src/utils.rs | 15 +++++- tests/test_class_basics.rs | 28 ++++++++++ tests/test_enum.rs | 60 +++++++++++++++++++++ tests/ui/invalid_pyclass_args.rs | 19 +++++++ tests/ui/invalid_pyclass_args.stderr | 75 ++++++++++++++++++++++++++- tests/ui/invalid_pyclass_enum.rs | 28 ++++++++++ tests/ui/invalid_pyclass_enum.stderr | 42 +++++++++++++++ 13 files changed, 328 insertions(+), 5 deletions(-) create mode 100644 newsfragments/4206.added.md diff --git a/guide/pyclass-parameters.md b/guide/pyclass-parameters.md index 77750e36cdf..1756e8dfb35 100644 --- a/guide/pyclass-parameters.md +++ b/guide/pyclass-parameters.md @@ -11,6 +11,7 @@ | `freelist = N` | Implements a [free list][params-2] of size N. This can improve performance for types that are often created and deleted in quick succession. Profile your code to see whether `freelist` is right for you. | | `frozen` | Declares that your pyclass is immutable. It removes the borrow checker overhead when retrieving a shared reference to the Rust struct, but disables the ability to get a mutable reference. | | `get_all` | Generates getters for all fields of the pyclass. | +| `hash` | Implements `__hash__` using the `Hash` implementation of the underlying Rust datatype. | | `mapping` | Inform PyO3 that this class is a [`Mapping`][params-mapping], and so leave its implementation of sequence C-API slots empty. | | `module = "module_name"` | Python code will see the class as being defined in this module. Defaults to `builtins`. | | `name = "python_name"` | Sets the name that Python sees this class as. Defaults to the name of the Rust struct. | diff --git a/guide/src/class/object.md b/guide/src/class/object.md index e7a366870b7..3b775c2b438 100644 --- a/guide/src/class/object.md +++ b/guide/src/class/object.md @@ -121,6 +121,19 @@ impl Number { } } ``` +To implement `__hash__` using the Rust [`Hash`] trait implementation, the `hash` option can be used. +This option is only available for `frozen` classes to prevent accidental hash changes from mutating the object. If you need +an `__hash__` implementation for a mutable class, use the manual method from above. This option also requires `eq`: According to the +[Python docs](https://docs.python.org/3/reference/datamodel.html#object.__hash__) "If a class does not define an `__eq__()` +method it should not define a `__hash__()` operation either" +```rust +# use pyo3::prelude::*; +# +#[pyclass(frozen, eq, hash)] +#[derive(PartialEq, Hash)] +struct Number(i32); +``` + > **Note**: When implementing `__hash__` and comparisons, it is important that the following property holds: > diff --git a/newsfragments/4206.added.md b/newsfragments/4206.added.md new file mode 100644 index 00000000000..90a74af329c --- /dev/null +++ b/newsfragments/4206.added.md @@ -0,0 +1 @@ +Added `#[pyclass(hash)]` option to implement `__hash__` in terms of the `Hash` implementation diff --git a/pyo3-macros-backend/src/attributes.rs b/pyo3-macros-backend/src/attributes.rs index 3bccf0ae3ee..b7ef2ae6718 100644 --- a/pyo3-macros-backend/src/attributes.rs +++ b/pyo3-macros-backend/src/attributes.rs @@ -22,6 +22,7 @@ pub mod kw { syn::custom_keyword!(frozen); syn::custom_keyword!(get); syn::custom_keyword!(get_all); + syn::custom_keyword!(hash); syn::custom_keyword!(item); syn::custom_keyword!(from_item_all); syn::custom_keyword!(mapping); diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index 5838f7c5f5c..6c7e7d8609c 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -12,7 +12,7 @@ use crate::pyfunction::ConstructorAttribute; use crate::pyimpl::{gen_py_const, PyClassMethodsType}; use crate::pymethod::{ impl_py_getter_def, impl_py_setter_def, MethodAndMethodDef, MethodAndSlotDef, PropertyType, - SlotDef, __GETITEM__, __INT__, __LEN__, __REPR__, __RICHCMP__, + SlotDef, __GETITEM__, __HASH__, __INT__, __LEN__, __REPR__, __RICHCMP__, }; use crate::utils::Ctx; use crate::utils::{self, apply_renaming_rule, PythonDoc}; @@ -21,6 +21,7 @@ use proc_macro2::{Ident, Span, TokenStream}; use quote::{format_ident, quote, quote_spanned}; use syn::ext::IdentExt; use syn::parse::{Parse, ParseStream}; +use syn::parse_quote_spanned; use syn::punctuated::Punctuated; use syn::{parse_quote, spanned::Spanned, Result, Token}; @@ -65,6 +66,7 @@ pub struct PyClassPyO3Options { pub get_all: Option, pub freelist: Option, pub frozen: Option, + pub hash: Option, pub mapping: Option, pub module: Option, pub name: Option, @@ -85,6 +87,7 @@ enum PyClassPyO3Option { Freelist(FreelistAttribute), Frozen(kw::frozen), GetAll(kw::get_all), + Hash(kw::hash), Mapping(kw::mapping), Module(ModuleAttribute), Name(NameAttribute), @@ -115,6 +118,8 @@ impl Parse for PyClassPyO3Option { input.parse().map(PyClassPyO3Option::Frozen) } else if lookahead.peek(attributes::kw::get_all) { input.parse().map(PyClassPyO3Option::GetAll) + } else if lookahead.peek(attributes::kw::hash) { + input.parse().map(PyClassPyO3Option::Hash) } else if lookahead.peek(attributes::kw::mapping) { input.parse().map(PyClassPyO3Option::Mapping) } else if lookahead.peek(attributes::kw::module) { @@ -180,6 +185,7 @@ impl PyClassPyO3Options { PyClassPyO3Option::Freelist(freelist) => set_option!(freelist), PyClassPyO3Option::Frozen(frozen) => set_option!(frozen), PyClassPyO3Option::GetAll(get_all) => set_option!(get_all), + PyClassPyO3Option::Hash(hash) => set_option!(hash), PyClassPyO3Option::Mapping(mapping) => set_option!(mapping), PyClassPyO3Option::Module(module) => set_option!(module), PyClassPyO3Option::Name(name) => set_option!(name), @@ -363,8 +369,12 @@ fn impl_class( let (default_richcmp, default_richcmp_slot) = pyclass_richcmp(&args.options, &syn::parse_quote!(#cls), ctx)?; + let (default_hash, default_hash_slot) = + pyclass_hash(&args.options, &syn::parse_quote!(#cls), ctx)?; + let mut slots = Vec::new(); slots.extend(default_richcmp_slot); + slots.extend(default_hash_slot); let py_class_impl = PyClassImplsBuilder::new( cls, @@ -393,6 +403,7 @@ fn impl_class( #[allow(non_snake_case)] impl #cls { #default_richcmp + #default_hash } }) } @@ -798,9 +809,11 @@ fn impl_simple_enum( let (default_richcmp, default_richcmp_slot) = pyclass_richcmp_simple_enum(&args.options, &ty, repr_type, ctx); + let (default_hash, default_hash_slot) = pyclass_hash(&args.options, &ty, ctx)?; let mut default_slots = vec![default_repr_slot, default_int_slot]; default_slots.extend(default_richcmp_slot); + default_slots.extend(default_hash_slot); let pyclass_impls = PyClassImplsBuilder::new( cls, @@ -827,6 +840,7 @@ fn impl_simple_enum( #default_repr #default_int #default_richcmp + #default_hash } }) } @@ -858,9 +872,11 @@ fn impl_complex_enum( let pytypeinfo = impl_pytypeinfo(cls, &args, None, ctx); let (default_richcmp, default_richcmp_slot) = pyclass_richcmp(&args.options, &ty, ctx)?; + let (default_hash, default_hash_slot) = pyclass_hash(&args.options, &ty, ctx)?; let mut default_slots = vec![]; default_slots.extend(default_richcmp_slot); + default_slots.extend(default_hash_slot); let impl_builder = PyClassImplsBuilder::new( cls, @@ -967,6 +983,7 @@ fn impl_complex_enum( #[allow(non_snake_case)] impl #cls { #default_richcmp + #default_hash } #(#variant_cls_zsts)* @@ -1783,6 +1800,35 @@ fn pyclass_richcmp( } } +fn pyclass_hash( + options: &PyClassPyO3Options, + cls: &syn::Type, + ctx: &Ctx, +) -> Result<(Option, Option)> { + if options.hash.is_some() { + ensure_spanned!( + options.frozen.is_some(), options.hash.span() => "The `hash` option requires the `frozen` option."; + options.eq.is_some(), options.hash.span() => "The `hash` option requires the `eq` option."; + ); + } + // FIXME: Use hash.map(...).unzip() on MSRV >= 1.66 + match options.hash { + Some(opt) => { + let mut hash_impl = parse_quote_spanned! { opt.span() => + fn __pyo3__generated____hash__(&self) -> u64 { + let mut s = ::std::collections::hash_map::DefaultHasher::new(); + ::std::hash::Hash::hash(self, &mut s); + ::std::hash::Hasher::finish(&s) + } + }; + let hash_slot = + generate_protocol_slot(cls, &mut hash_impl, &__HASH__, "__hash__", ctx).unwrap(); + Ok((Some(hash_impl), Some(hash_slot))) + } + None => Ok((None, None)), + } +} + /// Implements most traits used by `#[pyclass]`. /// /// Specifically, it implements traits that only depend on class name, diff --git a/pyo3-macros-backend/src/pymethod.rs b/pyo3-macros-backend/src/pymethod.rs index f5b11af3c27..013b15010bf 100644 --- a/pyo3-macros-backend/src/pymethod.rs +++ b/pyo3-macros-backend/src/pymethod.rs @@ -910,7 +910,7 @@ impl PropertyType<'_> { const __STR__: SlotDef = SlotDef::new("Py_tp_str", "reprfunc"); pub const __REPR__: SlotDef = SlotDef::new("Py_tp_repr", "reprfunc"); -const __HASH__: SlotDef = SlotDef::new("Py_tp_hash", "hashfunc") +pub const __HASH__: SlotDef = SlotDef::new("Py_tp_hash", "hashfunc") .ret_ty(Ty::PyHashT) .return_conversion(TokenGenerator( |Ctx { pyo3_path }: &Ctx| quote! { #pyo3_path::callback::HashCallbackOutput }, diff --git a/pyo3-macros-backend/src/utils.rs b/pyo3-macros-backend/src/utils.rs index ca32abb42b3..a4c2c5e8a3b 100644 --- a/pyo3-macros-backend/src/utils.rs +++ b/pyo3-macros-backend/src/utils.rs @@ -25,7 +25,20 @@ macro_rules! ensure_spanned { if !($condition) { bail_spanned!($span => $msg); } - } + }; + ($($condition:expr, $span:expr => $msg:expr;)*) => { + if let Some(e) = [$( + (!($condition)).then(|| err_spanned!($span => $msg)), + )*] + .into_iter() + .flatten() + .reduce(|mut acc, e| { + acc.combine(e); + acc + }) { + return Err(e); + } + }; } /// Check if the given type `ty` is `pyo3::Python`. diff --git a/tests/test_class_basics.rs b/tests/test_class_basics.rs index d0e745377c8..5b14e8a6121 100644 --- a/tests/test_class_basics.rs +++ b/tests/test_class_basics.rs @@ -200,6 +200,34 @@ fn class_with_object_field() { }); } +#[pyclass(frozen, eq, hash)] +#[derive(PartialEq, Hash)] +struct ClassWithHash { + value: usize, +} + +#[test] +fn class_with_hash() { + Python::with_gil(|py| { + use pyo3::types::IntoPyDict; + let class = ClassWithHash { value: 42 }; + let hash = { + use std::hash::{Hash, Hasher}; + let mut hasher = std::collections::hash_map::DefaultHasher::new(); + class.hash(&mut hasher); + hasher.finish() as isize + }; + + let env = [ + ("obj", Py::new(py, class).unwrap().into_any()), + ("hsh", hash.into_py(py)), + ] + .into_py_dict_bound(py); + + py_assert!(py, *env, "hash(obj) == hsh"); + }); +} + #[pyclass(unsendable, subclass)] struct UnsendableBase { value: std::rc::Rc, diff --git a/tests/test_enum.rs b/tests/test_enum.rs index 148520dd771..7bfd624af03 100644 --- a/tests/test_enum.rs +++ b/tests/test_enum.rs @@ -220,3 +220,63 @@ fn test_renaming_all_enum_variants() { ); }); } + +#[pyclass(frozen, eq, eq_int, hash)] +#[derive(PartialEq, Hash)] +enum SimpleEnumWithHash { + A, + B, +} + +#[test] +fn test_simple_enum_with_hash() { + Python::with_gil(|py| { + use pyo3::types::IntoPyDict; + let class = SimpleEnumWithHash::A; + let hash = { + use std::hash::{Hash, Hasher}; + let mut hasher = std::collections::hash_map::DefaultHasher::new(); + class.hash(&mut hasher); + hasher.finish() as isize + }; + + let env = [ + ("obj", Py::new(py, class).unwrap().into_any()), + ("hsh", hash.into_py(py)), + ] + .into_py_dict_bound(py); + + py_assert!(py, *env, "hash(obj) == hsh"); + }); +} + +#[pyclass(eq, hash)] +#[derive(PartialEq, Hash)] +enum ComplexEnumWithHash { + A(u32), + B { msg: String }, +} + +#[test] +fn test_complex_enum_with_hash() { + Python::with_gil(|py| { + use pyo3::types::IntoPyDict; + let class = ComplexEnumWithHash::B { + msg: String::from("Hello"), + }; + let hash = { + use std::hash::{Hash, Hasher}; + let mut hasher = std::collections::hash_map::DefaultHasher::new(); + class.hash(&mut hasher); + hasher.finish() as isize + }; + + let env = [ + ("obj", Py::new(py, class).unwrap().into_any()), + ("hsh", hash.into_py(py)), + ] + .into_py_dict_bound(py); + + py_assert!(py, *env, "hash(obj) == hsh"); + }); +} diff --git a/tests/ui/invalid_pyclass_args.rs b/tests/ui/invalid_pyclass_args.rs index 6e359f6130e..24842eb484a 100644 --- a/tests/ui/invalid_pyclass_args.rs +++ b/tests/ui/invalid_pyclass_args.rs @@ -52,4 +52,23 @@ impl EqOptAndManualRichCmp { #[pyclass(eq_int)] struct NoEqInt {} +#[pyclass(frozen, eq, hash)] +#[derive(PartialEq)] +struct HashOptRequiresHash; + +#[pyclass(hash)] +#[derive(Hash)] +struct HashWithoutFrozenAndEq; + +#[pyclass(frozen, eq, hash)] +#[derive(PartialEq, Hash)] +struct HashOptAndManualHash {} + +#[pymethods] +impl HashOptAndManualHash { + fn __hash__(&self) -> u64 { + todo!() + } +} + fn main() {} diff --git a/tests/ui/invalid_pyclass_args.stderr b/tests/ui/invalid_pyclass_args.stderr index 72da82385e7..8f1b671dfd9 100644 --- a/tests/ui/invalid_pyclass_args.stderr +++ b/tests/ui/invalid_pyclass_args.stderr @@ -1,4 +1,4 @@ -error: expected one of: `crate`, `dict`, `eq`, `eq_int`, `extends`, `freelist`, `frozen`, `get_all`, `mapping`, `module`, `name`, `rename_all`, `sequence`, `set_all`, `subclass`, `unsendable`, `weakref` +error: expected one of: `crate`, `dict`, `eq`, `eq_int`, `extends`, `freelist`, `frozen`, `get_all`, `hash`, `mapping`, `module`, `name`, `rename_all`, `sequence`, `set_all`, `subclass`, `unsendable`, `weakref` --> tests/ui/invalid_pyclass_args.rs:3:11 | 3 | #[pyclass(extend=pyo3::types::PyDict)] @@ -46,7 +46,7 @@ error: expected string literal 24 | #[pyclass(module = my_module)] | ^^^^^^^^^ -error: expected one of: `crate`, `dict`, `eq`, `eq_int`, `extends`, `freelist`, `frozen`, `get_all`, `mapping`, `module`, `name`, `rename_all`, `sequence`, `set_all`, `subclass`, `unsendable`, `weakref` +error: expected one of: `crate`, `dict`, `eq`, `eq_int`, `extends`, `freelist`, `frozen`, `get_all`, `hash`, `mapping`, `module`, `name`, `rename_all`, `sequence`, `set_all`, `subclass`, `unsendable`, `weakref` --> tests/ui/invalid_pyclass_args.rs:27:11 | 27 | #[pyclass(weakrev)] @@ -64,6 +64,18 @@ error: `eq_int` can only be used on simple enums. 52 | #[pyclass(eq_int)] | ^^^^^^ +error: The `hash` option requires the `frozen` option. + --> tests/ui/invalid_pyclass_args.rs:59:11 + | +59 | #[pyclass(hash)] + | ^^^^ + +error: The `hash` option requires the `eq` option. + --> tests/ui/invalid_pyclass_args.rs:59:11 + | +59 | #[pyclass(hash)] + | ^^^^ + error[E0592]: duplicate definitions with name `__pymethod___richcmp____` --> tests/ui/invalid_pyclass_args.rs:36:1 | @@ -75,6 +87,17 @@ error[E0592]: duplicate definitions with name `__pymethod___richcmp____` | = note: this error originates in the attribute macro `pyclass` (in Nightly builds, run with -Z macro-backtrace for more info) +error[E0592]: duplicate definitions with name `__pymethod___hash____` + --> tests/ui/invalid_pyclass_args.rs:63:1 + | +63 | #[pyclass(frozen, eq, hash)] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ duplicate definitions for `__pymethod___hash____` +... +67 | #[pymethods] + | ------------ other definition for `__pymethod___hash____` + | + = note: this error originates in the attribute macro `pyclass` (in Nightly builds, run with -Z macro-backtrace for more info) + error[E0369]: binary operation `==` cannot be applied to type `&EqOptRequiresEq` --> tests/ui/invalid_pyclass_args.rs:33:11 | @@ -144,3 +167,51 @@ note: candidate #2 is defined in an impl for the type `EqOptAndManualRichCmp` 40 | #[pymethods] | ^^^^^^^^^^^^ = 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 `HashOptRequiresHash: Hash` is not satisfied + --> tests/ui/invalid_pyclass_args.rs:55:23 + | +55 | #[pyclass(frozen, eq, hash)] + | ^^^^ the trait `Hash` is not implemented for `HashOptRequiresHash` + | +help: consider annotating `HashOptRequiresHash` with `#[derive(Hash)]` + | +57 + #[derive(Hash)] +58 | struct HashOptRequiresHash; + | + +error[E0034]: multiple applicable items in scope + --> tests/ui/invalid_pyclass_args.rs:63:1 + | +63 | #[pyclass(frozen, eq, hash)] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ multiple `__pymethod___hash____` found + | +note: candidate #1 is defined in an impl for the type `HashOptAndManualHash` + --> tests/ui/invalid_pyclass_args.rs:63:1 + | +63 | #[pyclass(frozen, eq, hash)] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +note: candidate #2 is defined in an impl for the type `HashOptAndManualHash` + --> tests/ui/invalid_pyclass_args.rs:67:1 + | +67 | #[pymethods] + | ^^^^^^^^^^^^ + = note: this error originates in the attribute macro `pyclass` which comes from the expansion of the attribute macro `pymethods` (in Nightly builds, run with -Z macro-backtrace for more info) + +error[E0034]: multiple applicable items in scope + --> tests/ui/invalid_pyclass_args.rs:67:1 + | +67 | #[pymethods] + | ^^^^^^^^^^^^ multiple `__pymethod___hash____` found + | +note: candidate #1 is defined in an impl for the type `HashOptAndManualHash` + --> tests/ui/invalid_pyclass_args.rs:63:1 + | +63 | #[pyclass(frozen, eq, hash)] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +note: candidate #2 is defined in an impl for the type `HashOptAndManualHash` + --> tests/ui/invalid_pyclass_args.rs:67:1 + | +67 | #[pymethods] + | ^^^^^^^^^^^^ + = note: this error originates in the attribute macro `pymethods` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/tests/ui/invalid_pyclass_enum.rs b/tests/ui/invalid_pyclass_enum.rs index 3c6f08da653..f4b94a612ff 100644 --- a/tests/ui/invalid_pyclass_enum.rs +++ b/tests/ui/invalid_pyclass_enum.rs @@ -46,4 +46,32 @@ enum NoEqInt { B { msg: String }, } +#[pyclass(frozen, eq, eq_int, hash)] +#[derive(PartialEq)] +enum SimpleHashOptRequiresHash { + A, + B, +} + +#[pyclass(frozen, eq, hash)] +#[derive(PartialEq)] +enum ComplexHashOptRequiresHash { + A(i32), + B { msg: String }, +} + +#[pyclass(hash)] +#[derive(Hash)] +enum SimpleHashOptRequiresFrozenAndEq { + A, + B, +} + +#[pyclass(hash)] +#[derive(Hash)] +enum ComplexHashOptRequiresEq { + A(i32), + B { msg: String }, +} + fn main() {} diff --git a/tests/ui/invalid_pyclass_enum.stderr b/tests/ui/invalid_pyclass_enum.stderr index 551d920eae3..d817e6011ff 100644 --- a/tests/ui/invalid_pyclass_enum.stderr +++ b/tests/ui/invalid_pyclass_enum.stderr @@ -36,6 +36,24 @@ error: `eq_int` can only be used on simple enums. 43 | #[pyclass(eq_int)] | ^^^^^^ +error: The `hash` option requires the `frozen` option. + --> tests/ui/invalid_pyclass_enum.rs:63:11 + | +63 | #[pyclass(hash)] + | ^^^^ + +error: The `hash` option requires the `eq` option. + --> tests/ui/invalid_pyclass_enum.rs:63:11 + | +63 | #[pyclass(hash)] + | ^^^^ + +error: The `hash` option requires the `eq` option. + --> tests/ui/invalid_pyclass_enum.rs:70:11 + | +70 | #[pyclass(hash)] + | ^^^^ + error[E0369]: binary operation `==` cannot be applied to type `&SimpleEqOptRequiresPartialEq` --> tests/ui/invalid_pyclass_enum.rs:31:11 | @@ -103,3 +121,27 @@ help: consider annotating `ComplexEqOptRequiresPartialEq` with `#[derive(Partial 38 + #[derive(PartialEq)] 39 | enum ComplexEqOptRequiresPartialEq { | + +error[E0277]: the trait bound `SimpleHashOptRequiresHash: Hash` is not satisfied + --> tests/ui/invalid_pyclass_enum.rs:49:31 + | +49 | #[pyclass(frozen, eq, eq_int, hash)] + | ^^^^ the trait `Hash` is not implemented for `SimpleHashOptRequiresHash` + | +help: consider annotating `SimpleHashOptRequiresHash` with `#[derive(Hash)]` + | +51 + #[derive(Hash)] +52 | enum SimpleHashOptRequiresHash { + | + +error[E0277]: the trait bound `ComplexHashOptRequiresHash: Hash` is not satisfied + --> tests/ui/invalid_pyclass_enum.rs:56:23 + | +56 | #[pyclass(frozen, eq, hash)] + | ^^^^ the trait `Hash` is not implemented for `ComplexHashOptRequiresHash` + | +help: consider annotating `ComplexHashOptRequiresHash` with `#[derive(Hash)]` + | +58 + #[derive(Hash)] +59 | enum ComplexHashOptRequiresHash { + | From 5d47c4ae4ca9b1eb4112fbc5273c3706365f59d9 Mon Sep 17 00:00:00 2001 From: JRRudy1 <31031841+JRRudy1@users.noreply.github.com> Date: Sat, 1 Jun 2024 16:09:14 -0500 Subject: [PATCH 094/495] Added `ToPyObject` and `IntoPy` impls for `PyBackedStr`. (#4205) * Added `ToPyObject` and `Into` impls for `PyBackedStr`. * Create 4205.added.md * Added attributes limiting the `ToPyObject` and `IntoPy` impls to the case where `cfg(any(Py_3_10, not(Py_LIMITED_API)))`. When this cfg does not apply, the conversion is less trivial since the `storage` is actually `PyBytes`, not `PyString`. * Fixed imports format. * Updated the `ToPyObject` and `IntoPy` impls to support the `cfg(not(any(Py_3_10, not(Py_LIMITED_API))))` case by converting the `PyBytes` back to `PyString`. * Added `ToPyObject` and `IntoPy` impls for `PyBackedBytes`. * Added tests for the `PyBackedBytes` conversion impls. * Updated newsfragment entry to include the `PyBackedBytes` impls. * Changed the `IntoPy` and `ToPyObject` impls for `PyBackedBytes` to produce `PyBytes` regardless of the backing variant. Updated tests to demonstrate this. * retrigger checks * Updated `PyBackedStr` conversion tests to extract the result as a `PyBackedStr` instead of `&str` since the latter is not supported under some `cfg`'s. * Fixed `IntoPy for PyBackedBytes` impl to create `bytes` for both storage types as intended. Updated test to properly catch the error. --------- Co-authored-by: jrudolph --- newsfragments/4205.added.md | 1 + src/pybacked.rs | 94 ++++++++++++++++++++++++++++++++++++- 2 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 newsfragments/4205.added.md diff --git a/newsfragments/4205.added.md b/newsfragments/4205.added.md new file mode 100644 index 00000000000..86ce9fc32ae --- /dev/null +++ b/newsfragments/4205.added.md @@ -0,0 +1 @@ +Added `ToPyObject` and `IntoPy` impls for `PyBackedStr` and `PyBackedBytes`. diff --git a/src/pybacked.rs b/src/pybacked.rs index ed68ea52ec7..f6a0f99fe9a 100644 --- a/src/pybacked.rs +++ b/src/pybacked.rs @@ -7,7 +7,7 @@ use crate::{ any::PyAnyMethods, bytearray::PyByteArrayMethods, bytes::PyBytesMethods, string::PyStringMethods, PyByteArray, PyBytes, PyString, }, - Bound, DowncastError, FromPyObject, Py, PyAny, PyErr, PyResult, + Bound, DowncastError, FromPyObject, IntoPy, Py, PyAny, PyErr, PyResult, Python, ToPyObject, }; /// A wrapper around `str` where the storage is owned by a Python `bytes` or `str` object. @@ -85,6 +85,28 @@ impl FromPyObject<'_> for PyBackedStr { } } +impl ToPyObject for PyBackedStr { + #[cfg(any(Py_3_10, not(Py_LIMITED_API)))] + fn to_object(&self, py: Python<'_>) -> Py { + self.storage.clone_ref(py) + } + #[cfg(not(any(Py_3_10, not(Py_LIMITED_API))))] + fn to_object(&self, py: Python<'_>) -> Py { + PyString::new_bound(py, self).into_any().unbind() + } +} + +impl IntoPy> for PyBackedStr { + #[cfg(any(Py_3_10, not(Py_LIMITED_API)))] + fn into_py(self, _py: Python<'_>) -> Py { + self.storage + } + #[cfg(not(any(Py_3_10, not(Py_LIMITED_API))))] + fn into_py(self, py: Python<'_>) -> Py { + PyString::new_bound(py, &self).into_any().unbind() + } +} + /// A wrapper around `[u8]` where the storage is either owned by a Python `bytes` object, or a Rust `Box<[u8]>`. /// /// This type gives access to the underlying data via a `Deref` implementation. @@ -181,6 +203,24 @@ impl FromPyObject<'_> for PyBackedBytes { } } +impl ToPyObject for PyBackedBytes { + fn to_object(&self, py: Python<'_>) -> Py { + match &self.storage { + PyBackedBytesStorage::Python(bytes) => bytes.to_object(py), + PyBackedBytesStorage::Rust(bytes) => PyBytes::new_bound(py, bytes).into_any().unbind(), + } + } +} + +impl IntoPy> for PyBackedBytes { + fn into_py(self, py: Python<'_>) -> Py { + match self.storage { + PyBackedBytesStorage::Python(bytes) => bytes.into_any(), + PyBackedBytesStorage::Rust(bytes) => PyBytes::new_bound(py, &bytes).into_any().unbind(), + } + } +} + macro_rules! impl_traits { ($slf:ty, $equiv:ty) => { impl std::fmt::Debug for $slf { @@ -288,6 +328,30 @@ mod test { }); } + #[test] + fn py_backed_str_to_object() { + Python::with_gil(|py| { + let orig_str = PyString::new_bound(py, "hello"); + let py_backed_str = orig_str.extract::().unwrap(); + let new_str = py_backed_str.to_object(py); + assert_eq!(new_str.extract::(py).unwrap(), "hello"); + #[cfg(any(Py_3_10, not(Py_LIMITED_API)))] + assert!(new_str.is(&orig_str)); + }); + } + + #[test] + fn py_backed_str_into_py() { + Python::with_gil(|py| { + let orig_str = PyString::new_bound(py, "hello"); + let py_backed_str = orig_str.extract::().unwrap(); + let new_str = py_backed_str.into_py(py); + assert_eq!(new_str.extract::(py).unwrap(), "hello"); + #[cfg(any(Py_3_10, not(Py_LIMITED_API)))] + assert!(new_str.is(&orig_str)); + }); + } + #[test] fn py_backed_bytes_empty() { Python::with_gil(|py| { @@ -324,6 +388,34 @@ mod test { }); } + #[test] + fn py_backed_bytes_into_py() { + Python::with_gil(|py| { + let orig_bytes = PyBytes::new_bound(py, b"abcde"); + let py_backed_bytes = PyBackedBytes::from(orig_bytes.clone()); + assert!(py_backed_bytes.to_object(py).is(&orig_bytes)); + assert!(py_backed_bytes.into_py(py).is(&orig_bytes)); + }); + } + + #[test] + fn rust_backed_bytes_into_py() { + Python::with_gil(|py| { + let orig_bytes = PyByteArray::new_bound(py, b"abcde"); + let rust_backed_bytes = PyBackedBytes::from(orig_bytes); + assert!(matches!( + rust_backed_bytes.storage, + PyBackedBytesStorage::Rust(_) + )); + let to_object = rust_backed_bytes.to_object(py).into_bound(py); + assert!(&to_object.is_exact_instance_of::()); + assert_eq!(&to_object.extract::().unwrap(), b"abcde"); + let into_py = rust_backed_bytes.into_py(py).into_bound(py); + assert!(&into_py.is_exact_instance_of::()); + assert_eq!(&into_py.extract::().unwrap(), b"abcde"); + }); + } + #[test] fn test_backed_types_send_sync() { fn is_send() {} From 88b6f23e3b324e3def0a08efa1b6cfac0bb6d290 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Sun, 2 Jun 2024 12:11:14 +0100 Subject: [PATCH 095/495] fix calling POOL.update_counts() when no `gil-refs` feature (#4200) * fix calling POOL.update_counts() when no `gil-refs` feature * fixup conditional compilation * always increment gil count * correct test * clippy fix * fix clippy * Deduplicate construction of GILGuard::Assumed. * Remove unsafe-block-with-unsafe-function triggering errors in our MSRV builds. --------- Co-authored-by: Adam Reichold --- src/gil.rs | 116 ++++++++++++++++++++++++++++++-------------------- src/marker.rs | 5 +++ 2 files changed, 75 insertions(+), 46 deletions(-) diff --git a/src/gil.rs b/src/gil.rs index 6f97011b71c..3e25c7cfb0f 100644 --- a/src/gil.rs +++ b/src/gil.rs @@ -166,8 +166,8 @@ impl GILGuard { /// `GILGuard::Ensured` will be returned. pub(crate) fn acquire() -> Self { if gil_is_acquired() { - increment_gil_count(); - return GILGuard::Assumed; + // SAFETY: We just checked that the GIL is already acquired. + return unsafe { Self::assume() }; } // Maybe auto-initialize the GIL: @@ -205,7 +205,8 @@ impl GILGuard { } } - Self::acquire_unchecked() + // SAFETY: We have ensured the Python interpreter is initialized. + unsafe { Self::acquire_unchecked() } } /// Acquires the `GILGuard` without performing any state checking. @@ -213,35 +214,34 @@ impl GILGuard { /// This can be called in "unsafe" contexts where the normal interpreter state /// checking performed by `GILGuard::acquire` may fail. This includes calling /// as part of multi-phase interpreter initialization. - pub(crate) fn acquire_unchecked() -> Self { + pub(crate) unsafe fn acquire_unchecked() -> Self { if gil_is_acquired() { - increment_gil_count(); - return GILGuard::Assumed; + return Self::assume(); } - let gstate = unsafe { ffi::PyGILState_Ensure() }; // acquire GIL + let gstate = ffi::PyGILState_Ensure(); // acquire GIL + increment_gil_count(); + #[cfg(feature = "gil-refs")] #[allow(deprecated)] - { - let pool = unsafe { mem::ManuallyDrop::new(GILPool::new()) }; - increment_gil_count(); - GILGuard::Ensured { gstate, pool } - } + let pool = mem::ManuallyDrop::new(GILPool::new()); - #[cfg(not(feature = "gil-refs"))] - { - increment_gil_count(); - // Update counts of PyObjects / Py that have been cloned or dropped since last acquisition - #[cfg(not(pyo3_disable_reference_pool))] - POOL.update_counts(unsafe { Python::assume_gil_acquired() }); - - GILGuard::Ensured { gstate } + #[cfg(not(pyo3_disable_reference_pool))] + POOL.update_counts(Python::assume_gil_acquired()); + GILGuard::Ensured { + gstate, + #[cfg(feature = "gil-refs")] + pool, } } + /// Acquires the `GILGuard` while assuming that the GIL is already held. pub(crate) unsafe fn assume() -> Self { increment_gil_count(); - GILGuard::Assumed + let guard = GILGuard::Assumed; + #[cfg(not(pyo3_disable_reference_pool))] + POOL.update_counts(guard.python()); + guard } /// Gets the Python token associated with this [`GILGuard`]. @@ -256,15 +256,14 @@ impl Drop for GILGuard { fn drop(&mut self) { match self { GILGuard::Assumed => {} - #[cfg(feature = "gil-refs")] - GILGuard::Ensured { gstate, pool } => unsafe { + GILGuard::Ensured { + gstate, + #[cfg(feature = "gil-refs")] + pool, + } => unsafe { // Drop the objects in the pool before attempting to release the thread state + #[cfg(feature = "gil-refs")] mem::ManuallyDrop::drop(pool); - - ffi::PyGILState_Release(*gstate); - }, - #[cfg(not(feature = "gil-refs"))] - GILGuard::Ensured { gstate } => unsafe { ffi::PyGILState_Release(*gstate); }, } @@ -549,14 +548,15 @@ fn decrement_gil_count() { #[cfg(test)] mod tests { - #[cfg(not(pyo3_disable_reference_pool))] - use super::{gil_is_acquired, POOL}; + use super::GIL_COUNT; #[cfg(feature = "gil-refs")] #[allow(deprecated)] - use super::{GILPool, GIL_COUNT, OWNED_OBJECTS}; - use crate::types::any::PyAnyMethods; + use super::OWNED_OBJECTS; + #[cfg(not(pyo3_disable_reference_pool))] + use super::{gil_is_acquired, POOL}; #[cfg(feature = "gil-refs")] use crate::{ffi, gil}; + use crate::{gil::GILGuard, types::any::PyAnyMethods}; use crate::{PyObject, Python}; use std::ptr::NonNull; @@ -582,7 +582,7 @@ mod tests { .contains(&unsafe { NonNull::new_unchecked(obj.as_ptr()) }) } - #[cfg(all(not(pyo3_disable_reference_pool), not(target_arch = "wasm32")))] + #[cfg(not(pyo3_disable_reference_pool))] fn pool_dec_refs_contains(obj: &PyObject) -> bool { POOL.pending_decrefs .lock() @@ -702,30 +702,29 @@ mod tests { } #[test] - #[cfg(feature = "gil-refs")] #[allow(deprecated)] fn test_gil_counts() { - // Check with_gil and GILPool both increase counts correctly + // Check with_gil and GILGuard both increase counts correctly let get_gil_count = || GIL_COUNT.with(|c| c.get()); assert_eq!(get_gil_count(), 0); Python::with_gil(|_| { assert_eq!(get_gil_count(), 1); - let pool = unsafe { GILPool::new() }; - assert_eq!(get_gil_count(), 1); + let pool = unsafe { GILGuard::assume() }; + assert_eq!(get_gil_count(), 2); - let pool2 = unsafe { GILPool::new() }; - assert_eq!(get_gil_count(), 1); + let pool2 = unsafe { GILGuard::assume() }; + assert_eq!(get_gil_count(), 3); drop(pool); - assert_eq!(get_gil_count(), 1); + assert_eq!(get_gil_count(), 2); Python::with_gil(|_| { - // nested with_gil doesn't update gil count - assert_eq!(get_gil_count(), 2); + // nested with_gil updates gil count + assert_eq!(get_gil_count(), 3); }); - assert_eq!(get_gil_count(), 1); + assert_eq!(get_gil_count(), 2); drop(pool2); assert_eq!(get_gil_count(), 1); @@ -794,19 +793,20 @@ mod tests { #[test] #[cfg(not(pyo3_disable_reference_pool))] - #[cfg(feature = "gil-refs")] fn test_update_counts_does_not_deadlock() { // update_counts can run arbitrary Python code during Py_DECREF. // if the locking is implemented incorrectly, it will deadlock. + use crate::ffi; + use crate::gil::GILGuard; + Python::with_gil(|py| { let obj = get_object(py); unsafe extern "C" fn capsule_drop(capsule: *mut ffi::PyObject) { // This line will implicitly call update_counts // -> and so cause deadlock if update_counts is not handling recursion correctly. - #[allow(deprecated)] - let pool = GILPool::new(); + let pool = GILGuard::assume(); // Rebuild obj so that it can be dropped PyObject::from_owned_ptr( @@ -826,4 +826,28 @@ mod tests { POOL.update_counts(py); }) } + + #[test] + #[cfg(not(pyo3_disable_reference_pool))] + fn test_gil_guard_update_counts() { + use crate::gil::GILGuard; + + Python::with_gil(|py| { + let obj = get_object(py); + + // For GILGuard::acquire + + POOL.register_decref(NonNull::new(obj.clone_ref(py).into_ptr()).unwrap()); + assert!(pool_dec_refs_contains(&obj)); + let _guard = GILGuard::acquire(); + assert!(pool_dec_refs_does_not_contain(&obj)); + + // For GILGuard::assume + + POOL.register_decref(NonNull::new(obj.clone_ref(py).into_ptr()).unwrap()); + assert!(pool_dec_refs_contains(&obj)); + let _guard2 = unsafe { GILGuard::assume() }; + assert!(pool_dec_refs_does_not_contain(&obj)); + }) + } } diff --git a/src/marker.rs b/src/marker.rs index 1f4655d9656..a6b1e305252 100644 --- a/src/marker.rs +++ b/src/marker.rs @@ -1280,6 +1280,11 @@ mod tests { const GIL_NOT_HELD: c_int = 0; const GIL_HELD: c_int = 1; + // Before starting the interpreter the state of calling `PyGILState_Check` + // seems to be undefined, so let's ensure that Python is up. + #[cfg(not(any(PyPy, GraalPy)))] + crate::prepare_freethreaded_python(); + let state = unsafe { crate::ffi::PyGILState_Check() }; assert_eq!(state, GIL_NOT_HELD); From b4b780b475f1abecf68341a6a52219ff9db050dd Mon Sep 17 00:00:00 2001 From: liammcinroy <1899809+liammcinroy@users.noreply.github.com> Date: Mon, 3 Jun 2024 01:57:04 -0600 Subject: [PATCH 096/495] Allow module= attribute in complex enum variants (#4228) * Allow module= attribute in complex enum variants * stderr test update * towncrier * inherit `module`, rather than specifying everywhere. * clippy fix --- newsfragments/4228.changed.md | 1 + pyo3-macros-backend/src/pyclass.rs | 9 +++++++-- .../src/pyfunction/signature.rs | 13 +++++++------ tests/test_enum.rs | 18 ++++++++++++++++++ 4 files changed, 33 insertions(+), 8 deletions(-) create mode 100644 newsfragments/4228.changed.md diff --git a/newsfragments/4228.changed.md b/newsfragments/4228.changed.md new file mode 100644 index 00000000000..84323876b42 --- /dev/null +++ b/newsfragments/4228.changed.md @@ -0,0 +1 @@ +Changed the `module` option for complex enum variants to inherit from the value set on the complex enum `module`. diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index 6c7e7d8609c..255fc80c418 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -676,7 +676,7 @@ struct PyClassEnumVariantUnnamedField<'a> { } /// `#[pyo3()]` options for pyclass enum variants -#[derive(Default)] +#[derive(Clone, Default)] struct EnumVariantPyO3Options { name: Option, constructor: Option, @@ -949,7 +949,12 @@ fn impl_complex_enum( let variant_args = PyClassArgs { class_kind: PyClassKind::Struct, // TODO(mkovaxx): propagate variant.options - options: parse_quote!(extends = #cls, frozen), + options: { + let mut rigged_options: PyClassPyO3Options = parse_quote!(extends = #cls, frozen); + // If a specific module was given to the base class, use it for all variants. + rigged_options.module.clone_from(&args.options.module); + rigged_options + }, }; let variant_cls_pytypeinfo = impl_pytypeinfo(&variant_cls, &variant_args, None, ctx); diff --git a/pyo3-macros-backend/src/pyfunction/signature.rs b/pyo3-macros-backend/src/pyfunction/signature.rs index b73b96a3d59..0a2d861d2b1 100644 --- a/pyo3-macros-backend/src/pyfunction/signature.rs +++ b/pyo3-macros-backend/src/pyfunction/signature.rs @@ -13,6 +13,7 @@ use crate::{ method::{FnArg, RegularArg}, }; +#[derive(Clone)] pub struct Signature { paren_token: syn::token::Paren, pub items: Punctuated, @@ -36,35 +37,35 @@ impl ToTokens for Signature { } } -#[derive(Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct SignatureItemArgument { pub ident: syn::Ident, pub eq_and_default: Option<(Token![=], syn::Expr)>, } -#[derive(Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct SignatureItemPosargsSep { pub slash: Token![/], } -#[derive(Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct SignatureItemVarargsSep { pub asterisk: Token![*], } -#[derive(Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct SignatureItemVarargs { pub sep: SignatureItemVarargsSep, pub ident: syn::Ident, } -#[derive(Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct SignatureItemKwargs { pub asterisks: (Token![*], Token![*]), pub ident: syn::Ident, } -#[derive(Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq)] pub enum SignatureItem { Argument(Box), PosargsSep(SignatureItemPosargsSep), diff --git a/tests/test_enum.rs b/tests/test_enum.rs index 7bfd624af03..1406ac7f4d0 100644 --- a/tests/test_enum.rs +++ b/tests/test_enum.rs @@ -221,6 +221,24 @@ fn test_renaming_all_enum_variants() { }); } +#[pyclass(module = "custom_module")] +#[derive(Debug, Clone)] +enum CustomModuleComplexEnum { + Variant(), +} + +#[test] +fn test_custom_module() { + Python::with_gil(|py| { + let enum_obj = py.get_type_bound::(); + py_assert!( + py, + enum_obj, + "enum_obj.Variant.__module__ == 'custom_module'" + ); + }); +} + #[pyclass(frozen, eq, eq_int, hash)] #[derive(PartialEq, Hash)] enum SimpleEnumWithHash { From 36cdeb29c1bcab30f37adb71786e623b18b2f3ce Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Mon, 3 Jun 2024 19:32:35 +0200 Subject: [PATCH 097/495] fix incorrect raw identifier handling for the `name` attribute of `pyfunction`s (#4229) --- pyo3-macros-backend/src/pyfunction.rs | 5 ++++- tests/test_pyfunction.rs | 27 +++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/pyo3-macros-backend/src/pyfunction.rs b/pyo3-macros-backend/src/pyfunction.rs index e259f0e2c1e..a5f090a0e2c 100644 --- a/pyo3-macros-backend/src/pyfunction.rs +++ b/pyo3-macros-backend/src/pyfunction.rs @@ -208,7 +208,10 @@ pub fn impl_wrap_pyfunction( let ctx = &Ctx::new(&krate); let Ctx { pyo3_path } = &ctx; - let python_name = name.map_or_else(|| func.sig.ident.unraw(), |name| name.value.0); + let python_name = name + .as_ref() + .map_or_else(|| &func.sig.ident, |name| &name.value.0) + .unraw(); let tp = if pass_module.is_some() { let span = match func.sig.inputs.first() { diff --git a/tests/test_pyfunction.rs b/tests/test_pyfunction.rs index 4a90f3f9d99..ce4dff7b127 100644 --- a/tests/test_pyfunction.rs +++ b/tests/test_pyfunction.rs @@ -558,3 +558,30 @@ fn test_reference_to_bound_arguments() { py_assert!(py, function, "function(1, 2) == 3"); }) } + +#[test] +fn test_pyfunction_raw_ident() { + #[pyfunction] + fn r#struct() -> bool { + true + } + + #[pyfunction] + #[pyo3(name = "r#enum")] + fn raw_ident() -> bool { + true + } + + #[pymodule] + fn m(m: &Bound<'_, PyModule>) -> PyResult<()> { + m.add_function(wrap_pyfunction!(r#struct, m)?)?; + m.add_function(wrap_pyfunction!(raw_ident, m)?)?; + Ok(()) + } + + Python::with_gil(|py| { + let m = pyo3::wrap_pymodule!(m)(py); + py_assert!(py, m, "m.struct()"); + py_assert!(py, m, "m.enum()"); + }) +} From 7e5884c40bab8c7feee7b01bb8ba1785fb36ecc4 Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Mon, 3 Jun 2024 20:49:36 +0200 Subject: [PATCH 098/495] fix incorrect `__richcmp__` for `eq_int` only simple enums (#4224) * fix incorrect `__richcmp__` for `eq_int` only simple enums * add tests for deprecated simple enum eq behavior * only emit deprecation warning if neither `eq` nor `eq_int` were given * require `eq` for `eq_int` --- pyo3-macros-backend/src/pyclass.rs | 35 ++++++++----- tests/test_enum.rs | 78 ++++++++++++++++++++++++++++ tests/ui/invalid_pyclass_enum.rs | 6 +++ tests/ui/invalid_pyclass_enum.stderr | 36 +++++++------ 4 files changed, 126 insertions(+), 29 deletions(-) diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index 255fc80c418..1e7f29d84c1 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -808,7 +808,7 @@ fn impl_simple_enum( }; let (default_richcmp, default_richcmp_slot) = - pyclass_richcmp_simple_enum(&args.options, &ty, repr_type, ctx); + pyclass_richcmp_simple_enum(&args.options, &ty, repr_type, ctx)?; let (default_hash, default_hash_slot) = pyclass_hash(&args.options, &ty, ctx)?; let mut default_slots = vec![default_repr_slot, default_int_slot]; @@ -1670,14 +1670,16 @@ fn pyclass_richcmp_arms(options: &PyClassPyO3Options, ctx: &Ctx) -> TokenStream let eq_arms = options .eq - .map(|eq| { - quote_spanned! { eq.span() => + .map(|eq| eq.span) + .or(options.eq_int.map(|eq_int| eq_int.span)) + .map(|span| { + quote_spanned! { span => #pyo3_path::pyclass::CompareOp::Eq => { ::std::result::Result::Ok(#pyo3_path::conversion::IntoPy::into_py(self_val == other, py)) }, #pyo3_path::pyclass::CompareOp::Ne => { ::std::result::Result::Ok(#pyo3_path::conversion::IntoPy::into_py(self_val != other, py)) - }, + }, } }) .unwrap_or_default(); @@ -1692,15 +1694,15 @@ fn pyclass_richcmp_simple_enum( cls: &syn::Type, repr_type: &syn::Ident, ctx: &Ctx, -) -> (Option, Option) { +) -> Result<(Option, Option)> { let Ctx { pyo3_path } = ctx; - let arms = pyclass_richcmp_arms(options, ctx); + if let Some(eq_int) = options.eq_int { + ensure_spanned!(options.eq.is_some(), eq_int.span() => "The `eq_int` option requires the `eq` option."); + } - let deprecation = options - .eq_int - .map(|_| TokenStream::new()) - .unwrap_or_else(|| { + let deprecation = (options.eq_int.is_none() && options.eq.is_none()) + .then(|| { quote! { #[deprecated( since = "0.22.0", @@ -1709,15 +1711,20 @@ fn pyclass_richcmp_simple_enum( const DEPRECATION: () = (); const _: () = DEPRECATION; } - }); + }) + .unwrap_or_default(); let mut options = options.clone(); - options.eq_int = Some(parse_quote!(eq_int)); + if options.eq.is_none() { + options.eq_int = Some(parse_quote!(eq_int)); + } if options.eq.is_none() && options.eq_int.is_none() { - return (None, None); + return Ok((None, None)); } + let arms = pyclass_richcmp_arms(&options, ctx); + let eq = options.eq.map(|eq| { quote_spanned! { eq.span() => let self_val = self; @@ -1766,7 +1773,7 @@ fn pyclass_richcmp_simple_enum( } else { generate_default_protocol_slot(cls, &mut richcmp_impl, &__RICHCMP__, ctx).unwrap() }; - (Some(richcmp_impl), Some(richcmp_slot)) + Ok((Some(richcmp_impl), Some(richcmp_slot))) } fn pyclass_richcmp( diff --git a/tests/test_enum.rs b/tests/test_enum.rs index 1406ac7f4d0..4b72143539f 100644 --- a/tests/test_enum.rs +++ b/tests/test_enum.rs @@ -239,6 +239,24 @@ fn test_custom_module() { }); } +#[pyclass(eq)] +#[derive(Debug, Clone, PartialEq)] +pub enum EqOnly { + VariantA, + VariantB, +} + +#[test] +fn test_simple_enum_eq_only() { + Python::with_gil(|py| { + let var1 = Py::new(py, EqOnly::VariantA).unwrap(); + let var2 = Py::new(py, EqOnly::VariantA).unwrap(); + let var3 = Py::new(py, EqOnly::VariantB).unwrap(); + py_assert!(py, var1 var2, "var1 == var2"); + py_assert!(py, var1 var3, "var1 != var3"); + }) +} + #[pyclass(frozen, eq, eq_int, hash)] #[derive(PartialEq, Hash)] enum SimpleEnumWithHash { @@ -298,3 +316,63 @@ fn test_complex_enum_with_hash() { py_assert!(py, *env, "hash(obj) == hsh"); }); } + +#[allow(deprecated)] +mod deprecated { + use crate::py_assert; + use pyo3::prelude::*; + use pyo3::py_run; + + #[pyclass] + #[derive(Debug, PartialEq, Eq, Clone)] + pub enum MyEnum { + Variant, + OtherVariant, + } + + #[test] + fn test_enum_eq_enum() { + Python::with_gil(|py| { + let var1 = Py::new(py, MyEnum::Variant).unwrap(); + let var2 = Py::new(py, MyEnum::Variant).unwrap(); + let other_var = Py::new(py, MyEnum::OtherVariant).unwrap(); + py_assert!(py, var1 var2, "var1 == var2"); + py_assert!(py, var1 other_var, "var1 != other_var"); + py_assert!(py, var1 var2, "(var1 != var2) == False"); + }) + } + + #[test] + fn test_enum_eq_incomparable() { + Python::with_gil(|py| { + let var1 = Py::new(py, MyEnum::Variant).unwrap(); + py_assert!(py, var1, "(var1 == 'foo') == False"); + py_assert!(py, var1, "(var1 != 'foo') == True"); + }) + } + + #[pyclass] + enum CustomDiscriminant { + One = 1, + Two = 2, + } + + #[test] + fn test_custom_discriminant() { + Python::with_gil(|py| { + #[allow(non_snake_case)] + let CustomDiscriminant = py.get_type_bound::(); + let one = Py::new(py, CustomDiscriminant::One).unwrap(); + let two = Py::new(py, CustomDiscriminant::Two).unwrap(); + py_run!(py, CustomDiscriminant one two, r#" + assert CustomDiscriminant.One == one + assert CustomDiscriminant.Two == two + assert CustomDiscriminant.One == 1 + assert CustomDiscriminant.Two == 2 + assert one != two + assert CustomDiscriminant.One != 2 + assert CustomDiscriminant.Two != 1 + "#); + }) + } +} diff --git a/tests/ui/invalid_pyclass_enum.rs b/tests/ui/invalid_pyclass_enum.rs index f4b94a612ff..73bc992571a 100644 --- a/tests/ui/invalid_pyclass_enum.rs +++ b/tests/ui/invalid_pyclass_enum.rs @@ -40,6 +40,12 @@ enum ComplexEqOptRequiresPartialEq { B { msg: String }, } +#[pyclass(eq_int)] +enum SimpleEqIntWithoutEq { + A, + B, +} + #[pyclass(eq_int)] enum NoEqInt { A(i32), diff --git a/tests/ui/invalid_pyclass_enum.stderr b/tests/ui/invalid_pyclass_enum.stderr index d817e6011ff..cfa3922ef62 100644 --- a/tests/ui/invalid_pyclass_enum.stderr +++ b/tests/ui/invalid_pyclass_enum.stderr @@ -30,28 +30,34 @@ error: `constructor` can't be used on a simple enum variant 26 | #[pyo3(constructor = (a, b))] | ^^^^^^^^^^^ -error: `eq_int` can only be used on simple enums. +error: The `eq_int` option requires the `eq` option. --> tests/ui/invalid_pyclass_enum.rs:43:11 | 43 | #[pyclass(eq_int)] | ^^^^^^ +error: `eq_int` can only be used on simple enums. + --> tests/ui/invalid_pyclass_enum.rs:49:11 + | +49 | #[pyclass(eq_int)] + | ^^^^^^ + error: The `hash` option requires the `frozen` option. - --> tests/ui/invalid_pyclass_enum.rs:63:11 + --> tests/ui/invalid_pyclass_enum.rs:69:11 | -63 | #[pyclass(hash)] +69 | #[pyclass(hash)] | ^^^^ error: The `hash` option requires the `eq` option. - --> tests/ui/invalid_pyclass_enum.rs:63:11 + --> tests/ui/invalid_pyclass_enum.rs:69:11 | -63 | #[pyclass(hash)] +69 | #[pyclass(hash)] | ^^^^ error: The `hash` option requires the `eq` option. - --> tests/ui/invalid_pyclass_enum.rs:70:11 + --> tests/ui/invalid_pyclass_enum.rs:76:11 | -70 | #[pyclass(hash)] +76 | #[pyclass(hash)] | ^^^^ error[E0369]: binary operation `==` cannot be applied to type `&SimpleEqOptRequiresPartialEq` @@ -123,25 +129,25 @@ help: consider annotating `ComplexEqOptRequiresPartialEq` with `#[derive(Partial | error[E0277]: the trait bound `SimpleHashOptRequiresHash: Hash` is not satisfied - --> tests/ui/invalid_pyclass_enum.rs:49:31 + --> tests/ui/invalid_pyclass_enum.rs:55:31 | -49 | #[pyclass(frozen, eq, eq_int, hash)] +55 | #[pyclass(frozen, eq, eq_int, hash)] | ^^^^ the trait `Hash` is not implemented for `SimpleHashOptRequiresHash` | help: consider annotating `SimpleHashOptRequiresHash` with `#[derive(Hash)]` | -51 + #[derive(Hash)] -52 | enum SimpleHashOptRequiresHash { +57 + #[derive(Hash)] +58 | enum SimpleHashOptRequiresHash { | error[E0277]: the trait bound `ComplexHashOptRequiresHash: Hash` is not satisfied - --> tests/ui/invalid_pyclass_enum.rs:56:23 + --> tests/ui/invalid_pyclass_enum.rs:62:23 | -56 | #[pyclass(frozen, eq, hash)] +62 | #[pyclass(frozen, eq, hash)] | ^^^^ the trait `Hash` is not implemented for `ComplexHashOptRequiresHash` | help: consider annotating `ComplexHashOptRequiresHash` with `#[derive(Hash)]` | -58 + #[derive(Hash)] -59 | enum ComplexHashOptRequiresHash { +64 + #[derive(Hash)] +65 | enum ComplexHashOptRequiresHash { | From 93ef056711ce1769c6fb95d1f6cc063b34015cb1 Mon Sep 17 00:00:00 2001 From: "A. Cody Schuffelen" Date: Mon, 3 Jun 2024 12:45:36 -0700 Subject: [PATCH 099/495] Use `Ident::parse_any` for `name` attributes (#4226) This makes it possible to use rust keywords as the name of python class methods and standalone functions. For example: ``` struct MyClass { } impl MyClass { #[new] fn new() -> Self { MyClass {} } #[pyo3(name = "struct")] fn struct_method(&self) -> usize { 42 } } fn struct_function() -> usize { 42 } ``` From the [`syn::Ident` documentation](https://docs.rs/syn/2.0.66/syn/struct.Ident.html): > An identifier constructed with `Ident::new` is permitted to be a Rust keyword, though parsing one through its [`Parse`](https://docs.rs/syn/2.0.66/syn/parse/trait.Parse.html) implementation rejects Rust keywords. Use `input.call(Ident::parse_any)` when parsing to match the behaviour of `Ident::new`. Fixes issue #4225 --- newsfragments/4226.fixed.md | 1 + pyo3-macros-backend/src/attributes.rs | 3 ++- tests/test_class_basics.rs | 30 +++++++++++++++++++++++++++ tests/test_pyfunction.rs | 12 +++++++++++ 4 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 newsfragments/4226.fixed.md diff --git a/newsfragments/4226.fixed.md b/newsfragments/4226.fixed.md new file mode 100644 index 00000000000..b2b7d7d12a2 --- /dev/null +++ b/newsfragments/4226.fixed.md @@ -0,0 +1 @@ +Fixes a compile error when declaring a standalone function or class method with a Python name that is a Rust keyword. diff --git a/pyo3-macros-backend/src/attributes.rs b/pyo3-macros-backend/src/attributes.rs index b7ef2ae6718..52479552b34 100644 --- a/pyo3-macros-backend/src/attributes.rs +++ b/pyo3-macros-backend/src/attributes.rs @@ -1,6 +1,7 @@ use proc_macro2::TokenStream; use quote::ToTokens; use syn::{ + ext::IdentExt, parse::{Parse, ParseStream}, punctuated::Punctuated, spanned::Spanned, @@ -72,7 +73,7 @@ pub struct NameLitStr(pub Ident); impl Parse for NameLitStr { fn parse(input: ParseStream<'_>) -> Result { let string_literal: LitStr = input.parse()?; - if let Ok(ident) = string_literal.parse() { + if let Ok(ident) = string_literal.parse_with(Ident::parse_any) { Ok(NameLitStr(ident)) } else { bail_spanned!(string_literal.span() => "expected a single identifier in double quotes") diff --git a/tests/test_class_basics.rs b/tests/test_class_basics.rs index 5b14e8a6121..325b3d52c3d 100644 --- a/tests/test_class_basics.rs +++ b/tests/test_class_basics.rs @@ -123,6 +123,36 @@ fn custom_names() { }); } +#[pyclass(name = "loop")] +struct ClassRustKeywords { + #[pyo3(name = "unsafe", get, set)] + unsafe_variable: usize, +} + +#[pymethods] +impl ClassRustKeywords { + #[pyo3(name = "struct")] + fn struct_method(&self) {} + + #[staticmethod] + #[pyo3(name = "type")] + fn type_method() {} +} + +#[test] +fn keyword_names() { + Python::with_gil(|py| { + let typeobj = py.get_type_bound::(); + py_assert!(py, typeobj, "typeobj.__name__ == 'loop'"); + py_assert!(py, typeobj, "typeobj.struct.__name__ == 'struct'"); + py_assert!(py, typeobj, "typeobj.type.__name__ == 'type'"); + py_assert!(py, typeobj, "typeobj.unsafe.__name__ == 'unsafe'"); + py_assert!(py, typeobj, "not hasattr(typeobj, 'unsafe_variable')"); + py_assert!(py, typeobj, "not hasattr(typeobj, 'struct_method')"); + py_assert!(py, typeobj, "not hasattr(typeobj, 'type_method')"); + }); +} + #[pyclass] struct RawIdents { #[pyo3(get, set)] diff --git a/tests/test_pyfunction.rs b/tests/test_pyfunction.rs index ce4dff7b127..9028f71a232 100644 --- a/tests/test_pyfunction.rs +++ b/tests/test_pyfunction.rs @@ -14,6 +14,18 @@ use pyo3::types::{self, PyCFunction}; #[path = "../src/tests/common.rs"] mod common; +#[pyfunction(name = "struct")] +fn struct_function() {} + +#[test] +fn test_rust_keyword_name() { + Python::with_gil(|py| { + let f = wrap_pyfunction_bound!(struct_function)(py).unwrap(); + + py_assert!(py, f, "f.__name__ == 'struct'"); + }); +} + #[pyfunction(signature = (arg = true))] fn optional_bool(arg: Option) -> String { format!("{:?}", arg) From 37a5f6a94e9dab31575b016a4295fb94322b9aba Mon Sep 17 00:00:00 2001 From: Cheuk Ting Ho Date: Wed, 5 Jun 2024 22:21:44 +0100 Subject: [PATCH 100/495] remove internal APIs from pyo3-ffi (#4201) * remove internal APIs from pyo3-ffi * fix formating * add conditional import * remove _Py_c_neg/abs/pow * fix formating * adding changelog * expose PyAnyMethods::neg/pos/abs and use them * Update src/types/any.rs Co-authored-by: David Hewitt * Update src/types/any.rs Co-authored-by: David Hewitt * Adding details to changelog * update docs * remove PyREsultExt import for GraalPy --------- Co-authored-by: David Hewitt --- newsfragments/4201.changed.md | 1 + src/types/any.rs | 35 ++++++++++++++++++ src/types/complex.rs | 68 ++++++++++++++--------------------- 3 files changed, 62 insertions(+), 42 deletions(-) create mode 100644 newsfragments/4201.changed.md diff --git a/newsfragments/4201.changed.md b/newsfragments/4201.changed.md new file mode 100644 index 00000000000..c236dd27210 --- /dev/null +++ b/newsfragments/4201.changed.md @@ -0,0 +1 @@ +Remove CPython internal ffi call for complex number including: add, sub, mul, div, neg, abs, pow. Added PyAnyMethods::{abs, pos, neg} diff --git a/src/types/any.rs b/src/types/any.rs index ba5ea01b1a3..85e540f9c15 100644 --- a/src/types/any.rs +++ b/src/types/any.rs @@ -1127,6 +1127,21 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed { where O: ToPyObject; + /// Computes the negative of self. + /// + /// Equivalent to the Python expression `-self`. + fn neg(&self) -> PyResult>; + + /// Computes the positive of self. + /// + /// Equivalent to the Python expression `+self`. + fn pos(&self) -> PyResult>; + + /// Computes the absolute of self. + /// + /// Equivalent to the Python expression `abs(self)`. + fn abs(&self) -> PyResult>; + /// Tests whether this object is less than another. /// /// This is equivalent to the Python expression `self < other`. @@ -1862,6 +1877,26 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { inner(self, other.to_object(py).into_bound(py), compare_op) } + fn neg(&self) -> PyResult> { + unsafe { ffi::PyNumber_Negative(self.as_ptr()).assume_owned_or_err(self.py()) } + } + + fn pos(&self) -> PyResult> { + fn inner<'py>(any: &Bound<'py, PyAny>) -> PyResult> { + unsafe { ffi::PyNumber_Positive(any.as_ptr()).assume_owned_or_err(any.py()) } + } + + inner(self) + } + + fn abs(&self) -> PyResult> { + fn inner<'py>(any: &Bound<'py, PyAny>) -> PyResult> { + unsafe { ffi::PyNumber_Absolute(any.as_ptr()).assume_owned_or_err(any.py()) } + } + + inner(self) + } + fn lt(&self, other: O) -> PyResult where O: ToPyObject, diff --git a/src/types/complex.rs b/src/types/complex.rs index 65b08cc9c5c..5ec9e2f00a4 100644 --- a/src/types/complex.rs +++ b/src/types/complex.rs @@ -1,3 +1,5 @@ +#[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))] +use crate::py_result_ext::PyResultExt; #[cfg(feature = "gil-refs")] use crate::PyNativeType; use crate::{ffi, types::any::PyAnyMethods, Bound, PyAny, Python}; @@ -59,7 +61,6 @@ impl PyComplex { #[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))] mod not_limited_impls { - use crate::ffi_ptr_ext::FfiPtrExt; use crate::Borrowed; use super::*; @@ -77,27 +78,17 @@ mod not_limited_impls { } } - #[inline(always)] - pub(super) unsafe fn complex_operation<'py>( - l: Borrowed<'_, 'py, PyComplex>, - r: Borrowed<'_, 'py, PyComplex>, - operation: unsafe extern "C" fn(ffi::Py_complex, ffi::Py_complex) -> ffi::Py_complex, - ) -> *mut ffi::PyObject { - let l_val = (*l.as_ptr().cast::()).cval; - let r_val = (*r.as_ptr().cast::()).cval; - ffi::PyComplex_FromCComplex(operation(l_val, r_val)) - } - macro_rules! bin_ops { - ($trait:ident, $fn:ident, $op:tt, $ffi:path) => { + ($trait:ident, $fn:ident, $op:tt) => { impl<'py> $trait for Borrowed<'_, 'py, PyComplex> { type Output = Bound<'py, PyComplex>; fn $fn(self, other: Self) -> Self::Output { - unsafe { - complex_operation(self, other, $ffi) - .assume_owned(self.py()) - .downcast_into_unchecked() - } + PyAnyMethods::$fn(self.as_any(), other) + .downcast_into().expect( + concat!("Complex method ", + stringify!($fn), + " failed.") + ) } } @@ -139,10 +130,10 @@ mod not_limited_impls { }; } - bin_ops!(Add, add, +, ffi::_Py_c_sum); - bin_ops!(Sub, sub, -, ffi::_Py_c_diff); - bin_ops!(Mul, mul, *, ffi::_Py_c_prod); - bin_ops!(Div, div, /, ffi::_Py_c_quot); + bin_ops!(Add, add, +); + bin_ops!(Sub, sub, -); + bin_ops!(Mul, mul, *); + bin_ops!(Div, div, /); #[cfg(feature = "gil-refs")] impl<'py> Neg for &'py PyComplex { @@ -155,12 +146,9 @@ mod not_limited_impls { impl<'py> Neg for Borrowed<'_, 'py, PyComplex> { type Output = Bound<'py, PyComplex>; fn neg(self) -> Self::Output { - unsafe { - let val = (*self.as_ptr().cast::()).cval; - ffi::PyComplex_FromCComplex(ffi::_Py_c_neg(val)) - .assume_owned(self.py()) - .downcast_into_unchecked() - } + PyAnyMethods::neg(self.as_any()) + .downcast_into() + .expect("Complex method __neg__ failed.") } } @@ -289,24 +277,20 @@ impl<'py> PyComplexMethods<'py> for Bound<'py, PyComplex> { #[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))] fn abs(&self) -> c_double { - unsafe { - let val = (*self.as_ptr().cast::()).cval; - ffi::_Py_c_abs(val) - } + PyAnyMethods::abs(self.as_any()) + .downcast_into() + .expect("Complex method __abs__ failed.") + .extract() + .expect("Failed to extract to c double.") } #[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))] fn pow(&self, other: &Bound<'py, PyComplex>) -> Bound<'py, PyComplex> { - use crate::ffi_ptr_ext::FfiPtrExt; - unsafe { - not_limited_impls::complex_operation( - self.as_borrowed(), - other.as_borrowed(), - ffi::_Py_c_pow, - ) - .assume_owned(self.py()) - .downcast_into_unchecked() - } + Python::with_gil(|py| { + PyAnyMethods::pow(self.as_any(), other, py.None()) + .downcast_into() + .expect("Complex method __pow__ failed.") + }) } } From 11d67b3acc35d6e3999fb011ff97e7e151333b75 Mon Sep 17 00:00:00 2001 From: Thomas Tanon Date: Thu, 6 Jun 2024 09:54:26 +0200 Subject: [PATCH 101/495] Automated module= field creation in declarative modules (#4213) * Automated module= field creation in declarative modules Sets automatically the "module" field of all contained classes and submodules in a declarative module Adds the "module" field to pymodule attributes in order to set the name of the parent modules. By default, the module is assumed to be a root module * fix guide test error --------- Co-authored-by: David Hewitt --- guide/src/module.md | 35 ++++++++++++- newsfragments/4213.added.md | 1 + pyo3-macros-backend/src/module.rs | 84 +++++++++++++++++++++++++++--- pyo3-macros-backend/src/pyclass.rs | 2 +- tests/test_declarative_module.rs | 57 ++++++++++++++++++-- 5 files changed, 165 insertions(+), 14 deletions(-) create mode 100644 newsfragments/4213.added.md diff --git a/guide/src/module.md b/guide/src/module.md index c9c7f78aaf5..51d3ef914f0 100644 --- a/guide/src/module.md +++ b/guide/src/module.md @@ -152,8 +152,39 @@ mod my_extension { # } ``` +The `#[pymodule]` macro automatically sets the `module` attribute of the `#[pyclass]` macros declared inside of it with its name. +For nested modules, the name of the parent module is automatically added. +In the following example, the `Unit` class will have for `module` `my_extension.submodule` because it is properly nested +but the `Ext` class will have for `module` the default `builtins` because it not nested. +```rust +# #[cfg(feature = "experimental-declarative-modules")] +# mod declarative_module_module_attr_test { +use pyo3::prelude::*; + +#[pyclass] +struct Ext; + +#[pymodule] +mod my_extension { + use super::*; + + #[pymodule_export] + use super::Ext; + + #[pymodule] + mod submodule { + use super::*; + // This is a submodule + + #[pyclass] // This will be part of the module + struct Unit; + } +} +# } +``` +It is possible to customize the `module` value for a `#[pymodule]` with the `#[pyo3(module = "MY_MODULE")]` option. + Some changes are planned to this feature before stabilization, like automatically -filling submodules into `sys.modules` to allow easier imports (see [issue #759](https://github.com/PyO3/pyo3/issues/759)) -and filling the `module` argument of inlined `#[pyclass]` automatically with the proper module name. +filling submodules into `sys.modules` to allow easier imports (see [issue #759](https://github.com/PyO3/pyo3/issues/759)). Macro names might also change. See [issue #3900](https://github.com/PyO3/pyo3/issues/3900) to track this feature progress. diff --git a/newsfragments/4213.added.md b/newsfragments/4213.added.md new file mode 100644 index 00000000000..6f553dc93ab --- /dev/null +++ b/newsfragments/4213.added.md @@ -0,0 +1 @@ +Properly fills the `module=` attribute of declarative modules child `#[pymodule]` and `#[pyclass]`. \ No newline at end of file diff --git a/pyo3-macros-backend/src/module.rs b/pyo3-macros-backend/src/module.rs index 756037263e3..71d776bf350 100644 --- a/pyo3-macros-backend/src/module.rs +++ b/pyo3-macros-backend/src/module.rs @@ -1,10 +1,13 @@ //! Code generation for the function that initializes a python module and adds classes and function. -use crate::utils::Ctx; use crate::{ - attributes::{self, take_attributes, take_pyo3_options, CrateAttribute, NameAttribute}, + attributes::{ + self, take_attributes, take_pyo3_options, CrateAttribute, ModuleAttribute, NameAttribute, + }, get_doc, + pyclass::PyClassPyO3Option, pyfunction::{impl_wrap_pyfunction, PyFunctionOptions}, + utils::Ctx, }; use proc_macro2::TokenStream; use quote::quote; @@ -12,15 +15,17 @@ use syn::{ ext::IdentExt, parse::{Parse, ParseStream}, parse_quote, parse_quote_spanned, + punctuated::Punctuated, spanned::Spanned, token::Comma, - Item, Path, Result, + Item, Meta, Path, Result, }; #[derive(Default)] pub struct PyModuleOptions { krate: Option, name: Option, + module: Option, } impl PyModuleOptions { @@ -31,6 +36,7 @@ impl PyModuleOptions { match option { PyModulePyO3Option::Name(name) => options.set_name(name.value.0)?, PyModulePyO3Option::Crate(path) => options.set_crate(path)?, + PyModulePyO3Option::Module(module) => options.set_module(module)?, } } @@ -56,6 +62,16 @@ impl PyModuleOptions { self.krate = Some(path); Ok(()) } + + fn set_module(&mut self, name: ModuleAttribute) -> Result<()> { + ensure_spanned!( + self.module.is_none(), + name.span() => "`module` may only be specified once" + ); + + self.module = Some(name); + Ok(()) + } } pub fn pymodule_module_impl(mut module: syn::ItemMod) -> Result { @@ -77,6 +93,12 @@ pub fn pymodule_module_impl(mut module: syn::ItemMod) -> Result { let ctx = &Ctx::new(&options.krate); let Ctx { pyo3_path } = ctx; let doc = get_doc(attrs, None); + let name = options.name.unwrap_or_else(|| ident.unraw()); + let full_name = if let Some(module) = &options.module { + format!("{}.{}", module.value.value(), name) + } else { + name.to_string() + }; let mut module_items = Vec::new(); let mut module_items_cfg_attrs = Vec::new(); @@ -156,6 +178,13 @@ pub fn pymodule_module_impl(mut module: syn::ItemMod) -> Result { if has_attribute(&item_struct.attrs, "pyclass") { module_items.push(item_struct.ident.clone()); module_items_cfg_attrs.push(get_cfg_attributes(&item_struct.attrs)); + if !has_pyo3_module_declared::( + &item_struct.attrs, + "pyclass", + |option| matches!(option, PyClassPyO3Option::Module(_)), + )? { + set_module_attribute(&mut item_struct.attrs, &full_name); + } } } Item::Enum(item_enum) => { @@ -166,6 +195,13 @@ pub fn pymodule_module_impl(mut module: syn::ItemMod) -> Result { if has_attribute(&item_enum.attrs, "pyclass") { module_items.push(item_enum.ident.clone()); module_items_cfg_attrs.push(get_cfg_attributes(&item_enum.attrs)); + if !has_pyo3_module_declared::( + &item_enum.attrs, + "pyclass", + |option| matches!(option, PyClassPyO3Option::Module(_)), + )? { + set_module_attribute(&mut item_enum.attrs, &full_name); + } } } Item::Mod(item_mod) => { @@ -176,6 +212,13 @@ pub fn pymodule_module_impl(mut module: syn::ItemMod) -> Result { if has_attribute(&item_mod.attrs, "pymodule") { module_items.push(item_mod.ident.clone()); module_items_cfg_attrs.push(get_cfg_attributes(&item_mod.attrs)); + if !has_pyo3_module_declared::( + &item_mod.attrs, + "pymodule", + |option| matches!(option, PyModulePyO3Option::Module(_)), + )? { + set_module_attribute(&mut item_mod.attrs, &full_name); + } } } Item::ForeignMod(item) => { @@ -242,7 +285,7 @@ pub fn pymodule_module_impl(mut module: syn::ItemMod) -> Result { } } - let initialization = module_initialization(options, ident); + let initialization = module_initialization(&name, ctx); Ok(quote!( #vis mod #ident { #(#items)* @@ -286,10 +329,11 @@ pub fn pymodule_function_impl(mut function: syn::ItemFn) -> Result let stmts = std::mem::take(&mut function.block.stmts); let Ctx { pyo3_path } = ctx; let ident = &function.sig.ident; + let name = options.name.unwrap_or_else(|| ident.unraw()); let vis = &function.vis; let doc = get_doc(&function.attrs, None); - let initialization = module_initialization(options, ident); + let initialization = module_initialization(&name, ctx); // Module function called with optional Python<'_> marker as first arg, followed by the module. let mut module_args = Vec::new(); @@ -354,9 +398,7 @@ pub fn pymodule_function_impl(mut function: syn::ItemFn) -> Result }) } -fn module_initialization(options: PyModuleOptions, ident: &syn::Ident) -> TokenStream { - let name = options.name.unwrap_or_else(|| ident.unraw()); - let ctx = &Ctx::new(&options.krate); +fn module_initialization(name: &syn::Ident, ctx: &Ctx) -> TokenStream { let Ctx { pyo3_path } = ctx; let pyinit_symbol = format!("PyInit_{}", name); @@ -491,9 +533,33 @@ fn has_attribute(attrs: &[syn::Attribute], ident: &str) -> bool { attrs.iter().any(|attr| attr.path().is_ident(ident)) } +fn set_module_attribute(attrs: &mut Vec, module_name: &str) { + attrs.push(parse_quote!(#[pyo3(module = #module_name)])); +} + +fn has_pyo3_module_declared( + attrs: &[syn::Attribute], + root_attribute_name: &str, + is_module_option: impl Fn(&T) -> bool + Copy, +) -> Result { + for attr in attrs { + if (attr.path().is_ident("pyo3") || attr.path().is_ident(root_attribute_name)) + && matches!(attr.meta, Meta::List(_)) + { + for option in &attr.parse_args_with(Punctuated::::parse_terminated)? { + if is_module_option(option) { + return Ok(true); + } + } + } + } + Ok(false) +} + enum PyModulePyO3Option { Crate(CrateAttribute), Name(NameAttribute), + Module(ModuleAttribute), } impl Parse for PyModulePyO3Option { @@ -503,6 +569,8 @@ impl Parse for PyModulePyO3Option { input.parse().map(PyModulePyO3Option::Name) } else if lookahead.peek(syn::Token![crate]) { input.parse().map(PyModulePyO3Option::Crate) + } else if lookahead.peek(attributes::kw::module) { + input.parse().map(PyModulePyO3Option::Module) } else { Err(lookahead.error()) } diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index 1e7f29d84c1..717fdfb3dea 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -78,7 +78,7 @@ pub struct PyClassPyO3Options { pub weakref: Option, } -enum PyClassPyO3Option { +pub enum PyClassPyO3Option { Crate(CrateAttribute), Dict(kw::dict), Eq(kw::eq), diff --git a/tests/test_declarative_module.rs b/tests/test_declarative_module.rs index 820cf63806d..0858f84e04a 100644 --- a/tests/test_declarative_module.rs +++ b/tests/test_declarative_module.rs @@ -3,6 +3,7 @@ use pyo3::create_exception; use pyo3::exceptions::PyException; use pyo3::prelude::*; +use pyo3::sync::GILOnceCell; #[cfg(not(Py_LIMITED_API))] use pyo3::types::PyBool; @@ -78,7 +79,7 @@ mod declarative_module { x * 3 } - #[pyclass] + #[pyclass(name = "Struct")] struct Struct; #[pymethods] @@ -89,12 +90,31 @@ mod declarative_module { } } - #[pyclass(eq, eq_int)] + #[pyclass(module = "foo")] + struct StructInCustomModule; + + #[pyclass(eq, eq_int, name = "Enum")] #[derive(PartialEq)] enum Enum { A, B, } + + #[pyclass(eq, eq_int, module = "foo")] + #[derive(PartialEq)] + enum EnumInCustomModule { + A, + B, + } + } + + #[pymodule] + #[pyo3(module = "custom_root")] + mod inner_custom_root { + use super::*; + + #[pyclass] + struct Struct; } #[pymodule_init] @@ -121,10 +141,17 @@ mod declarative_module2 { use super::double; } +fn declarative_module(py: Python<'_>) -> &Bound<'_, PyModule> { + static MODULE: GILOnceCell> = GILOnceCell::new(); + MODULE + .get_or_init(py, || pyo3::wrap_pymodule!(declarative_module)(py)) + .bind(py) +} + #[test] fn test_declarative_module() { Python::with_gil(|py| { - let m = pyo3::wrap_pymodule!(declarative_module)(py).into_bound(py); + let m = declarative_module(py); py_assert!( py, m, @@ -188,3 +215,27 @@ fn test_raw_ident_module() { py_assert!(py, m, "m.double(2) == 4"); }) } + +#[test] +fn test_module_names() { + Python::with_gil(|py| { + let m = declarative_module(py); + py_assert!( + py, + m, + "m.inner.Struct.__module__ == 'declarative_module.inner'" + ); + py_assert!(py, m, "m.inner.StructInCustomModule.__module__ == 'foo'"); + py_assert!( + py, + m, + "m.inner.Enum.__module__ == 'declarative_module.inner'" + ); + py_assert!(py, m, "m.inner.EnumInCustomModule.__module__ == 'foo'"); + py_assert!( + py, + m, + "m.inner_custom_root.Struct.__module__ == 'custom_root.inner_custom_root'" + ); + }) +} From c644c0b0b8c5d685c355eeda49c73103b6a51ba4 Mon Sep 17 00:00:00 2001 From: Adam Reichold Date: Thu, 6 Jun 2024 10:45:16 +0200 Subject: [PATCH 102/495] Lazy-initialize the global reference pool to reduce its overhead when unused (#4178) * Add benchmarks exercising the global reference count decrement pool. * Lazy-initialize the global reference pool to reduce its overhead when unused --- Cargo.toml | 1 + newsfragments/4178.changed.md | 1 + pyo3-benches/benches/bench_pyobject.rs | 106 ++++++++++++++++++++++++- src/gil.rs | 23 ++++-- 4 files changed, 124 insertions(+), 7 deletions(-) create mode 100644 newsfragments/4178.changed.md diff --git a/Cargo.toml b/Cargo.toml index 921e551751c..30b394c344e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ rust-version = "1.63" cfg-if = "1.0" libc = "0.2.62" memoffset = "0.9" +once_cell = "1" # ffi bindings to the python interpreter, split into a separate crate so they can be used independently pyo3-ffi = { path = "pyo3-ffi", version = "=0.22.0-dev" } diff --git a/newsfragments/4178.changed.md b/newsfragments/4178.changed.md new file mode 100644 index 00000000000..a97c1ec8f6e --- /dev/null +++ b/newsfragments/4178.changed.md @@ -0,0 +1 @@ +The global reference pool (to track pending reference count decrements) is now initialized lazily to avoid the overhead of taking a mutex upon function entry when the functionality is not actually used. diff --git a/pyo3-benches/benches/bench_pyobject.rs b/pyo3-benches/benches/bench_pyobject.rs index af25d61ce6a..a57a98a8d30 100644 --- a/pyo3-benches/benches/bench_pyobject.rs +++ b/pyo3-benches/benches/bench_pyobject.rs @@ -1,4 +1,12 @@ -use codspeed_criterion_compat::{criterion_group, criterion_main, Bencher, Criterion}; +use codspeed_criterion_compat::{criterion_group, criterion_main, BatchSize, Bencher, Criterion}; + +use std::sync::{ + atomic::{AtomicUsize, Ordering}, + mpsc::channel, + Arc, Barrier, +}; +use std::thread::spawn; +use std::time::{Duration, Instant}; use pyo3::prelude::*; @@ -6,14 +14,108 @@ fn drop_many_objects(b: &mut Bencher<'_>) { Python::with_gil(|py| { b.iter(|| { for _ in 0..1000 { - std::mem::drop(py.None()); + drop(py.None()); } }); }); } +fn drop_many_objects_without_gil(b: &mut Bencher<'_>) { + b.iter_batched( + || { + Python::with_gil(|py| { + (0..1000) + .map(|_| py.None().into_py(py)) + .collect::>() + }) + }, + |objs| { + drop(objs); + + Python::with_gil(|_py| ()); + }, + BatchSize::SmallInput, + ); +} + +fn drop_many_objects_multiple_threads(b: &mut Bencher<'_>) { + const THREADS: usize = 5; + + let barrier = Arc::new(Barrier::new(1 + THREADS)); + + let done = Arc::new(AtomicUsize::new(0)); + + let sender = (0..THREADS) + .map(|_| { + let (sender, receiver) = channel(); + + let barrier = barrier.clone(); + + let done = done.clone(); + + spawn(move || { + for objs in receiver { + barrier.wait(); + + drop(objs); + + done.fetch_add(1, Ordering::AcqRel); + } + }); + + sender + }) + .collect::>(); + + b.iter_custom(|iters| { + let mut duration = Duration::ZERO; + + let mut last_done = done.load(Ordering::Acquire); + + for _ in 0..iters { + for sender in &sender { + let objs = Python::with_gil(|py| { + (0..1000 / THREADS) + .map(|_| py.None().into_py(py)) + .collect::>() + }); + + sender.send(objs).unwrap(); + } + + barrier.wait(); + + let start = Instant::now(); + + loop { + Python::with_gil(|_py| ()); + + let done = done.load(Ordering::Acquire); + if done - last_done == THREADS { + last_done = done; + break; + } + } + + Python::with_gil(|_py| ()); + + duration += start.elapsed(); + } + + duration + }); +} + fn criterion_benchmark(c: &mut Criterion) { c.bench_function("drop_many_objects", drop_many_objects); + c.bench_function( + "drop_many_objects_without_gil", + drop_many_objects_without_gil, + ); + c.bench_function( + "drop_many_objects_multiple_threads", + drop_many_objects_multiple_threads, + ); } criterion_group!(benches, criterion_benchmark); diff --git a/src/gil.rs b/src/gil.rs index 3e25c7cfb0f..ec20fc64c34 100644 --- a/src/gil.rs +++ b/src/gil.rs @@ -5,6 +5,8 @@ use crate::impl_::not_send::{NotSend, NOT_SEND}; #[cfg(pyo3_disable_reference_pool)] use crate::impl_::panic::PanicTrap; use crate::{ffi, Python}; +#[cfg(not(pyo3_disable_reference_pool))] +use once_cell::sync::Lazy; use std::cell::Cell; #[cfg(all(feature = "gil-refs", debug_assertions))] use std::cell::RefCell; @@ -227,7 +229,9 @@ impl GILGuard { let pool = mem::ManuallyDrop::new(GILPool::new()); #[cfg(not(pyo3_disable_reference_pool))] - POOL.update_counts(Python::assume_gil_acquired()); + if let Some(pool) = Lazy::get(&POOL) { + pool.update_counts(Python::assume_gil_acquired()); + } GILGuard::Ensured { gstate, #[cfg(feature = "gil-refs")] @@ -240,7 +244,9 @@ impl GILGuard { increment_gil_count(); let guard = GILGuard::Assumed; #[cfg(not(pyo3_disable_reference_pool))] - POOL.update_counts(guard.python()); + if let Some(pool) = Lazy::get(&POOL) { + pool.update_counts(guard.python()); + } guard } @@ -307,11 +313,14 @@ impl ReferencePool { } } +#[cfg(not(pyo3_disable_reference_pool))] +unsafe impl Send for ReferencePool {} + #[cfg(not(pyo3_disable_reference_pool))] unsafe impl Sync for ReferencePool {} #[cfg(not(pyo3_disable_reference_pool))] -static POOL: ReferencePool = ReferencePool::new(); +static POOL: Lazy = Lazy::new(ReferencePool::new); /// A guard which can be used to temporarily release the GIL and restore on `Drop`. pub(crate) struct SuspendGIL { @@ -336,7 +345,9 @@ impl Drop for SuspendGIL { // Update counts of PyObjects / Py that were cloned or dropped while the GIL was released. #[cfg(not(pyo3_disable_reference_pool))] - POOL.update_counts(Python::assume_gil_acquired()); + if let Some(pool) = Lazy::get(&POOL) { + pool.update_counts(Python::assume_gil_acquired()); + } } } } @@ -409,7 +420,9 @@ impl GILPool { pub unsafe fn new() -> GILPool { // Update counts of PyObjects / Py that have been cloned or dropped since last acquisition #[cfg(not(pyo3_disable_reference_pool))] - POOL.update_counts(Python::assume_gil_acquired()); + if let Some(pool) = Lazy::get(&POOL) { + pool.update_counts(Python::assume_gil_acquired()); + } GILPool { start: OWNED_OBJECTS .try_with(|owned_objects| { From 74619143b61273212054181289bbfcc8cfb8f90e Mon Sep 17 00:00:00 2001 From: Thomas Tanon Date: Thu, 6 Jun 2024 23:19:37 +0200 Subject: [PATCH 103/495] Declarative modules: make sure to emit doc comments and other attributes (#4236) * Declarative modules: make sure to emmit doc comments and other attributes * Adds a test * Apply suggestions from code review --------- Co-authored-by: David Hewitt --- newsfragments/4236.fixed.md | 1 + pyo3-macros-backend/src/module.rs | 1 + tests/ui/pymodule_missing_docs.rs | 5 +++++ 3 files changed, 7 insertions(+) create mode 100644 newsfragments/4236.fixed.md diff --git a/newsfragments/4236.fixed.md b/newsfragments/4236.fixed.md new file mode 100644 index 00000000000..f87d16941c7 --- /dev/null +++ b/newsfragments/4236.fixed.md @@ -0,0 +1 @@ +Declarative modules: do not discard doc comments on the `mod` node. \ No newline at end of file diff --git a/pyo3-macros-backend/src/module.rs b/pyo3-macros-backend/src/module.rs index 71d776bf350..c1e46276544 100644 --- a/pyo3-macros-backend/src/module.rs +++ b/pyo3-macros-backend/src/module.rs @@ -287,6 +287,7 @@ pub fn pymodule_module_impl(mut module: syn::ItemMod) -> Result { let initialization = module_initialization(&name, ctx); Ok(quote!( + #(#attrs)* #vis mod #ident { #(#items)* diff --git a/tests/ui/pymodule_missing_docs.rs b/tests/ui/pymodule_missing_docs.rs index 1b196fa65e0..ed7d772fafd 100644 --- a/tests/ui/pymodule_missing_docs.rs +++ b/tests/ui/pymodule_missing_docs.rs @@ -9,4 +9,9 @@ pub fn python_module(_m: &Bound<'_, PyModule>) -> PyResult<()> { Ok(()) } +#[cfg(feature = "experimental-declarative-modules")] +/// Some module documentation +#[pymodule] +pub mod declarative_python_module {} + fn main() {} From fbb6f20c2bafbc6d709a32871331e87b64cbea1c Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Fri, 7 Jun 2024 15:26:36 +0100 Subject: [PATCH 104/495] migration: close all 0.20->0.21 items (#4238) --- guide/src/migration.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/guide/src/migration.md b/guide/src/migration.md index 31e912a6856..77f97cf01d2 100644 --- a/guide/src/migration.md +++ b/guide/src/migration.md @@ -82,7 +82,7 @@ enum SimpleEnum {
## from 0.20.* to 0.21 -
+
Click to expand PyO3 0.21 introduces a new `Bound<'py, T>` smart pointer which replaces the existing "GIL Refs" API to interact with Python objects. For example, in PyO3 0.20 the reference `&'py PyAny` would be used to interact with Python objects. In PyO3 0.21 the updated type is `Bound<'py, PyAny>`. Making this change moves Rust ownership semantics out of PyO3's internals and into user code. This change fixes [a known soundness edge case of interaction with gevent](https://github.com/PyO3/pyo3/issues/3668) as well as improves CPU and [memory performance](https://github.com/PyO3/pyo3/issues/1056). For a full history of discussion see https://github.com/PyO3/pyo3/issues/3382. @@ -100,7 +100,7 @@ The following sections are laid out in this order.
### Enable the `gil-refs` feature -
+
Click to expand To make the transition for the PyO3 ecosystem away from the GIL Refs API as smooth as possible, in PyO3 0.21 no APIs consuming or producing GIL Refs have been altered. Instead, variants using `Bound` smart pointers have been introduced, for example `PyTuple::new_bound` which returns `Bound` is the replacement form of `PyTuple::new`. The GIL Ref APIs have been deprecated, but to make migration easier it is possible to disable these deprecation warnings by enabling the `gil-refs` feature. @@ -127,7 +127,7 @@ pyo3 = { version = "0.21", features = ["gil-refs"] }
### `PyTypeInfo` and `PyTryFrom` have been adjusted -
+
Click to expand The `PyTryFrom` trait has aged poorly, its `try_from` method now conflicts with `TryFrom::try_from` in the 2021 edition prelude. A lot of its functionality was also duplicated with `PyTypeInfo`. @@ -170,7 +170,7 @@ Python::with_gil(|py| {
### `Iter(A)NextOutput` are deprecated -
+
Click to expand The `__next__` and `__anext__` magic methods can now return any type convertible into Python objects directly just like all other `#[pymethods]`. The `IterNextOutput` used by `__next__` and `IterANextOutput` used by `__anext__` are subsequently deprecated. Most importantly, this change allows returning an awaitable from `__anext__` without non-sensically wrapping it into `Yield` or `Some`. Only the return types `Option` and `Result, E>` are still handled in a special manner where `Some(val)` yields `val` and `None` stops iteration. @@ -292,21 +292,21 @@ impl PyClassAsyncIter {
### `PyType::name` has been renamed to `PyType::qualname` -
+
Click to expand `PyType::name` has been renamed to `PyType::qualname` to indicate that it does indeed return the [qualified name](https://docs.python.org/3/glossary.html#term-qualified-name), matching the `__qualname__` attribute. The newly added `PyType::name` yields the full name including the module name now which corresponds to `__module__.__name__` on the level of attributes.
### `PyCell` has been deprecated -
+
Click to expand Interactions with Python objects implemented in Rust no longer need to go though `PyCell`. Instead iteractions with Python object now consistently go through `Bound` or `Py` independently of whether `T` is native Python object or a `#[pyclass]` implemented in Rust. Use `Bound::new` or `Py::new` respectively to create and `Bound::borrow(_mut)` / `Py::borrow(_mut)` to borrow the Rust object.
### Migrating from the GIL Refs API to `Bound` -
+
Click to expand To minimise breakage of code using the GIL Refs API, the `Bound` smart pointer has been introduced by adding complements to all functions which accept or return GIL Refs. This allows code to migrate by replacing the deprecated APIs with the new ones. @@ -404,7 +404,7 @@ Despite a large amount of deprecations warnings produced by PyO3 to aid with the
### Deactivating the `gil-refs` feature -
+
Click to expand As a final step of migration, deactivating the `gil-refs` feature will set up code for best performance and is intended to set up a forward-compatible API for PyO3 0.22. From b8fb367582f2746323a35fa199cc8a9f1a14c2cc Mon Sep 17 00:00:00 2001 From: Michael Gilbert Date: Fri, 7 Jun 2024 12:08:53 -0700 Subject: [PATCH 105/495] feat: Add 'ord' option for PyClass and corresponding tests (#4202) * feat: Add 'ord' option for PyClass and corresponding tests Updated the macros back-end to include 'ord' as an option for PyClass allowing for Python-style ordering comparison of enum variants. Additionally, test cases to verify the proper functioning of this new feature have been introduced. * update: fix formatting with cargo fmt * update: documented added feature in newsfragments * update: updated saved errors for comparison test for invalid pyclass args * update: removed nested match arms and extended cases for ordering instead * update: alphabetically ordered entries * update: added section to class documentation with example for using ord argument. * refactor: reduced duplication of code using closure to process tokens. * update: used ensure_spanned macro to emit compile time errors for uses of ord on complex enums or structs, updated test errors for bad compile cases * fix: remove errant character * update: added note about PartialOrd being required. * feat: implemented ordering for structs and complex enums. Retained the equality logic for simple enums until PartialEq is deprecated. * update: adjusted compile time error checks for missing PartialOrd implementations. Refactored growing set of comparison tests for simple and complex enums and structs into separate test file. * fix: updated with clippy findings * update: added not to pyclass parameters on ord (assumes that eq will be implemented and merged first) * update: rebased on main after merging of `eq` feature * update: format update * update: update all test output and doc tests * Update guide/src/class.md Co-authored-by: Icxolu <10486322+Icxolu@users.noreply.github.com> * Update pyo3-macros-backend/src/pyclass.rs Co-authored-by: Icxolu <10486322+Icxolu@users.noreply.github.com> * Update newsfragments/4202.added.md Co-authored-by: Icxolu <10486322+Icxolu@users.noreply.github.com> * Update guide/pyclass-parameters.md Co-authored-by: Icxolu <10486322+Icxolu@users.noreply.github.com> * update: added note about `ord` implementation with example. * fix doc formatting --------- Co-authored-by: Michael Gilbert Co-authored-by: Icxolu <10486322+Icxolu@users.noreply.github.com> --- guide/pyclass-parameters.md | 1 + guide/src/class.md | 26 +++ guide/src/class/object.md | 10 + newsfragments/4202.added.md | 1 + pyo3-macros-backend/src/attributes.rs | 1 + pyo3-macros-backend/src/pyclass.rs | 48 ++++- tests/test_class_comparisons.rs | 287 ++++++++++++++++++++++++++ tests/test_enum.rs | 21 -- tests/ui/invalid_pyclass_args.rs | 5 + tests/ui/invalid_pyclass_args.stderr | 10 +- tests/ui/invalid_pyclass_enum.rs | 13 ++ tests/ui/invalid_pyclass_enum.stderr | 74 +++++++ 12 files changed, 465 insertions(+), 32 deletions(-) create mode 100644 newsfragments/4202.added.md create mode 100644 tests/test_class_comparisons.rs diff --git a/guide/pyclass-parameters.md b/guide/pyclass-parameters.md index 1756e8dfb35..a3a4e1f0c7d 100644 --- a/guide/pyclass-parameters.md +++ b/guide/pyclass-parameters.md @@ -15,6 +15,7 @@ | `mapping` | Inform PyO3 that this class is a [`Mapping`][params-mapping], and so leave its implementation of sequence C-API slots empty. | | `module = "module_name"` | Python code will see the class as being defined in this module. Defaults to `builtins`. | | `name = "python_name"` | Sets the name that Python sees this class as. Defaults to the name of the Rust struct. | +| `ord` | Implements `__lt__`, `__gt__`, `__le__`, & `__ge__` using the `PartialOrd` implementation of the underlying Rust datatype. *Requires `eq`* | | `rename_all = "renaming_rule"` | Applies renaming rules to every getters and setters of a struct, or every variants of an enum. Possible values are: "camelCase", "kebab-case", "lowercase", "PascalCase", "SCREAMING-KEBAB-CASE", "SCREAMING_SNAKE_CASE", "snake_case", "UPPERCASE". | | `sequence` | Inform PyO3 that this class is a [`Sequence`][params-sequence], and so leave its C-API mapping length slot empty. | | `set_all` | Generates setters for all fields of the pyclass. | diff --git a/guide/src/class.md b/guide/src/class.md index b72cae34e25..2bcfe75911e 100644 --- a/guide/src/class.md +++ b/guide/src/class.md @@ -1160,6 +1160,32 @@ Python::with_gil(|py| { }) ``` +Ordering of enum variants is optionally added using `#[pyo3(ord)]`. +*Note: Implementation of the `PartialOrd` trait is required when passing the `ord` argument. If not implemented, a compile time error is raised.* + +```rust +# use pyo3::prelude::*; +#[pyclass(eq, ord)] +#[derive(PartialEq, PartialOrd)] +enum MyEnum{ + A, + B, + C, +} + +Python::with_gil(|py| { + let cls = py.get_type_bound::(); + let a = Py::new(py, MyEnum::A).unwrap(); + let b = Py::new(py, MyEnum::B).unwrap(); + let c = Py::new(py, MyEnum::C).unwrap(); + pyo3::py_run!(py, cls a b c, r#" + assert (a < b) == True + assert (c <= b) == False + assert (c > a) == True + "#) +}) +``` + You may not use enums as a base class or let enums inherit from other classes. ```rust,compile_fail diff --git a/guide/src/class/object.md b/guide/src/class/object.md index 3b775c2b438..c0d25cd0597 100644 --- a/guide/src/class/object.md +++ b/guide/src/class/object.md @@ -249,6 +249,16 @@ To implement `__eq__` using the Rust [`PartialEq`] trait implementation, the `eq struct Number(i32); ``` +To implement `__lt__`, `__le__`, `__gt__`, & `__ge__` using the Rust `PartialOrd` trait implementation, the `ord` option can be used. *Note: Requires `eq`.* + +```rust +# use pyo3::prelude::*; +# +#[pyclass(eq, ord)] +#[derive(PartialEq, PartialOrd)] +struct Number(i32); +``` + ### Truthyness We'll consider `Number` to be `True` if it is nonzero: diff --git a/newsfragments/4202.added.md b/newsfragments/4202.added.md new file mode 100644 index 00000000000..d15b6ce810c --- /dev/null +++ b/newsfragments/4202.added.md @@ -0,0 +1 @@ +Add `#[pyclass(ord)]` to implement ordering based on `PartialOrd`. \ No newline at end of file diff --git a/pyo3-macros-backend/src/attributes.rs b/pyo3-macros-backend/src/attributes.rs index 52479552b34..02af17b618b 100644 --- a/pyo3-macros-backend/src/attributes.rs +++ b/pyo3-macros-backend/src/attributes.rs @@ -29,6 +29,7 @@ pub mod kw { syn::custom_keyword!(mapping); syn::custom_keyword!(module); syn::custom_keyword!(name); + syn::custom_keyword!(ord); syn::custom_keyword!(pass_module); syn::custom_keyword!(rename_all); syn::custom_keyword!(sequence); diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index 717fdfb3dea..403e5e8e9dc 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -70,6 +70,7 @@ pub struct PyClassPyO3Options { pub mapping: Option, pub module: Option, pub name: Option, + pub ord: Option, pub rename_all: Option, pub sequence: Option, pub set_all: Option, @@ -91,6 +92,7 @@ pub enum PyClassPyO3Option { Mapping(kw::mapping), Module(ModuleAttribute), Name(NameAttribute), + Ord(kw::ord), RenameAll(RenameAllAttribute), Sequence(kw::sequence), SetAll(kw::set_all), @@ -126,6 +128,8 @@ impl Parse for PyClassPyO3Option { input.parse().map(PyClassPyO3Option::Module) } else if lookahead.peek(kw::name) { input.parse().map(PyClassPyO3Option::Name) + } else if lookahead.peek(attributes::kw::ord) { + input.parse().map(PyClassPyO3Option::Ord) } else if lookahead.peek(kw::rename_all) { input.parse().map(PyClassPyO3Option::RenameAll) } else if lookahead.peek(attributes::kw::sequence) { @@ -189,6 +193,7 @@ impl PyClassPyO3Options { PyClassPyO3Option::Mapping(mapping) => set_option!(mapping), PyClassPyO3Option::Module(module) => set_option!(module), PyClassPyO3Option::Name(name) => set_option!(name), + PyClassPyO3Option::Ord(ord) => set_option!(ord), PyClassPyO3Option::RenameAll(rename_all) => set_option!(rename_all), PyClassPyO3Option::Sequence(sequence) => set_option!(sequence), PyClassPyO3Option::SetAll(set_all) => set_option!(set_all), @@ -1665,7 +1670,10 @@ fn impl_pytypeinfo( } } -fn pyclass_richcmp_arms(options: &PyClassPyO3Options, ctx: &Ctx) -> TokenStream { +fn pyclass_richcmp_arms( + options: &PyClassPyO3Options, + ctx: &Ctx, +) -> std::result::Result { let Ctx { pyo3_path } = ctx; let eq_arms = options @@ -1684,9 +1692,34 @@ fn pyclass_richcmp_arms(options: &PyClassPyO3Options, ctx: &Ctx) -> TokenStream }) .unwrap_or_default(); - // TODO: `ord` can be integrated here (#4202) - #[allow(clippy::let_and_return)] - eq_arms + if let Some(ord) = options.ord { + ensure_spanned!(options.eq.is_some(), ord.span() => "The `ord` option requires the `eq` option."); + } + + let ord_arms = options + .ord + .map(|ord| { + quote_spanned! { ord.span() => + #pyo3_path::pyclass::CompareOp::Gt => { + ::std::result::Result::Ok(#pyo3_path::conversion::IntoPy::into_py(self_val > other, py)) + }, + #pyo3_path::pyclass::CompareOp::Lt => { + ::std::result::Result::Ok(#pyo3_path::conversion::IntoPy::into_py(self_val < other, py)) + }, + #pyo3_path::pyclass::CompareOp::Le => { + ::std::result::Result::Ok(#pyo3_path::conversion::IntoPy::into_py(self_val <= other, py)) + }, + #pyo3_path::pyclass::CompareOp::Ge => { + ::std::result::Result::Ok(#pyo3_path::conversion::IntoPy::into_py(self_val >= other, py)) + }, + } + }) + .unwrap_or_else(|| quote! { _ => ::std::result::Result::Ok(py.NotImplemented()) }); + + Ok(quote! { + #eq_arms + #ord_arms + }) } fn pyclass_richcmp_simple_enum( @@ -1723,7 +1756,7 @@ fn pyclass_richcmp_simple_enum( return Ok((None, None)); } - let arms = pyclass_richcmp_arms(&options, ctx); + let arms = pyclass_richcmp_arms(&options, ctx)?; let eq = options.eq.map(|eq| { quote_spanned! { eq.span() => @@ -1732,7 +1765,6 @@ fn pyclass_richcmp_simple_enum( let other = &*other.borrow(); return match op { #arms - _ => ::std::result::Result::Ok(py.NotImplemented()) } } } @@ -1746,7 +1778,6 @@ fn pyclass_richcmp_simple_enum( }) { return match op { #arms - _ => ::std::result::Result::Ok(py.NotImplemented()) } } } @@ -1786,7 +1817,7 @@ fn pyclass_richcmp( bail_spanned!(eq_int.span() => "`eq_int` can only be used on simple enums.") } - let arms = pyclass_richcmp_arms(options, ctx); + let arms = pyclass_richcmp_arms(options, ctx)?; if options.eq.is_some() { let mut richcmp_impl = parse_quote! { fn __pyo3__generated____richcmp__( @@ -1799,7 +1830,6 @@ fn pyclass_richcmp( let other = &*#pyo3_path::types::PyAnyMethods::downcast::(other)?.borrow(); match op { #arms - _ => ::std::result::Result::Ok(py.NotImplemented()) } } }; diff --git a/tests/test_class_comparisons.rs b/tests/test_class_comparisons.rs new file mode 100644 index 00000000000..75bd6251aac --- /dev/null +++ b/tests/test_class_comparisons.rs @@ -0,0 +1,287 @@ +#![cfg(feature = "macros")] + +use pyo3::prelude::*; + +#[path = "../src/tests/common.rs"] +mod common; + +#[pyclass(eq)] +#[derive(Debug, Clone, PartialEq)] +pub enum MyEnum { + Variant, + OtherVariant, +} + +#[pyclass(eq, ord)] +#[derive(Debug, PartialEq, Eq, Clone, PartialOrd)] +pub enum MyEnumOrd { + Variant, + OtherVariant, +} + +#[test] +fn test_enum_eq_enum() { + Python::with_gil(|py| { + let var1 = Py::new(py, MyEnum::Variant).unwrap(); + let var2 = Py::new(py, MyEnum::Variant).unwrap(); + let other_var = Py::new(py, MyEnum::OtherVariant).unwrap(); + py_assert!(py, var1 var2, "var1 == var2"); + py_assert!(py, var1 other_var, "var1 != other_var"); + py_assert!(py, var1 var2, "(var1 != var2) == False"); + }) +} + +#[test] +fn test_enum_eq_incomparable() { + Python::with_gil(|py| { + let var1 = Py::new(py, MyEnum::Variant).unwrap(); + py_assert!(py, var1, "(var1 == 'foo') == False"); + py_assert!(py, var1, "(var1 != 'foo') == True"); + }) +} + +#[test] +fn test_enum_ord_comparable_opt_in_only() { + Python::with_gil(|py| { + let var1 = Py::new(py, MyEnum::Variant).unwrap(); + let var2 = Py::new(py, MyEnum::OtherVariant).unwrap(); + // ordering on simple enums if opt in only, thus raising an error below + py_expect_exception!(py, var1 var2, "(var1 > var2) == False", PyTypeError); + }) +} + +#[test] +fn test_simple_enum_ord_comparable() { + Python::with_gil(|py| { + let var1 = Py::new(py, MyEnumOrd::Variant).unwrap(); + let var2 = Py::new(py, MyEnumOrd::OtherVariant).unwrap(); + let var3 = Py::new(py, MyEnumOrd::OtherVariant).unwrap(); + py_assert!(py, var1 var2, "(var1 > var2) == False"); + py_assert!(py, var1 var2, "(var1 < var2) == True"); + py_assert!(py, var1 var2, "(var1 >= var2) == False"); + py_assert!(py, var2 var3, "(var3 >= var2) == True"); + py_assert!(py, var1 var2, "(var1 <= var2) == True"); + }) +} + +#[pyclass(eq, ord)] +#[derive(Debug, PartialEq, Eq, Clone, PartialOrd)] +pub enum MyComplexEnumOrd { + Variant(i32), + OtherVariant(String), +} + +#[pyclass(eq, ord)] +#[derive(Debug, PartialEq, Eq, Clone, PartialOrd)] +pub enum MyComplexEnumOrd2 { + Variant { msg: String, idx: u32 }, + OtherVariant { name: String, idx: u32 }, +} + +#[test] +fn test_complex_enum_ord_comparable() { + Python::with_gil(|py| { + let var1 = Py::new(py, MyComplexEnumOrd::Variant(-2)).unwrap(); + let var2 = Py::new(py, MyComplexEnumOrd::Variant(5)).unwrap(); + let var3 = Py::new(py, MyComplexEnumOrd::OtherVariant("a".to_string())).unwrap(); + let var4 = Py::new(py, MyComplexEnumOrd::OtherVariant("b".to_string())).unwrap(); + py_assert!(py, var1 var2, "(var1 > var2) == False"); + py_assert!(py, var1 var2, "(var1 < var2) == True"); + py_assert!(py, var1 var2, "(var1 >= var2) == False"); + py_assert!(py, var1 var2, "(var1 <= var2) == True"); + + py_assert!(py, var1 var3, "(var1 >= var3) == False"); + py_assert!(py, var1 var3, "(var1 <= var3) == True"); + + py_assert!(py, var3 var4, "(var3 >= var4) == False"); + py_assert!(py, var3 var4, "(var3 <= var4) == True"); + + let var5 = Py::new( + py, + MyComplexEnumOrd2::Variant { + msg: "hello".to_string(), + idx: 1, + }, + ) + .unwrap(); + let var6 = Py::new( + py, + MyComplexEnumOrd2::Variant { + msg: "hello".to_string(), + idx: 1, + }, + ) + .unwrap(); + let var7 = Py::new( + py, + MyComplexEnumOrd2::Variant { + msg: "goodbye".to_string(), + idx: 7, + }, + ) + .unwrap(); + let var8 = Py::new( + py, + MyComplexEnumOrd2::Variant { + msg: "about".to_string(), + idx: 0, + }, + ) + .unwrap(); + let var9 = Py::new( + py, + MyComplexEnumOrd2::OtherVariant { + name: "albert".to_string(), + idx: 1, + }, + ) + .unwrap(); + + py_assert!(py, var5 var6, "(var5 == var6) == True"); + py_assert!(py, var5 var6, "(var5 <= var6) == True"); + py_assert!(py, var6 var7, "(var6 <= var7) == False"); + py_assert!(py, var6 var7, "(var6 >= var7) == True"); + py_assert!(py, var5 var8, "(var5 > var8) == True"); + py_assert!(py, var8 var9, "(var9 > var8) == True"); + }) +} + +#[pyclass(eq, ord)] +#[derive(Debug, PartialEq, Eq, Clone, PartialOrd)] +pub struct Point { + x: i32, + y: i32, + z: i32, +} + +#[test] +fn test_struct_numeric_ord_comparable() { + Python::with_gil(|py| { + let var1 = Py::new(py, Point { x: 10, y: 2, z: 3 }).unwrap(); + let var2 = Py::new(py, Point { x: 2, y: 2, z: 3 }).unwrap(); + let var3 = Py::new(py, Point { x: 1, y: 22, z: 4 }).unwrap(); + let var4 = Py::new(py, Point { x: 1, y: 3, z: 4 }).unwrap(); + let var5 = Py::new(py, Point { x: 1, y: 3, z: 4 }).unwrap(); + py_assert!(py, var1 var2, "(var1 > var2) == True"); + py_assert!(py, var1 var2, "(var1 <= var2) == False"); + py_assert!(py, var2 var3, "(var3 < var2) == True"); + py_assert!(py, var3 var4, "(var3 > var4) == True"); + py_assert!(py, var4 var5, "(var4 == var5) == True"); + py_assert!(py, var3 var5, "(var3 != var5) == True"); + }) +} + +#[pyclass(eq, ord)] +#[derive(Debug, PartialEq, Eq, Clone, PartialOrd)] +pub struct Person { + surname: String, + given_name: String, +} + +#[test] +fn test_struct_string_ord_comparable() { + Python::with_gil(|py| { + let var1 = Py::new( + py, + Person { + surname: "zzz".to_string(), + given_name: "bob".to_string(), + }, + ) + .unwrap(); + let var2 = Py::new( + py, + Person { + surname: "aaa".to_string(), + given_name: "sally".to_string(), + }, + ) + .unwrap(); + let var3 = Py::new( + py, + Person { + surname: "eee".to_string(), + given_name: "qqq".to_string(), + }, + ) + .unwrap(); + let var4 = Py::new( + py, + Person { + surname: "ddd".to_string(), + given_name: "aaa".to_string(), + }, + ) + .unwrap(); + + py_assert!(py, var1 var2, "(var1 > var2) == True"); + py_assert!(py, var1 var2, "(var1 <= var2) == False"); + py_assert!(py, var1 var3, "(var1 >= var3) == True"); + py_assert!(py, var2 var3, "(var2 >= var3) == False"); + py_assert!(py, var3 var4, "(var3 >= var4) == True"); + py_assert!(py, var3 var4, "(var3 != var4) == True"); + }) +} + +#[pyclass(eq, ord)] +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct Record { + name: String, + title: String, + idx: u32, +} + +impl PartialOrd for Record { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.idx.partial_cmp(&other.idx).unwrap()) + } +} + +#[test] +fn test_struct_custom_ord_comparable() { + Python::with_gil(|py| { + let var1 = Py::new( + py, + Record { + name: "zzz".to_string(), + title: "bbb".to_string(), + idx: 9, + }, + ) + .unwrap(); + let var2 = Py::new( + py, + Record { + name: "ddd".to_string(), + title: "aaa".to_string(), + idx: 1, + }, + ) + .unwrap(); + let var3 = Py::new( + py, + Record { + name: "vvv".to_string(), + title: "ggg".to_string(), + idx: 19, + }, + ) + .unwrap(); + let var4 = Py::new( + py, + Record { + name: "vvv".to_string(), + title: "ggg".to_string(), + idx: 19, + }, + ) + .unwrap(); + + py_assert!(py, var1 var2, "(var1 > var2) == True"); + py_assert!(py, var1 var2, "(var1 <= var2) == False"); + py_assert!(py, var1 var3, "(var1 >= var3) == False"); + py_assert!(py, var2 var3, "(var2 >= var3) == False"); + py_assert!(py, var3 var4, "(var3 == var4) == True"); + py_assert!(py, var2 var4, "(var2 != var4) == True"); + }) +} diff --git a/tests/test_enum.rs b/tests/test_enum.rs index 4b72143539f..96a07c3fe41 100644 --- a/tests/test_enum.rs +++ b/tests/test_enum.rs @@ -52,27 +52,6 @@ fn test_enum_arg() { }) } -#[test] -fn test_enum_eq_enum() { - Python::with_gil(|py| { - let var1 = Py::new(py, MyEnum::Variant).unwrap(); - let var2 = Py::new(py, MyEnum::Variant).unwrap(); - let other_var = Py::new(py, MyEnum::OtherVariant).unwrap(); - py_assert!(py, var1 var2, "var1 == var2"); - py_assert!(py, var1 other_var, "var1 != other_var"); - py_assert!(py, var1 var2, "(var1 != var2) == False"); - }) -} - -#[test] -fn test_enum_eq_incomparable() { - Python::with_gil(|py| { - let var1 = Py::new(py, MyEnum::Variant).unwrap(); - py_assert!(py, var1, "(var1 == 'foo') == False"); - py_assert!(py, var1, "(var1 != 'foo') == True"); - }) -} - #[pyclass(eq, eq_int)] #[derive(Debug, PartialEq, Eq, Clone)] enum CustomDiscriminant { diff --git a/tests/ui/invalid_pyclass_args.rs b/tests/ui/invalid_pyclass_args.rs index 24842eb484a..f74fa49d8de 100644 --- a/tests/ui/invalid_pyclass_args.rs +++ b/tests/ui/invalid_pyclass_args.rs @@ -71,4 +71,9 @@ impl HashOptAndManualHash { } } +#[pyclass(ord)] +struct InvalidOrderedStruct { + inner: i32 +} + fn main() {} diff --git a/tests/ui/invalid_pyclass_args.stderr b/tests/ui/invalid_pyclass_args.stderr index 8f1b671dfd9..23d3c3bbc64 100644 --- a/tests/ui/invalid_pyclass_args.stderr +++ b/tests/ui/invalid_pyclass_args.stderr @@ -1,4 +1,4 @@ -error: expected one of: `crate`, `dict`, `eq`, `eq_int`, `extends`, `freelist`, `frozen`, `get_all`, `hash`, `mapping`, `module`, `name`, `rename_all`, `sequence`, `set_all`, `subclass`, `unsendable`, `weakref` +error: expected one of: `crate`, `dict`, `eq`, `eq_int`, `extends`, `freelist`, `frozen`, `get_all`, `hash`, `mapping`, `module`, `name`, `ord`, `rename_all`, `sequence`, `set_all`, `subclass`, `unsendable`, `weakref` --> tests/ui/invalid_pyclass_args.rs:3:11 | 3 | #[pyclass(extend=pyo3::types::PyDict)] @@ -46,7 +46,7 @@ error: expected string literal 24 | #[pyclass(module = my_module)] | ^^^^^^^^^ -error: expected one of: `crate`, `dict`, `eq`, `eq_int`, `extends`, `freelist`, `frozen`, `get_all`, `hash`, `mapping`, `module`, `name`, `rename_all`, `sequence`, `set_all`, `subclass`, `unsendable`, `weakref` +error: expected one of: `crate`, `dict`, `eq`, `eq_int`, `extends`, `freelist`, `frozen`, `get_all`, `hash`, `mapping`, `module`, `name`, `ord`, `rename_all`, `sequence`, `set_all`, `subclass`, `unsendable`, `weakref` --> tests/ui/invalid_pyclass_args.rs:27:11 | 27 | #[pyclass(weakrev)] @@ -76,6 +76,12 @@ error: The `hash` option requires the `eq` option. 59 | #[pyclass(hash)] | ^^^^ +error: The `ord` option requires the `eq` option. + --> tests/ui/invalid_pyclass_args.rs:74:11 + | +74 | #[pyclass(ord)] + | ^^^ + error[E0592]: duplicate definitions with name `__pymethod___richcmp____` --> tests/ui/invalid_pyclass_args.rs:36:1 | diff --git a/tests/ui/invalid_pyclass_enum.rs b/tests/ui/invalid_pyclass_enum.rs index 73bc992571a..c490f9291e3 100644 --- a/tests/ui/invalid_pyclass_enum.rs +++ b/tests/ui/invalid_pyclass_enum.rs @@ -80,4 +80,17 @@ enum ComplexHashOptRequiresEq { B { msg: String }, } +#[pyclass(ord)] +enum InvalidOrderedComplexEnum { + VariantA (i32), + VariantB { msg: String } +} + +#[pyclass(eq,ord)] +#[derive(PartialEq)] +enum InvalidOrderedComplexEnum2 { + VariantA (i32), + VariantB { msg: String } +} + fn main() {} diff --git a/tests/ui/invalid_pyclass_enum.stderr b/tests/ui/invalid_pyclass_enum.stderr index cfa3922ef62..98ca2d77bfa 100644 --- a/tests/ui/invalid_pyclass_enum.stderr +++ b/tests/ui/invalid_pyclass_enum.stderr @@ -60,6 +60,12 @@ error: The `hash` option requires the `eq` option. 76 | #[pyclass(hash)] | ^^^^ +error: The `ord` option requires the `eq` option. + --> tests/ui/invalid_pyclass_enum.rs:83:11 + | +83 | #[pyclass(ord)] + | ^^^ + error[E0369]: binary operation `==` cannot be applied to type `&SimpleEqOptRequiresPartialEq` --> tests/ui/invalid_pyclass_enum.rs:31:11 | @@ -151,3 +157,71 @@ help: consider annotating `ComplexHashOptRequiresHash` with `#[derive(Hash)]` 64 + #[derive(Hash)] 65 | enum ComplexHashOptRequiresHash { | + +error[E0369]: binary operation `>` cannot be applied to type `&InvalidOrderedComplexEnum2` + --> tests/ui/invalid_pyclass_enum.rs:89:14 + | +89 | #[pyclass(eq,ord)] + | ^^^ + | +note: an implementation of `PartialOrd` might be missing for `InvalidOrderedComplexEnum2` + --> tests/ui/invalid_pyclass_enum.rs:91:1 + | +91 | enum InvalidOrderedComplexEnum2 { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ must implement `PartialOrd` +help: consider annotating `InvalidOrderedComplexEnum2` with `#[derive(PartialEq, PartialOrd)]` + | +91 + #[derive(PartialEq, PartialOrd)] +92 | enum InvalidOrderedComplexEnum2 { + | + +error[E0369]: binary operation `<` cannot be applied to type `&InvalidOrderedComplexEnum2` + --> tests/ui/invalid_pyclass_enum.rs:89:14 + | +89 | #[pyclass(eq,ord)] + | ^^^ + | +note: an implementation of `PartialOrd` might be missing for `InvalidOrderedComplexEnum2` + --> tests/ui/invalid_pyclass_enum.rs:91:1 + | +91 | enum InvalidOrderedComplexEnum2 { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ must implement `PartialOrd` +help: consider annotating `InvalidOrderedComplexEnum2` with `#[derive(PartialEq, PartialOrd)]` + | +91 + #[derive(PartialEq, PartialOrd)] +92 | enum InvalidOrderedComplexEnum2 { + | + +error[E0369]: binary operation `<=` cannot be applied to type `&InvalidOrderedComplexEnum2` + --> tests/ui/invalid_pyclass_enum.rs:89:14 + | +89 | #[pyclass(eq,ord)] + | ^^^ + | +note: an implementation of `PartialOrd` might be missing for `InvalidOrderedComplexEnum2` + --> tests/ui/invalid_pyclass_enum.rs:91:1 + | +91 | enum InvalidOrderedComplexEnum2 { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ must implement `PartialOrd` +help: consider annotating `InvalidOrderedComplexEnum2` with `#[derive(PartialEq, PartialOrd)]` + | +91 + #[derive(PartialEq, PartialOrd)] +92 | enum InvalidOrderedComplexEnum2 { + | + +error[E0369]: binary operation `>=` cannot be applied to type `&InvalidOrderedComplexEnum2` + --> tests/ui/invalid_pyclass_enum.rs:89:14 + | +89 | #[pyclass(eq,ord)] + | ^^^ + | +note: an implementation of `PartialOrd` might be missing for `InvalidOrderedComplexEnum2` + --> tests/ui/invalid_pyclass_enum.rs:91:1 + | +91 | enum InvalidOrderedComplexEnum2 { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ must implement `PartialOrd` +help: consider annotating `InvalidOrderedComplexEnum2` with `#[derive(PartialEq, PartialOrd)]` + | +91 + #[derive(PartialEq, PartialOrd)] +92 | enum InvalidOrderedComplexEnum2 { + | From 9c670577450a48ee58ca98ed4bdc8ba311c9564e Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Fri, 7 Jun 2024 23:47:27 +0100 Subject: [PATCH 106/495] refactor: use `ptr_from_ref` and ptr `.cast()` (#4240) * refactor: use `ptr_from_ref` and ptr `.cast()` * fix unused imports --- pyo3-ffi/src/abstract_.rs | 5 +---- pyo3-ffi/src/cpython/abstract_.rs | 2 +- pyo3-ffi/src/datetime.rs | 10 ++++++---- src/buffer.rs | 20 +++++--------------- src/conversions/num_rational.rs | 11 ++--------- src/conversions/std/osstr.rs | 4 +--- src/exceptions.rs | 3 +-- src/impl_/extract_argument.rs | 4 ++-- src/impl_/pymodule.rs | 4 ++-- src/instance.rs | 15 ++++++++------- src/internal_tricks.rs | 6 ++++++ src/macros.rs | 2 +- src/marker.rs | 2 +- src/pycell.rs | 3 ++- src/pyclass/create_type_object.rs | 20 ++++++++++---------- src/types/any.rs | 5 +++-- src/types/bytearray.rs | 3 +-- src/types/bytes.rs | 5 ++--- src/types/string.rs | 25 +++++++++---------------- src/types/tuple.rs | 11 ++++------- 20 files changed, 68 insertions(+), 92 deletions(-) diff --git a/pyo3-ffi/src/abstract_.rs b/pyo3-ffi/src/abstract_.rs index b5bf9cc3d35..1e0020462fb 100644 --- a/pyo3-ffi/src/abstract_.rs +++ b/pyo3-ffi/src/abstract_.rs @@ -114,10 +114,7 @@ extern "C" { #[cfg(not(any(Py_3_8, PyPy)))] #[inline] pub unsafe fn PyIter_Check(o: *mut PyObject) -> c_int { - crate::PyObject_HasAttrString( - crate::Py_TYPE(o).cast(), - "__next__\0".as_ptr() as *const c_char, - ) + crate::PyObject_HasAttrString(crate::Py_TYPE(o).cast(), "__next__\0".as_ptr().cast()) } extern "C" { diff --git a/pyo3-ffi/src/cpython/abstract_.rs b/pyo3-ffi/src/cpython/abstract_.rs index ad28216cbff..34525cec16f 100644 --- a/pyo3-ffi/src/cpython/abstract_.rs +++ b/pyo3-ffi/src/cpython/abstract_.rs @@ -61,7 +61,7 @@ pub unsafe fn PyVectorcall_Function(callable: *mut PyObject) -> Option 0); let offset = (*tp).tp_vectorcall_offset; assert!(offset > 0); - let ptr = (callable as *const c_char).offset(offset) as *const Option; + let ptr = callable.cast::().offset(offset).cast(); *ptr } diff --git a/pyo3-ffi/src/datetime.rs b/pyo3-ffi/src/datetime.rs index a20b76aa91d..b985085fba3 100644 --- a/pyo3-ffi/src/datetime.rs +++ b/pyo3-ffi/src/datetime.rs @@ -13,7 +13,9 @@ use crate::{PyLong_AsLong, PyLong_Check, PyObject_GetAttrString, Py_DecRef}; use crate::{PyObject, PyObject_TypeCheck, PyTypeObject, Py_TYPE}; use std::cell::UnsafeCell; -use std::os::raw::{c_char, c_int}; +#[cfg(not(GraalPy))] +use std::os::raw::c_char; +use std::os::raw::c_int; use std::ptr; #[cfg(not(PyPy))] use {crate::PyCapsule_Import, std::ffi::CString}; @@ -356,7 +358,7 @@ pub unsafe fn PyDateTime_DELTA_GET_MICROSECONDS(o: *mut PyObject) -> c_int { #[inline] #[cfg(GraalPy)] pub unsafe fn _get_attr(obj: *mut PyObject, field: &str) -> c_int { - let result = PyObject_GetAttrString(obj, field.as_ptr() as *const c_char); + let result = PyObject_GetAttrString(obj, field.as_ptr().cast()); Py_DecRef(result); // the original macros are borrowing if PyLong_Check(result) == 1 { PyLong_AsLong(result) as c_int @@ -416,7 +418,7 @@ pub unsafe fn PyDateTime_DATE_GET_FOLD(o: *mut PyObject) -> c_int { #[inline] #[cfg(GraalPy)] pub unsafe fn PyDateTime_DATE_GET_TZINFO(o: *mut PyObject) -> *mut PyObject { - let res = PyObject_GetAttrString(o, "tzinfo\0".as_ptr() as *const c_char); + let res = PyObject_GetAttrString(o, "tzinfo\0".as_ptr().cast()); Py_DecRef(res); // the original macros are borrowing res } @@ -454,7 +456,7 @@ pub unsafe fn PyDateTime_TIME_GET_FOLD(o: *mut PyObject) -> c_int { #[inline] #[cfg(GraalPy)] pub unsafe fn PyDateTime_TIME_GET_TZINFO(o: *mut PyObject) -> *mut PyObject { - let res = PyObject_GetAttrString(o, "tzinfo\0".as_ptr() as *const c_char); + let res = PyObject_GetAttrString(o, "tzinfo\0".as_ptr().cast()); Py_DecRef(res); // the original macros are borrowing res } diff --git a/src/buffer.rs b/src/buffer.rs index 74ac7fe8e53..558fb5e9c8d 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -263,7 +263,7 @@ impl PyBuffer { }, #[cfg(Py_3_11)] { - indices.as_ptr() as *const ffi::Py_ssize_t + indices.as_ptr().cast() }, #[cfg(not(Py_3_11))] { @@ -317,7 +317,7 @@ impl PyBuffer { /// However, dimensions of length 0 are possible and might need special attention. #[inline] pub fn shape(&self) -> &[usize] { - unsafe { slice::from_raw_parts(self.0.shape as *const usize, self.0.ndim as usize) } + unsafe { slice::from_raw_parts(self.0.shape.cast(), self.0.ndim as usize) } } /// Returns an array that holds, for each dimension, the number of bytes to skip to get to the next element in the dimension. @@ -361,23 +361,13 @@ impl PyBuffer { /// Gets whether the buffer is contiguous in C-style order (last index varies fastest when visiting items in order of memory address). #[inline] pub fn is_c_contiguous(&self) -> bool { - unsafe { - ffi::PyBuffer_IsContiguous( - &*self.0 as *const ffi::Py_buffer, - b'C' as std::os::raw::c_char, - ) != 0 - } + unsafe { ffi::PyBuffer_IsContiguous(&*self.0, b'C' as std::os::raw::c_char) != 0 } } /// Gets whether the buffer is contiguous in Fortran-style order (first index varies fastest when visiting items in order of memory address). #[inline] pub fn is_fortran_contiguous(&self) -> bool { - unsafe { - ffi::PyBuffer_IsContiguous( - &*self.0 as *const ffi::Py_buffer, - b'F' as std::os::raw::c_char, - ) != 0 - } + unsafe { ffi::PyBuffer_IsContiguous(&*self.0, b'F' as std::os::raw::c_char) != 0 } } /// Gets the buffer memory as a slice. @@ -609,7 +599,7 @@ impl PyBuffer { }, #[cfg(Py_3_11)] { - source.as_ptr() as *const raw::c_void + source.as_ptr().cast() }, #[cfg(not(Py_3_11))] { diff --git a/src/conversions/num_rational.rs b/src/conversions/num_rational.rs index 31eb7ca1c7b..2129234dc4f 100644 --- a/src/conversions/num_rational.rs +++ b/src/conversions/num_rational.rs @@ -48,7 +48,6 @@ use crate::sync::GILOnceCell; use crate::types::any::PyAnyMethods; use crate::types::PyType; use crate::{Bound, FromPyObject, IntoPy, Py, PyAny, PyObject, PyResult, Python, ToPyObject}; -use std::os::raw::c_char; #[cfg(feature = "num-bigint")] use num_bigint::BigInt; @@ -68,19 +67,13 @@ macro_rules! rational_conversion { let py_numerator_obj = unsafe { Bound::from_owned_ptr_or_err( py, - ffi::PyObject_GetAttrString( - obj.as_ptr(), - "numerator\0".as_ptr() as *const c_char, - ), + ffi::PyObject_GetAttrString(obj.as_ptr(), "numerator\0".as_ptr().cast()), ) }; let py_denominator_obj = unsafe { Bound::from_owned_ptr_or_err( py, - ffi::PyObject_GetAttrString( - obj.as_ptr(), - "denominator\0".as_ptr() as *const c_char, - ), + ffi::PyObject_GetAttrString(obj.as_ptr(), "denominator\0".as_ptr().cast()), ) }; let numerator_owned = unsafe { diff --git a/src/conversions/std/osstr.rs b/src/conversions/std/osstr.rs index 4565c3fbd94..8616a11689c 100644 --- a/src/conversions/std/osstr.rs +++ b/src/conversions/std/osstr.rs @@ -4,8 +4,6 @@ use crate::types::PyString; use crate::{ffi, FromPyObject, IntoPy, PyAny, PyObject, PyResult, Python, ToPyObject}; use std::borrow::Cow; use std::ffi::{OsStr, OsString}; -#[cfg(not(windows))] -use std::os::raw::c_char; impl ToPyObject for OsStr { fn to_object(&self, py: Python<'_>) -> PyObject { @@ -23,7 +21,7 @@ impl ToPyObject for OsStr { #[cfg(not(target_os = "wasi"))] let bytes = std::os::unix::ffi::OsStrExt::as_bytes(self); - let ptr = bytes.as_ptr() as *const c_char; + let ptr = bytes.as_ptr().cast(); let len = bytes.len() as ffi::Py_ssize_t; unsafe { // DecodeFSDefault automatically chooses an appropriate decoding mechanism to diff --git a/src/exceptions.rs b/src/exceptions.rs index d6a6e859e3b..e2bb82f9bc1 100644 --- a/src/exceptions.rs +++ b/src/exceptions.rs @@ -12,7 +12,6 @@ use crate::{ffi, Bound, PyResult, Python}; use std::ffi::CStr; use std::ops; -use std::os::raw::c_char; /// The boilerplate to convert between a Rust type and a Python exception. #[doc(hidden)] @@ -682,7 +681,7 @@ impl PyUnicodeDecodeError { unsafe { ffi::PyUnicodeDecodeError_Create( encoding.as_ptr(), - input.as_ptr() as *const c_char, + input.as_ptr().cast(), input.len() as ffi::Py_ssize_t, range.start as ffi::Py_ssize_t, range.end as ffi::Py_ssize_t, diff --git a/src/impl_/extract_argument.rs b/src/impl_/extract_argument.rs index 5f652d75122..a354e578c5f 100644 --- a/src/impl_/extract_argument.rs +++ b/src/impl_/extract_argument.rs @@ -318,9 +318,9 @@ impl FunctionDescription { let kwnames: Option> = Borrowed::from_ptr_or_opt(py, kwnames).map(|kwnames| kwnames.downcast_unchecked()); if let Some(kwnames) = kwnames { - // Safety: PyArg has the same memory layout as `*mut ffi::PyObject` let kwargs = ::std::slice::from_raw_parts( - (args as *const PyArg<'py>).offset(nargs), + // Safety: PyArg has the same memory layout as `*mut ffi::PyObject` + args.offset(nargs).cast::>(), kwnames.len(), ); diff --git a/src/impl_/pymodule.rs b/src/impl_/pymodule.rs index 5f04d888a50..0c3d8951fc9 100644 --- a/src/impl_/pymodule.rs +++ b/src/impl_/pymodule.rs @@ -70,8 +70,8 @@ impl ModuleDef { }; let ffi_def = UnsafeCell::new(ffi::PyModuleDef { - m_name: name.as_ptr() as *const _, - m_doc: doc.as_ptr() as *const _, + m_name: name.as_ptr().cast(), + m_doc: doc.as_ptr().cast(), ..INIT }); diff --git a/src/instance.rs b/src/instance.rs index 82b05e782ff..2992b273a5b 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -1,5 +1,6 @@ use crate::err::{self, PyErr, PyResult}; use crate::impl_::pycell::PyClassObject; +use crate::internal_tricks::ptr_from_ref; use crate::pycell::{PyBorrowError, PyBorrowMutError}; use crate::pyclass::boolean_struct::{False, True}; #[cfg(feature = "gil-refs")] @@ -42,7 +43,7 @@ pub unsafe trait PyNativeType: Sized { // Safety: &'py Self is expected to be a Python pointer, // so has the same layout as Borrowed<'py, 'py, T> Borrowed( - unsafe { NonNull::new_unchecked(self as *const Self as *mut _) }, + unsafe { NonNull::new_unchecked(ptr_from_ref(self) as *mut _) }, PhantomData, self.py(), ) @@ -193,7 +194,7 @@ impl<'py> Bound<'py, PyAny> { _py: Python<'py>, ptr: &'a *mut ffi::PyObject, ) -> &'a Self { - &*(ptr as *const *mut ffi::PyObject).cast::>() + &*ptr_from_ref(ptr).cast::>() } /// Variant of the above which returns `None` for null pointers. @@ -205,7 +206,7 @@ impl<'py> Bound<'py, PyAny> { _py: Python<'py>, ptr: &'a *mut ffi::PyObject, ) -> &'a Option { - &*(ptr as *const *mut ffi::PyObject).cast::>>() + &*ptr_from_ref(ptr).cast::>>() } } @@ -454,7 +455,7 @@ impl<'py, T> Bound<'py, T> { pub fn as_any(&self) -> &Bound<'py, PyAny> { // Safety: all Bound have the same memory layout, and all Bound are valid // Bound, so pointer casting is valid. - unsafe { &*(self as *const Self).cast::>() } + unsafe { &*ptr_from_ref(self).cast::>() } } /// Helper to cast to `Bound<'py, PyAny>`, transferring ownership. @@ -694,7 +695,7 @@ impl<'py, T> Deref for Borrowed<'_, 'py, T> { #[inline] fn deref(&self) -> &Bound<'py, T> { // safety: Bound has the same layout as NonNull - unsafe { &*(&self.0 as *const _ as *const Bound<'py, T>) } + unsafe { &*ptr_from_ref(&self.0).cast() } } } @@ -1097,7 +1098,7 @@ impl Py { pub fn as_any(&self) -> &Py { // Safety: all Py have the same memory layout, and all Py are valid // Py, so pointer casting is valid. - unsafe { &*(self as *const Self).cast::>() } + unsafe { &*ptr_from_ref(self).cast::>() } } /// Helper to cast to `Py`, transferring ownership. @@ -1273,7 +1274,7 @@ impl Py { #[inline] pub fn bind<'py>(&self, _py: Python<'py>) -> &Bound<'py, T> { // Safety: `Bound` has the same layout as `Py` - unsafe { &*(self as *const Py).cast() } + unsafe { &*ptr_from_ref(self).cast() } } /// Same as `bind` but takes ownership of `self`. diff --git a/src/internal_tricks.rs b/src/internal_tricks.rs index 75f23edbbd8..62ec0d02166 100644 --- a/src/internal_tricks.rs +++ b/src/internal_tricks.rs @@ -217,3 +217,9 @@ pub(crate) fn extract_c_string( }; Ok(cow) } + +// TODO: use ptr::from_ref on MSRV 1.76 +#[inline] +pub(crate) const fn ptr_from_ref(t: &T) -> *const T { + t as *const T +} diff --git a/src/macros.rs b/src/macros.rs index 6dde89e51a0..9316b871390 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -214,7 +214,7 @@ macro_rules! append_to_inittab { ); } $crate::ffi::PyImport_AppendInittab( - $module::__PYO3_NAME.as_ptr() as *const ::std::os::raw::c_char, + $module::__PYO3_NAME.as_ptr().cast(), ::std::option::Option::Some($module::__pyo3_init), ); } diff --git a/src/marker.rs b/src/marker.rs index a6b1e305252..b2cbc317841 100644 --- a/src/marker.rs +++ b/src/marker.rs @@ -652,7 +652,7 @@ impl<'py> Python<'py> { ) -> PyResult> { let code = CString::new(code)?; unsafe { - let mptr = ffi::PyImport_AddModule("__main__\0".as_ptr() as *const _); + let mptr = ffi::PyImport_AddModule("__main__\0".as_ptr().cast()); if mptr.is_null() { return Err(PyErr::fetch(self)); } diff --git a/src/pycell.rs b/src/pycell.rs index f15f5a54431..1d601474bda 100644 --- a/src/pycell.rs +++ b/src/pycell.rs @@ -202,6 +202,7 @@ use crate::types::any::PyAnyMethods; use crate::{ conversion::ToPyObject, impl_::pyclass::PyClassImpl, + internal_tricks::ptr_from_ref, pyclass::boolean_struct::True, pyclass_init::PyClassInitializer, type_object::{PyLayout, PySizedLayout}, @@ -511,7 +512,7 @@ where #[allow(deprecated)] unsafe impl AsPyPointer for PyCell { fn as_ptr(&self) -> *mut ffi::PyObject { - (self as *const _) as *mut _ + ptr_from_ref(self) as *mut _ } } diff --git a/src/pyclass/create_type_object.rs b/src/pyclass/create_type_object.rs index 1b3a9fb1296..262d1e8ffc7 100644 --- a/src/pyclass/create_type_object.rs +++ b/src/pyclass/create_type_object.rs @@ -3,17 +3,17 @@ use pyo3_ffi::PyType_IS_GC; use crate::{ exceptions::PyTypeError, ffi, - impl_::pycell::PyClassObject, - impl_::pyclass::{ - assign_sequence_item_from_mapping, get_sequence_item_from_mapping, tp_dealloc, - tp_dealloc_with_gc, PyClassItemsIter, - }, impl_::{ + pycell::PyClassObject, + pyclass::{ + assign_sequence_item_from_mapping, get_sequence_item_from_mapping, tp_dealloc, + tp_dealloc_with_gc, PyClassItemsIter, + }, pymethods::{get_doc, get_name, Getter, Setter}, trampoline::trampoline, }, - types::typeobject::PyTypeMethods, - types::PyType, + internal_tricks::ptr_from_ref, + types::{typeobject::PyTypeMethods, PyType}, Py, PyClass, PyGetterDef, PyMethodDefType, PyResult, PySetterDef, PyTypeInfo, Python, }; use std::{ @@ -608,7 +608,7 @@ impl GetSetDefType { slf: *mut ffi::PyObject, closure: *mut c_void, ) -> *mut ffi::PyObject { - let getset: &GetterAndSetter = &*(closure as *const GetterAndSetter); + let getset: &GetterAndSetter = &*closure.cast(); trampoline(|py| (getset.getter)(py, slf)) } @@ -617,13 +617,13 @@ impl GetSetDefType { value: *mut ffi::PyObject, closure: *mut c_void, ) -> c_int { - let getset: &GetterAndSetter = &*(closure as *const GetterAndSetter); + let getset: &GetterAndSetter = &*closure.cast(); trampoline(|py| (getset.setter)(py, slf, value)) } ( Some(getset_getter), Some(getset_setter), - closure.as_ref() as *const GetterAndSetter as _, + ptr_from_ref::(closure) as *mut _, ) } }; diff --git a/src/types/any.rs b/src/types/any.rs index 85e540f9c15..06634d69b0e 100644 --- a/src/types/any.rs +++ b/src/types/any.rs @@ -4,6 +4,7 @@ use crate::err::{DowncastError, DowncastIntoError, PyErr, PyResult}; use crate::exceptions::{PyAttributeError, PyTypeError}; use crate::ffi_ptr_ext::FfiPtrExt; use crate::instance::Bound; +use crate::internal_tricks::ptr_from_ref; use crate::py_result_ext::PyResultExt; use crate::type_object::{PyTypeCheck, PyTypeInfo}; #[cfg(not(any(PyPy, GraalPy)))] @@ -912,7 +913,7 @@ impl PyAny { /// when they are finished with the pointer. #[inline] pub fn as_ptr(&self) -> *mut ffi::PyObject { - self as *const PyAny as *mut ffi::PyObject + ptr_from_ref(self) as *mut ffi::PyObject } /// Returns an owned raw FFI pointer represented by self. @@ -2211,7 +2212,7 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { #[inline] unsafe fn downcast_unchecked(&self) -> &Bound<'py, T> { - &*(self as *const Bound<'py, PyAny>).cast() + &*ptr_from_ref(self).cast() } #[inline] diff --git a/src/types/bytearray.rs b/src/types/bytearray.rs index ec3d7eafbfd..fbd77a38ad0 100644 --- a/src/types/bytearray.rs +++ b/src/types/bytearray.rs @@ -6,7 +6,6 @@ use crate::types::any::PyAnyMethods; use crate::{ffi, PyAny, Python}; #[cfg(feature = "gil-refs")] use crate::{AsPyPointer, PyNativeType}; -use std::os::raw::c_char; use std::slice; /// Represents a Python `bytearray`. @@ -20,7 +19,7 @@ impl PyByteArray { /// /// The byte string is initialized by copying the data from the `&[u8]`. pub fn new_bound<'py>(py: Python<'py>, src: &[u8]) -> Bound<'py, PyByteArray> { - let ptr = src.as_ptr() as *const c_char; + let ptr = src.as_ptr().cast(); let len = src.len() as ffi::Py_ssize_t; unsafe { ffi::PyByteArray_FromStringAndSize(ptr, len) diff --git a/src/types/bytes.rs b/src/types/bytes.rs index 1d6a2f8ec7d..661c3022183 100644 --- a/src/types/bytes.rs +++ b/src/types/bytes.rs @@ -5,7 +5,6 @@ use crate::types::any::PyAnyMethods; use crate::PyNativeType; use crate::{ffi, Py, PyAny, PyResult, Python}; use std::ops::Index; -use std::os::raw::c_char; use std::slice::SliceIndex; use std::str; @@ -23,7 +22,7 @@ impl PyBytes { /// /// Panics if out of memory. pub fn new_bound<'p>(py: Python<'p>, s: &[u8]) -> Bound<'p, PyBytes> { - let ptr = s.as_ptr() as *const c_char; + let ptr = s.as_ptr().cast(); let len = s.len() as ffi::Py_ssize_t; unsafe { ffi::PyBytes_FromStringAndSize(ptr, len) @@ -85,7 +84,7 @@ impl PyBytes { /// `std::slice::from_raw_parts`, this is /// unsafe](https://doc.rust-lang.org/std/slice/fn.from_raw_parts.html#safety). pub unsafe fn bound_from_ptr(py: Python<'_>, ptr: *const u8, len: usize) -> Bound<'_, PyBytes> { - ffi::PyBytes_FromStringAndSize(ptr as *const _, len as isize) + ffi::PyBytes_FromStringAndSize(ptr.cast(), len as isize) .assume_owned(py) .downcast_into_unchecked() } diff --git a/src/types/string.rs b/src/types/string.rs index 4f0025acfe8..0582a900870 100644 --- a/src/types/string.rs +++ b/src/types/string.rs @@ -10,7 +10,6 @@ use crate::types::PyBytes; use crate::PyNativeType; use crate::{ffi, Bound, IntoPy, Py, PyAny, PyResult, Python}; use std::borrow::Cow; -use std::os::raw::c_char; use std::str; /// Represents raw data backing a Python `str`. @@ -37,16 +36,10 @@ impl<'a> PyStringData<'a> { match self { Self::Ucs1(s) => s, Self::Ucs2(s) => unsafe { - std::slice::from_raw_parts( - s.as_ptr() as *const u8, - s.len() * self.value_width_bytes(), - ) + std::slice::from_raw_parts(s.as_ptr().cast(), s.len() * self.value_width_bytes()) }, Self::Ucs4(s) => unsafe { - std::slice::from_raw_parts( - s.as_ptr() as *const u8, - s.len() * self.value_width_bytes(), - ) + std::slice::from_raw_parts(s.as_ptr().cast(), s.len() * self.value_width_bytes()) }, } } @@ -141,7 +134,7 @@ impl PyString { /// /// Panics if out of memory. pub fn new_bound<'py>(py: Python<'py>, s: &str) -> Bound<'py, PyString> { - let ptr = s.as_ptr() as *const c_char; + let ptr = s.as_ptr().cast(); let len = s.len() as ffi::Py_ssize_t; unsafe { ffi::PyUnicode_FromStringAndSize(ptr, len) @@ -159,7 +152,7 @@ impl PyString { /// /// Panics if out of memory. pub fn intern_bound<'py>(py: Python<'py>, s: &str) -> Bound<'py, PyString> { - let ptr = s.as_ptr() as *const c_char; + let ptr = s.as_ptr().cast(); let len = s.len() as ffi::Py_ssize_t; unsafe { let mut ob = ffi::PyUnicode_FromStringAndSize(ptr, len); @@ -181,8 +174,8 @@ impl PyString { unsafe { ffi::PyUnicode_FromEncodedObject( src.as_ptr(), - encoding.as_ptr() as *const c_char, - errors.as_ptr() as *const c_char, + encoding.as_ptr().cast(), + errors.as_ptr().cast(), ) .assume_owned_or_err(src.py()) .downcast_into_unchecked() @@ -607,7 +600,7 @@ mod tests { let ptr = unsafe { crate::ffi::PyUnicode_FromKindAndData( crate::ffi::PyUnicode_1BYTE_KIND as _, - buffer.as_ptr() as *const _, + buffer.as_ptr().cast(), 2, ) }; @@ -651,7 +644,7 @@ mod tests { let ptr = unsafe { crate::ffi::PyUnicode_FromKindAndData( crate::ffi::PyUnicode_2BYTE_KIND as _, - buffer.as_ptr() as *const _, + buffer.as_ptr().cast(), 2, ) }; @@ -692,7 +685,7 @@ mod tests { let ptr = unsafe { crate::ffi::PyUnicode_FromKindAndData( crate::ffi::PyUnicode_4BYTE_KIND as _, - buffer.as_ptr() as *const _, + buffer.as_ptr().cast(), 2, ) }; diff --git a/src/types/tuple.rs b/src/types/tuple.rs index afe129879f9..fcf931c1d8a 100644 --- a/src/types/tuple.rs +++ b/src/types/tuple.rs @@ -388,13 +388,10 @@ impl<'py> PyTupleMethods<'py> for Bound<'py, PyTuple> { #[cfg(not(any(Py_LIMITED_API, GraalPy)))] fn as_slice(&self) -> &[Bound<'py, PyAny>] { - // This is safe because Bound<'py, PyAny> has the same memory layout as *mut ffi::PyObject, - // and because tuples are immutable. - unsafe { - let ptr = self.as_ptr() as *mut ffi::PyTupleObject; - let slice = std::slice::from_raw_parts((*ptr).ob_item.as_ptr(), self.len()); - &*(slice as *const [*mut ffi::PyObject] as *const [Bound<'py, PyAny>]) - } + // SAFETY: self is known to be a tuple object, and tuples are immutable + let items = unsafe { &(*self.as_ptr().cast::()).ob_item }; + // SAFETY: Bound<'py, PyAny> has the same memory layout as *mut ffi::PyObject + unsafe { std::slice::from_raw_parts(items.as_ptr().cast(), self.len()) } } #[inline] From d2dca2169c698c01fea9dacb4f4aca83f4883b72 Mon Sep 17 00:00:00 2001 From: JRRudy1 <31031841+JRRudy1@users.noreply.github.com> Date: Sun, 9 Jun 2024 02:17:23 -0500 Subject: [PATCH 107/495] Added `as_super` methods to `PyRef` and `PyRefMut`. (#4219) * Added `PyRef::as_super` and `PyRefMut::as_super` methods, including docstrings and tests. The implementation of these methods also required adding `#[repr(transparent)]` to the `PyRef` and `PyRefMut` structs. * Added newsfragment entry. * Changed the `AsRef`/`AsMut` impls for `PyRef` and `PyRefMut` to use the new `as_super` methods. Added the `PyRefMut::downgrade` associated function for converting `&PyRefMut` to `&PyRef`. Updated tests and docstrings to better demonstrate the new functionality. * Fixed newsfragment filename. * Removed unnecessary re-borrows flagged by clippy. * retrigger checks * Updated `PyRef::as_super`, `PyRefMut::as_super`, and `PyRefMut::downgrade` to use `.cast()` instead of `as _` pointer casts. Fixed typo. * Updated `PyRef::as_super` and `PyRefMut::downgrade` to use `ptr_from_ref` for the initial cast to `*const _` instead of `as _` casts. * Added `pyo3::internal_tricks::ptr_from_mut` function alongside the `ptr_from_ref` added in PR #4240. Updated `PyRefMut::as_super` to use this method instead of `as *mut _`. * Updated the user guide to recommend `as_super` for accessing the base class instead of `as_ref`, and updated the subsequent example/doctest to demonstrate this functionality. * Improved tests for the `as_super` methods. * Updated newsfragment to include additional changes. * Fixed formatting. --------- Co-authored-by: jrudolph --- guide/src/class.md | 39 +++++++-- newsfragments/4219.added.md | 3 + src/internal_tricks.rs | 6 ++ src/pycell.rs | 168 +++++++++++++++++++++++++++++++++++- 4 files changed, 206 insertions(+), 10 deletions(-) create mode 100644 newsfragments/4219.added.md diff --git a/guide/src/class.md b/guide/src/class.md index 2bcfe75911e..ab0c82fc88b 100644 --- a/guide/src/class.md +++ b/guide/src/class.md @@ -327,8 +327,12 @@ explicitly. To get a parent class from a child, use [`PyRef`] instead of `&self` for methods, or [`PyRefMut`] instead of `&mut self`. -Then you can access a parent class by `self_.as_ref()` as `&Self::BaseClass`, -or by `self_.into_super()` as `PyRef`. +Then you can access a parent class by `self_.as_super()` as `&PyRef`, +or by `self_.into_super()` as `PyRef` (and similar for the `PyRefMut` +case). For convenience, `self_.as_ref()` can also be used to get `&Self::BaseClass` +directly; however, this approach does not let you access base clases higher in the +inheritance hierarchy, for which you would need to chain multiple `as_super` or +`into_super` calls. ```rust # use pyo3::prelude::*; @@ -345,7 +349,7 @@ impl BaseClass { BaseClass { val1: 10 } } - pub fn method(&self) -> PyResult { + pub fn method1(&self) -> PyResult { Ok(self.val1) } } @@ -363,8 +367,8 @@ impl SubClass { } fn method2(self_: PyRef<'_, Self>) -> PyResult { - let super_ = self_.as_ref(); // Get &BaseClass - super_.method().map(|x| x * self_.val2) + let super_ = self_.as_super(); // Get &PyRef + super_.method1().map(|x| x * self_.val2) } } @@ -381,11 +385,28 @@ impl SubSubClass { } fn method3(self_: PyRef<'_, Self>) -> PyResult { + let base = self_.as_super().as_super(); // Get &PyRef<'_, BaseClass> + base.method1().map(|x| x * self_.val3) + } + + fn method4(self_: PyRef<'_, Self>) -> PyResult { let v = self_.val3; let super_ = self_.into_super(); // Get PyRef<'_, SubClass> SubClass::method2(super_).map(|x| x * v) } + fn get_values(self_: PyRef<'_, Self>) -> (usize, usize, usize) { + let val1 = self_.as_super().as_super().val1; + let val2 = self_.as_super().val2; + (val1, val2, self_.val3) + } + + fn double_values(mut self_: PyRefMut<'_, Self>) { + self_.as_super().as_super().val1 *= 2; + self_.as_super().val2 *= 2; + self_.val3 *= 2; + } + #[staticmethod] fn factory_method(py: Python<'_>, val: usize) -> PyResult { let base = PyClassInitializer::from(BaseClass::new()); @@ -400,7 +421,13 @@ impl SubSubClass { } # Python::with_gil(|py| { # let subsub = pyo3::Py::new(py, SubSubClass::new()).unwrap(); -# pyo3::py_run!(py, subsub, "assert subsub.method3() == 3000"); +# pyo3::py_run!(py, subsub, "assert subsub.method1() == 10"); +# pyo3::py_run!(py, subsub, "assert subsub.method2() == 150"); +# pyo3::py_run!(py, subsub, "assert subsub.method3() == 200"); +# pyo3::py_run!(py, subsub, "assert subsub.method4() == 3000"); +# pyo3::py_run!(py, subsub, "assert subsub.get_values() == (10, 15, 20)"); +# pyo3::py_run!(py, subsub, "assert subsub.double_values() == None"); +# pyo3::py_run!(py, subsub, "assert subsub.get_values() == (20, 30, 40)"); # let subsub = SubSubClass::factory_method(py, 2).unwrap(); # let subsubsub = SubSubClass::factory_method(py, 3).unwrap(); # let cls = py.get_type_bound::(); diff --git a/newsfragments/4219.added.md b/newsfragments/4219.added.md new file mode 100644 index 00000000000..cea8fa1c314 --- /dev/null +++ b/newsfragments/4219.added.md @@ -0,0 +1,3 @@ +- Added `as_super` methods to `PyRef` and `PyRefMut` for accesing the base class by reference +- Updated user guide to recommend `as_super` for referencing the base class instead of `as_ref` +- Added `pyo3::internal_tricks::ptr_from_mut` function for casting `&mut T` to `*mut T` \ No newline at end of file diff --git a/src/internal_tricks.rs b/src/internal_tricks.rs index 62ec0d02166..a8873dda007 100644 --- a/src/internal_tricks.rs +++ b/src/internal_tricks.rs @@ -223,3 +223,9 @@ pub(crate) fn extract_c_string( pub(crate) const fn ptr_from_ref(t: &T) -> *const T { t as *const T } + +// TODO: use ptr::from_mut on MSRV 1.76 +#[inline] +pub(crate) fn ptr_from_mut(t: &mut T) -> *mut T { + t as *mut T +} diff --git a/src/pycell.rs b/src/pycell.rs index 1d601474bda..9ed6c8aca7d 100644 --- a/src/pycell.rs +++ b/src/pycell.rs @@ -196,13 +196,13 @@ use crate::conversion::AsPyPointer; use crate::exceptions::PyRuntimeError; use crate::ffi_ptr_ext::FfiPtrExt; +use crate::internal_tricks::{ptr_from_mut, ptr_from_ref}; use crate::pyclass::{boolean_struct::False, PyClass}; use crate::types::any::PyAnyMethods; #[cfg(feature = "gil-refs")] use crate::{ conversion::ToPyObject, impl_::pyclass::PyClassImpl, - internal_tricks::ptr_from_ref, pyclass::boolean_struct::True, pyclass_init::PyClassInitializer, type_object::{PyLayout, PySizedLayout}, @@ -612,6 +612,7 @@ impl fmt::Debug for PyCell { /// ``` /// /// See the [module-level documentation](self) for more information. +#[repr(transparent)] pub struct PyRef<'p, T: PyClass> { // TODO: once the GIL Ref API is removed, consider adding a lifetime parameter to `PyRef` to // store `Borrowed` here instead, avoiding reference counting overhead. @@ -631,7 +632,7 @@ where U: PyClass, { fn as_ref(&self) -> &T::BaseType { - unsafe { &*self.inner.get_class_object().ob_base.get_ptr() } + self.as_super() } } @@ -743,6 +744,58 @@ where }, } } + + /// Borrows a shared reference to `PyRef`. + /// + /// With the help of this method, you can access attributes and call methods + /// on the superclass without consuming the `PyRef`. This method can also + /// be chained to access the super-superclass (and so on). + /// + /// # Examples + /// ``` + /// # use pyo3::prelude::*; + /// #[pyclass(subclass)] + /// struct Base { + /// base_name: &'static str, + /// } + /// #[pymethods] + /// impl Base { + /// fn base_name_len(&self) -> usize { + /// self.base_name.len() + /// } + /// } + /// + /// #[pyclass(extends=Base)] + /// struct Sub { + /// sub_name: &'static str, + /// } + /// + /// #[pymethods] + /// impl Sub { + /// #[new] + /// fn new() -> (Self, Base) { + /// (Self { sub_name: "sub_name" }, Base { base_name: "base_name" }) + /// } + /// fn sub_name_len(&self) -> usize { + /// self.sub_name.len() + /// } + /// fn format_name_lengths(slf: PyRef<'_, Self>) -> String { + /// format!("{} {}", slf.as_super().base_name_len(), slf.sub_name_len()) + /// } + /// } + /// # Python::with_gil(|py| { + /// # let sub = Py::new(py, Sub::new()).unwrap(); + /// # pyo3::py_run!(py, sub, "assert sub.format_name_lengths() == '9 8'") + /// # }); + /// ``` + pub fn as_super(&self) -> &PyRef<'p, U> { + let ptr = ptr_from_ref::>(&self.inner) + // `Bound` has the same layout as `Bound` + .cast::>() + // `Bound` has the same layout as `PyRef` + .cast::>(); + unsafe { &*ptr } + } } impl<'p, T: PyClass> Deref for PyRef<'p, T> { @@ -799,6 +852,7 @@ impl fmt::Debug for PyRef<'_, T> { /// A wrapper type for a mutably borrowed value from a [`Bound<'py, T>`]. /// /// See the [module-level documentation](self) for more information. +#[repr(transparent)] pub struct PyRefMut<'p, T: PyClass> { // TODO: once the GIL Ref API is removed, consider adding a lifetime parameter to `PyRef` to // store `Borrowed` here instead, avoiding reference counting overhead. @@ -818,7 +872,7 @@ where U: PyClass, { fn as_ref(&self) -> &T::BaseType { - unsafe { &*self.inner.get_class_object().ob_base.get_ptr() } + PyRefMut::downgrade(self).as_super() } } @@ -828,7 +882,7 @@ where U: PyClass, { fn as_mut(&mut self) -> &mut T::BaseType { - unsafe { &mut *self.inner.get_class_object().ob_base.get_ptr() } + self.as_super() } } @@ -870,6 +924,11 @@ impl<'py, T: PyClass> PyRefMut<'py, T> { .try_borrow_mut() .map(|_| Self { inner: obj.clone() }) } + + pub(crate) fn downgrade(slf: &Self) -> &PyRef<'py, T> { + // `PyRefMut` and `PyRef` have the same layout + unsafe { &*ptr_from_ref(slf).cast() } + } } impl<'p, T, U> PyRefMut<'p, T> @@ -891,6 +950,23 @@ where }, } } + + /// Borrows a mutable reference to `PyRefMut`. + /// + /// With the help of this method, you can mutate attributes and call mutating + /// methods on the superclass without consuming the `PyRefMut`. This method + /// can also be chained to access the super-superclass (and so on). + /// + /// See [`PyRef::as_super`] for more. + pub fn as_super(&mut self) -> &mut PyRefMut<'p, U> { + let ptr = ptr_from_mut::>(&mut self.inner) + // `Bound` has the same layout as `Bound` + .cast::>() + // `Bound` has the same layout as `PyRefMut`, + // and the mutable borrow on `self` prevents aliasing + .cast::>(); + unsafe { &mut *ptr } + } } impl<'p, T: PyClass> Deref for PyRefMut<'p, T> { @@ -1140,4 +1216,88 @@ mod tests { unsafe { ffi::Py_DECREF(ptr) }; }) } + + #[crate::pyclass] + #[pyo3(crate = "crate", subclass)] + struct BaseClass { + val1: usize, + } + + #[crate::pyclass] + #[pyo3(crate = "crate", extends=BaseClass, subclass)] + struct SubClass { + val2: usize, + } + + #[crate::pyclass] + #[pyo3(crate = "crate", extends=SubClass)] + struct SubSubClass { + val3: usize, + } + + #[crate::pymethods] + #[pyo3(crate = "crate")] + impl SubSubClass { + #[new] + fn new(py: Python<'_>) -> crate::Py { + let init = crate::PyClassInitializer::from(BaseClass { val1: 10 }) + .add_subclass(SubClass { val2: 15 }) + .add_subclass(SubSubClass { val3: 20 }); + crate::Py::new(py, init).expect("allocation error") + } + + fn get_values(self_: PyRef<'_, Self>) -> (usize, usize, usize) { + let val1 = self_.as_super().as_super().val1; + let val2 = self_.as_super().val2; + (val1, val2, self_.val3) + } + + fn double_values(mut self_: PyRefMut<'_, Self>) { + self_.as_super().as_super().val1 *= 2; + self_.as_super().val2 *= 2; + self_.val3 *= 2; + } + } + + #[test] + fn test_pyref_as_super() { + Python::with_gil(|py| { + let obj = SubSubClass::new(py).into_bound(py); + let pyref = obj.borrow(); + assert_eq!(pyref.as_super().as_super().val1, 10); + assert_eq!(pyref.as_super().val2, 15); + assert_eq!(pyref.as_ref().val2, 15); // `as_ref` also works + assert_eq!(pyref.val3, 20); + assert_eq!(SubSubClass::get_values(pyref), (10, 15, 20)); + }); + } + + #[test] + fn test_pyrefmut_as_super() { + Python::with_gil(|py| { + let obj = SubSubClass::new(py).into_bound(py); + assert_eq!(SubSubClass::get_values(obj.borrow()), (10, 15, 20)); + { + let mut pyrefmut = obj.borrow_mut(); + assert_eq!(pyrefmut.as_super().as_ref().val1, 10); + pyrefmut.as_super().as_super().val1 -= 5; + pyrefmut.as_super().val2 -= 3; + pyrefmut.as_mut().val2 -= 2; // `as_mut` also works + pyrefmut.val3 -= 5; + } + assert_eq!(SubSubClass::get_values(obj.borrow()), (5, 10, 15)); + SubSubClass::double_values(obj.borrow_mut()); + assert_eq!(SubSubClass::get_values(obj.borrow()), (10, 20, 30)); + }); + } + + #[test] + fn test_pyrefs_in_python() { + Python::with_gil(|py| { + let obj = SubSubClass::new(py); + crate::py_run!(py, obj, "assert obj.get_values() == (10, 15, 20)"); + crate::py_run!(py, obj, "assert obj.double_values() is None"); + crate::py_run!(py, obj, "assert obj.get_values() == (20, 30, 40)"); + }); + } } From f66124a79bc4df93d1d611dcd67cc9a18bcb735d Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Mon, 10 Jun 2024 08:26:05 +0100 Subject: [PATCH 108/495] pypy/graalpy: set `Py_LIMITED_API` when `abi3` requested (#4237) * pypy/graalpy: set `Py_LIMITED_API` when `abi3` requested * add newsfragment --- newsfragments/4237.changed.md | 1 + pyo3-build-config/src/impl_.rs | 28 +++++++++------------------- pyo3-ffi/build.rs | 15 ++++++++++++++- pyo3-ffi/src/cpython/pythonrun.rs | 8 ++------ pyo3-ffi/src/pythonrun.rs | 24 +++++++++++++++++++++++- src/ffi/tests.rs | 7 +------ 6 files changed, 50 insertions(+), 33 deletions(-) create mode 100644 newsfragments/4237.changed.md diff --git a/newsfragments/4237.changed.md b/newsfragments/4237.changed.md new file mode 100644 index 00000000000..25dd922d668 --- /dev/null +++ b/newsfragments/4237.changed.md @@ -0,0 +1 @@ +Respect the Python "limited API" when building for the `abi3` feature on PyPy or GraalPy. diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index 35c300da190..9ef9f477b9e 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -26,7 +26,7 @@ use target_lexicon::{Environment, OperatingSystem}; use crate::{ bail, ensure, errors::{Context, Error, Result}, - format_warn, warn, + warn, }; /// Minimum Python version PyO3 supports. @@ -171,20 +171,13 @@ impl InterpreterConfig { out.push(format!("cargo:rustc-cfg=Py_3_{}", i)); } - if self.implementation.is_pypy() { - out.push("cargo:rustc-cfg=PyPy".to_owned()); - if self.abi3 { - 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.implementation.is_graalpy() { - println!("cargo:rustc-cfg=GraalPy"); - if self.abi3 { - warn!("GraalPy does not support abi3 so the build artifacts will be version-specific."); - } - } else if self.abi3 { + match self.implementation { + PythonImplementation::CPython => {} + PythonImplementation::PyPy => out.push("cargo:rustc-cfg=PyPy".to_owned()), + PythonImplementation::GraalPy => out.push("cargo:rustc-cfg=GraalPy".to_owned()), + } + + if self.abi3 { out.push("cargo:rustc-cfg=Py_LIMITED_API".to_owned()); } @@ -2722,10 +2715,7 @@ mod tests { "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(), + "cargo:rustc-cfg=Py_LIMITED_API".to_owned(), ] ); } diff --git a/pyo3-ffi/build.rs b/pyo3-ffi/build.rs index 0f4931d6dc7..b4521678ba9 100644 --- a/pyo3-ffi/build.rs +++ b/pyo3-ffi/build.rs @@ -4,7 +4,7 @@ use pyo3_build_config::{ cargo_env_var, env_var, errors::Result, is_linking_libpython, resolve_interpreter_config, InterpreterConfig, PythonVersion, }, - PythonImplementation, + warn, PythonImplementation, }; /// Minimum Python version PyO3 supports. @@ -104,6 +104,19 @@ fn ensure_python_version(interpreter_config: &InterpreterConfig) -> Result<()> { } } + if interpreter_config.abi3 { + match interpreter_config.implementation { + PythonImplementation::CPython => {} + PythonImplementation::PyPy => 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." + ), + PythonImplementation::GraalPy => warn!( + "GraalPy does not support abi3 so the build artifacts will be version-specific." + ), + } + } + Ok(()) } diff --git a/pyo3-ffi/src/cpython/pythonrun.rs b/pyo3-ffi/src/cpython/pythonrun.rs index 94863166e11..fe78f55ca07 100644 --- a/pyo3-ffi/src/cpython/pythonrun.rs +++ b/pyo3-ffi/src/cpython/pythonrun.rs @@ -135,13 +135,9 @@ extern "C" { } #[inline] -#[cfg(not(GraalPy))] +#[cfg(not(any(PyPy, GraalPy)))] pub unsafe fn Py_CompileString(string: *const c_char, p: *const c_char, s: c_int) -> *mut PyObject { - #[cfg(not(PyPy))] - return Py_CompileStringExFlags(string, p, s, std::ptr::null_mut(), -1); - - #[cfg(PyPy)] - Py_CompileStringFlags(string, p, s, std::ptr::null_mut()) + Py_CompileStringExFlags(string, p, s, std::ptr::null_mut(), -1) } #[inline] diff --git a/pyo3-ffi/src/pythonrun.rs b/pyo3-ffi/src/pythonrun.rs index 10985b6068c..e7ea2d2efd0 100644 --- a/pyo3-ffi/src/pythonrun.rs +++ b/pyo3-ffi/src/pythonrun.rs @@ -1,7 +1,7 @@ use crate::object::*; #[cfg(not(any(PyPy, Py_LIMITED_API, Py_3_10)))] use libc::FILE; -#[cfg(all(not(PyPy), any(Py_LIMITED_API, not(Py_3_10), GraalPy)))] +#[cfg(any(Py_LIMITED_API, not(Py_3_10), PyPy, GraalPy))] use std::os::raw::c_char; use std::os::raw::c_int; @@ -20,6 +20,28 @@ extern "C" { pub fn PyErr_DisplayException(exc: *mut PyObject); } +#[inline] +#[cfg(PyPy)] +pub unsafe fn Py_CompileString(string: *const c_char, p: *const c_char, s: c_int) -> *mut PyObject { + // PyPy's implementation of Py_CompileString always forwards to Py_CompileStringFlags; this + // is only available in the non-limited API and has a real definition for all versions in + // the cpython/ subdirectory. + #[cfg(Py_LIMITED_API)] + extern "C" { + #[link_name = "PyPy_CompileStringFlags"] + pub fn Py_CompileStringFlags( + string: *const c_char, + p: *const c_char, + s: c_int, + f: *mut std::os::raw::c_void, // Actually *mut Py_CompilerFlags in the real definition + ) -> *mut PyObject; + } + #[cfg(not(Py_LIMITED_API))] + use crate::Py_CompileStringFlags; + + Py_CompileStringFlags(string, p, s, std::ptr::null_mut()) +} + // skipped PyOS_InputHook pub const PYOS_STACK_MARGIN: c_int = 2048; diff --git a/src/ffi/tests.rs b/src/ffi/tests.rs index 5aee1618472..b7878c9626c 100644 --- a/src/ffi/tests.rs +++ b/src/ffi/tests.rs @@ -2,10 +2,7 @@ use crate::ffi::*; use crate::types::any::PyAnyMethods; use crate::Python; -#[cfg(all(PyPy, feature = "macros"))] -use crate::types::PyString; - -#[cfg(not(any(Py_LIMITED_API, PyPy)))] +#[cfg(all(not(Py_LIMITED_API), any(not(PyPy), feature = "macros")))] use crate::types::PyString; #[cfg(not(Py_LIMITED_API))] @@ -164,7 +161,6 @@ fn ascii_object_bitfield() { #[test] #[cfg(not(any(Py_LIMITED_API, PyPy)))] -#[cfg_attr(Py_3_10, allow(deprecated))] fn ascii() { Python::with_gil(|py| { // This test relies on implementation details of PyString. @@ -206,7 +202,6 @@ fn ascii() { #[test] #[cfg(not(any(Py_LIMITED_API, PyPy)))] -#[cfg_attr(Py_3_10, allow(deprecated))] fn ucs4() { Python::with_gil(|py| { let s = "哈哈🐈"; From 5749a08b6336d9b557c08f343857e2a965c351e0 Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Thu, 13 Jun 2024 20:24:13 +0200 Subject: [PATCH 109/495] ci: updates for Rust 1.79 (#4244) * ci: updates for Rust 1.79 * ci: fix beta clippy * ci: fix `dead_code` warning on nightly --- Cargo.toml | 2 +- pyo3-build-config/src/impl_.rs | 8 +++---- src/exceptions.rs | 1 + src/ffi/mod.rs | 5 ++--- src/impl_/pyclass.rs | 2 ++ src/instance.rs | 2 ++ src/marker.rs | 22 ++++++++++---------- src/pycell.rs | 14 ++++++------- src/sync.rs | 4 ++-- src/types/any.rs | 8 +++---- src/types/bytearray.rs | 16 +++++++------- src/types/dict.rs | 4 ++-- tests/test_sequence.rs | 2 +- tests/ui/invalid_cancel_handle.stderr | 8 +++---- tests/ui/invalid_pyfunctions.stderr | 8 +++---- tests/ui/invalid_pymethod_receiver.stderr | 4 ++-- tests/ui/invalid_pymethods.stderr | 4 ++-- tests/ui/invalid_pymethods_duplicates.stderr | 14 ++++++------- tests/ui/invalid_result_conversion.stderr | 12 +++++------ tests/ui/static_ref.stderr | 6 ++++-- 20 files changed, 76 insertions(+), 70 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 30b394c344e..ffc87bb83af 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -172,7 +172,7 @@ used_underscore_binding = "warn" [workspace.lints.rust] elided_lifetimes_in_paths = "warn" invalid_doc_attributes = "warn" -rust_2018_idioms = "warn" +rust_2018_idioms = { level = "warn", priority = -1 } rust_2021_prelude_collisions = "warn" unused_lifetimes = "warn" diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index 9ef9f477b9e..1c50c842b91 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -959,11 +959,11 @@ impl CrossCompileEnvVars { /// /// This function relies on PyO3 cross-compiling environment variables: /// -/// * `PYO3_CROSS`: If present, forces PyO3 to configure as a cross-compilation. -/// * `PYO3_CROSS_LIB_DIR`: If present, must be set to the directory containing +/// * `PYO3_CROSS`: If present, forces PyO3 to configure as a cross-compilation. +/// * `PYO3_CROSS_LIB_DIR`: If present, must be set to the directory containing /// the target's libpython DSO and the associated `_sysconfigdata*.py` file for /// Unix-like targets, or the Python DLL import libraries for the Windows target. -/// * `PYO3_CROSS_PYTHON_VERSION`: Major and minor version (e.g. 3.9) of the target Python +/// * `PYO3_CROSS_PYTHON_VERSION`: Major and minor version (e.g. 3.9) of the target Python /// installation. This variable is only needed if PyO3 cannnot determine the version to target /// from `abi3-py3*` features, or if there are multiple versions of Python present in /// `PYO3_CROSS_LIB_DIR`. @@ -1056,7 +1056,7 @@ impl BuildFlags { .iter() .filter(|flag| { config_map - .get_value(&flag.to_string()) + .get_value(flag.to_string()) .map_or(false, |value| value == "1") }) .cloned() diff --git a/src/exceptions.rs b/src/exceptions.rs index e2bb82f9bc1..82bf3b668c6 100644 --- a/src/exceptions.rs +++ b/src/exceptions.rs @@ -180,6 +180,7 @@ macro_rules! import_exception_bound { /// * `name` is the name of the new exception type. /// * `base` is the base class of `MyError`, usually [`PyException`]. /// * `doc` (optional) is the docstring visible to users (with `.__doc__` and `help()`) and +/// /// accompanies your error type in your crate's documentation. /// /// # Examples diff --git a/src/ffi/mod.rs b/src/ffi/mod.rs index ce108223fbe..53cb2695b8c 100644 --- a/src/ffi/mod.rs +++ b/src/ffi/mod.rs @@ -13,11 +13,10 @@ //! The functions in this module lack individual safety documentation, but //! generally the following apply: //! - Pointer arguments have to point to a valid Python object of the correct type, -//! although null pointers are sometimes valid input. +//! although null pointers are sometimes valid input. //! - The vast majority can only be used safely while the GIL is held. //! - Some functions have additional safety requirements, consult the -//! [Python/C API Reference Manual][capi] -//! for more information. +//! [Python/C API Reference Manual][capi] for more information. //! //! [capi]: https://docs.python.org/3/c-api/index.html diff --git a/src/impl_/pyclass.rs b/src/impl_/pyclass.rs index a3e466670a4..84c00acdd74 100644 --- a/src/impl_/pyclass.rs +++ b/src/impl_/pyclass.rs @@ -76,6 +76,7 @@ impl PyClassWeakRef for PyClassDummySlot { /// /// `#[pyclass(dict)]` automatically adds this. #[repr(transparent)] +#[allow(dead_code)] // These are constructed in INIT and used by the macro code pub struct PyClassDictSlot(*mut ffi::PyObject); impl PyClassDict for PyClassDictSlot { @@ -93,6 +94,7 @@ impl PyClassDict for PyClassDictSlot { /// /// `#[pyclass(weakref)]` automatically adds this. #[repr(transparent)] +#[allow(dead_code)] // These are constructed in INIT and used by the macro code pub struct PyClassWeakRefSlot(*mut ffi::PyObject); impl PyClassWeakRef for PyClassWeakRefSlot { diff --git a/src/instance.rs b/src/instance.rs index 2992b273a5b..bc9b68ef38e 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -730,10 +730,12 @@ impl IntoPy for Borrowed<'_, '_, T> { /// Instead, call one of its methods to access the inner object: /// - [`Py::bind`] or [`Py::into_bound`], to borrow a GIL-bound reference to the contained object. /// - [`Py::borrow`], [`Py::try_borrow`], [`Py::borrow_mut`], or [`Py::try_borrow_mut`], +/// /// to get a (mutable) reference to a contained pyclass, using a scheme similar to std's [`RefCell`]. /// See the [guide entry](https://pyo3.rs/latest/class.html#bound-and-interior-mutability) /// for more information. /// - You can call methods directly on `Py` with [`Py::call_bound`], [`Py::call_method_bound`] and friends. +/// /// These require passing in the [`Python<'py>`](crate::Python) token but are otherwise similar to the corresponding /// methods on [`PyAny`]. /// diff --git a/src/marker.rs b/src/marker.rs index b2cbc317841..62d8a89ba53 100644 --- a/src/marker.rs +++ b/src/marker.rs @@ -11,7 +11,7 @@ //! It also supports this pattern that many extension modules employ: //! - Drop the GIL, so that other Python threads can acquire it and make progress themselves //! - Do something independently of the Python interpreter, like IO, a long running calculation or -//! awaiting a future +//! awaiting a future //! - Once that is done, reacquire the GIL //! //! That API is provided by [`Python::allow_threads`] and enforced via the [`Ungil`] bound on the @@ -308,9 +308,9 @@ pub use nightly::Ungil; /// It serves three main purposes: /// - It provides a global API for the Python interpreter, such as [`Python::eval_bound`]. /// - It can be passed to functions that require a proof of holding the GIL, such as -/// [`Py::clone_ref`]. +/// [`Py::clone_ref`]. /// - Its lifetime represents the scope of holding the GIL which can be used to create Rust -/// references that are bound to it, such as `&`[`PyAny`]. +/// references that are bound to it, such as `&`[`PyAny`]. /// /// Note that there are some caveats to using it that you might need to be aware of. See the /// [Deadlocks](#deadlocks) and [Releasing and freeing memory](#releasing-and-freeing-memory) @@ -320,11 +320,11 @@ pub use nightly::Ungil; /// /// The following are the recommended ways to obtain a [`Python`] token, in order of preference: /// - In a function or method annotated with [`#[pyfunction]`](crate::pyfunction) or [`#[pymethods]`](crate::pymethods) you can declare it -/// as a parameter, and PyO3 will pass in the token when Python code calls it. +/// as a parameter, and PyO3 will pass in the token when Python code calls it. /// - If you already have something with a lifetime bound to the GIL, such as `&`[`PyAny`], you can -/// use its `.py()` method to get a token. +/// use its `.py()` method to get a token. /// - When you need to acquire the GIL yourself, such as when calling Python code from Rust, you -/// should call [`Python::with_gil`] to do that and pass your code as a closure to it. +/// should call [`Python::with_gil`] to do that and pass your code as a closure to it. /// /// # Deadlocks /// @@ -1157,12 +1157,12 @@ impl<'unbound> Python<'unbound> { /// # Safety /// /// - This token and any borrowed Python references derived from it can only be safely used - /// whilst the currently executing thread is actually holding the GIL. + /// whilst the currently executing thread is actually holding the GIL. /// - This function creates a token with an *unbounded* lifetime. Safe code can assume that - /// holding a `Python<'py>` token means the GIL is and stays acquired for the lifetime `'py`. - /// If you let it or borrowed Python references escape to safe code you are - /// responsible for bounding the lifetime `'unbound` appropriately. For more on unbounded - /// lifetimes, see the [nomicon]. + /// holding a `Python<'py>` token means the GIL is and stays acquired for the lifetime `'py`. + /// If you let it or borrowed Python references escape to safe code you are + /// responsible for bounding the lifetime `'unbound` appropriately. For more on unbounded + /// lifetimes, see the [nomicon]. /// /// [nomicon]: https://doc.rust-lang.org/nomicon/unbounded-lifetimes.html #[inline] diff --git a/src/pycell.rs b/src/pycell.rs index 9ed6c8aca7d..77d174cb9e1 100644 --- a/src/pycell.rs +++ b/src/pycell.rs @@ -8,14 +8,14 @@ //! pattern. This requires that PyO3 enforces the borrowing rules and it has two mechanisms for //! doing so: //! - Statically it can enforce threadsafe access with the [`Python<'py>`](crate::Python) token. -//! All Rust code holding that token, or anything derived from it, can assume that they have -//! safe access to the Python interpreter's state. For this reason all the native Python objects -//! can be mutated through shared references. +//! All Rust code holding that token, or anything derived from it, can assume that they have +//! safe access to the Python interpreter's state. For this reason all the native Python objects +//! can be mutated through shared references. //! - However, methods and functions in Rust usually *do* need `&mut` references. While PyO3 can -//! use the [`Python<'py>`](crate::Python) token to guarantee thread-safe access to them, it cannot -//! statically guarantee uniqueness of `&mut` references. As such those references have to be tracked -//! dynamically at runtime, using `PyCell` and the other types defined in this module. This works -//! similar to std's [`RefCell`](std::cell::RefCell) type. +//! use the [`Python<'py>`](crate::Python) token to guarantee thread-safe access to them, it cannot +//! statically guarantee uniqueness of `&mut` references. As such those references have to be tracked +//! dynamically at runtime, using `PyCell` and the other types defined in this module. This works +//! similar to std's [`RefCell`](std::cell::RefCell) type. //! //! # When *not* to use PyCell //! diff --git a/src/sync.rs b/src/sync.rs index 856ba84d1e3..a8265eabdbd 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -67,8 +67,8 @@ unsafe impl Sync for GILProtected where T: Send {} /// 2) If the initialization function `f` provided to `get_or_init` (or `get_or_try_init`) /// temporarily releases the GIL (e.g. by calling `Python::import`) then it is possible /// for a second thread to also begin initializing the `GITOnceCell`. Even when this -/// happens `GILOnceCell` guarantees that only **one** write to the cell ever occurs -/// - this is treated as a race, other threads will discard the value they compute and +/// happens `GILOnceCell` guarantees that only **one** write to the cell ever occurs - +/// this is treated as a race, other threads will discard the value they compute and /// return the result of the first complete computation. /// /// # Examples diff --git a/src/types/any.rs b/src/types/any.rs index 06634d69b0e..f2a86ff528d 100644 --- a/src/types/any.rs +++ b/src/types/any.rs @@ -26,13 +26,13 @@ use std::os::raw::c_int; /// with the other [native Python types](crate::types): /// /// - It can only be obtained and used while the GIL is held, -/// therefore its API does not require a [`Python<'py>`](crate::Python) token. +/// therefore its API does not require a [`Python<'py>`](crate::Python) token. /// - It can't be used in situations where the GIL is temporarily released, -/// such as [`Python::allow_threads`](crate::Python::allow_threads)'s closure. +/// such as [`Python::allow_threads`](crate::Python::allow_threads)'s closure. /// - The underlying Python object, if mutable, can be mutated through any reference. /// - It can be converted to the GIL-independent [`Py`]`<`[`PyAny`]`>`, -/// allowing it to outlive the GIL scope. However, using [`Py`]`<`[`PyAny`]`>`'s API -/// *does* require a [`Python<'py>`](crate::Python) token. +/// allowing it to outlive the GIL scope. However, using [`Py`]`<`[`PyAny`]`>`'s API +/// *does* require a [`Python<'py>`](crate::Python) token. /// /// It can be cast to a concrete type with PyAny::downcast (for native Python types only) /// and FromPyObject::extract. See their documentation for more information. diff --git a/src/types/bytearray.rs b/src/types/bytearray.rs index fbd77a38ad0..1a66c71b997 100644 --- a/src/types/bytearray.rs +++ b/src/types/bytearray.rs @@ -149,11 +149,11 @@ impl PyByteArray { /// /// These mutations may occur in Python code as well as from Rust: /// - Calling methods like [`PyByteArray::as_bytes_mut`] and [`PyByteArray::resize`] will - /// invalidate the slice. + /// invalidate the slice. /// - Actions like dropping objects or raising exceptions can invoke `__del__`methods or signal - /// handlers, which may execute arbitrary Python code. This means that if Python code has a - /// reference to the `bytearray` you cannot safely use the vast majority of PyO3's API whilst - /// using the slice. + /// handlers, which may execute arbitrary Python code. This means that if Python code has a + /// reference to the `bytearray` you cannot safely use the vast majority of PyO3's API whilst + /// using the slice. /// /// As a result, this slice should only be used for short-lived operations without executing any /// Python code, such as copying into a Vec. @@ -311,11 +311,11 @@ pub trait PyByteArrayMethods<'py>: crate::sealed::Sealed { /// /// These mutations may occur in Python code as well as from Rust: /// - Calling methods like [`PyByteArrayMethods::as_bytes_mut`] and [`PyByteArrayMethods::resize`] will - /// invalidate the slice. + /// invalidate the slice. /// - Actions like dropping objects or raising exceptions can invoke `__del__`methods or signal - /// handlers, which may execute arbitrary Python code. This means that if Python code has a - /// reference to the `bytearray` you cannot safely use the vast majority of PyO3's API whilst - /// using the slice. + /// handlers, which may execute arbitrary Python code. This means that if Python code has a + /// reference to the `bytearray` you cannot safely use the vast majority of PyO3's API whilst + /// using the slice. /// /// As a result, this slice should only be used for short-lived operations without executing any /// Python code, such as copying into a Vec. diff --git a/src/types/dict.rs b/src/types/dict.rs index cab6d68124b..850e468c672 100644 --- a/src/types/dict.rs +++ b/src/types/dict.rs @@ -851,7 +851,7 @@ mod tests { #[cfg(not(any(PyPy, GraalPy)))] fn test_from_sequence() { Python::with_gil(|py| { - let items = PyList::new_bound(py, &vec![("a", 1), ("b", 2)]); + let items = PyList::new_bound(py, vec![("a", 1), ("b", 2)]); let dict = PyDict::from_sequence_bound(&items).unwrap(); assert_eq!( 1, @@ -882,7 +882,7 @@ mod tests { #[cfg(not(any(PyPy, GraalPy)))] fn test_from_sequence_err() { Python::with_gil(|py| { - let items = PyList::new_bound(py, &vec!["a", "b"]); + let items = PyList::new_bound(py, vec!["a", "b"]); assert!(PyDict::from_sequence_bound(&items).is_err()); }); } diff --git a/tests/test_sequence.rs b/tests/test_sequence.rs index 8adba35c86a..8c29205cc18 100644 --- a/tests/test_sequence.rs +++ b/tests/test_sequence.rs @@ -280,7 +280,7 @@ fn test_generic_list_set() { .items .iter() .zip(&[1u32, 2, 3]) - .all(|(a, b)| a.bind(py).eq(&b.into_py(py)).unwrap())); + .all(|(a, b)| a.bind(py).eq(b.into_py(py)).unwrap())); }); } diff --git a/tests/ui/invalid_cancel_handle.stderr b/tests/ui/invalid_cancel_handle.stderr index f6452611679..feb07d60161 100644 --- a/tests/ui/invalid_cancel_handle.stderr +++ b/tests/ui/invalid_cancel_handle.stderr @@ -45,10 +45,10 @@ error[E0277]: the trait bound `CancelHandle: PyFunctionArgument<'_, '_>` is not | ^^^^ the trait `PyClass` is not implemented for `CancelHandle`, which is required by `CancelHandle: PyFunctionArgument<'_, '_>` | = help: the following other types implement trait `PyFunctionArgument<'a, 'py>`: - Option<&'a pyo3::Bound<'py, T>> + &'a mut pyo3::coroutine::Coroutine &'a pyo3::Bound<'py, T> &'a pyo3::coroutine::Coroutine - &'a mut pyo3::coroutine::Coroutine + Option<&'a pyo3::Bound<'py, T>> = note: required for `CancelHandle` to implement `FromPyObject<'_>` = note: required for `CancelHandle` to implement `FromPyObjectBound<'_, '_>` = note: required for `CancelHandle` to implement `PyFunctionArgument<'_, '_>` @@ -68,10 +68,10 @@ error[E0277]: the trait bound `CancelHandle: PyFunctionArgument<'_, '_>` is not | ^^^^ the trait `Clone` is not implemented for `CancelHandle`, which is required by `CancelHandle: PyFunctionArgument<'_, '_>` | = help: the following other types implement trait `PyFunctionArgument<'a, 'py>`: - Option<&'a pyo3::Bound<'py, T>> + &'a mut pyo3::coroutine::Coroutine &'a pyo3::Bound<'py, T> &'a pyo3::coroutine::Coroutine - &'a mut pyo3::coroutine::Coroutine + Option<&'a pyo3::Bound<'py, T>> = note: required for `CancelHandle` to implement `FromPyObject<'_>` = note: required for `CancelHandle` to implement `FromPyObjectBound<'_, '_>` = note: required for `CancelHandle` to implement `PyFunctionArgument<'_, '_>` diff --git a/tests/ui/invalid_pyfunctions.stderr b/tests/ui/invalid_pyfunctions.stderr index 830f17ee877..0f94ef17254 100644 --- a/tests/ui/invalid_pyfunctions.stderr +++ b/tests/ui/invalid_pyfunctions.stderr @@ -54,10 +54,10 @@ error[E0277]: the trait bound `&str: From>` is not implemented for `&str`, which is required by `BoundRef<'_, '_, pyo3::prelude::PyModule>: Into<_>` | = help: the following other types implement trait `From`: - > + > + > + > >> >> - > - > - > + > = note: required for `BoundRef<'_, '_, pyo3::prelude::PyModule>` to implement `Into<&str>` diff --git a/tests/ui/invalid_pymethod_receiver.stderr b/tests/ui/invalid_pymethod_receiver.stderr index 2c8ec045819..3a8356c4b76 100644 --- a/tests/ui/invalid_pymethod_receiver.stderr +++ b/tests/ui/invalid_pymethod_receiver.stderr @@ -6,9 +6,9 @@ error[E0277]: the trait bound `i32: TryFrom>` is not s | = help: the following other types implement trait `From`: > - > > - > + > > + > = note: required for `BoundRef<'_, '_, MyClass>` to implement `Into` = note: required for `i32` to implement `TryFrom>` diff --git a/tests/ui/invalid_pymethods.stderr b/tests/ui/invalid_pymethods.stderr index 9b090e31adc..879cd3c7ece 100644 --- a/tests/ui/invalid_pymethods.stderr +++ b/tests/ui/invalid_pymethods.stderr @@ -187,8 +187,8 @@ error[E0277]: the trait bound `i32: From>` is not satis | = help: the following other types implement trait `From`: > - > > - > + > > + > = note: required for `BoundRef<'_, '_, PyType>` to implement `Into` diff --git a/tests/ui/invalid_pymethods_duplicates.stderr b/tests/ui/invalid_pymethods_duplicates.stderr index db301336e4f..466e933a6dc 100644 --- a/tests/ui/invalid_pymethods_duplicates.stderr +++ b/tests/ui/invalid_pymethods_duplicates.stderr @@ -16,14 +16,14 @@ error[E0277]: the trait bound `TwoNew: PyTypeInfo` is not satisfied | ^^^^^^ the trait `PyTypeInfo` is not implemented for `TwoNew` | = help: the following other types implement trait `PyTypeInfo`: + CancelledError + IncompleteReadError + InvalidStateError + LimitOverrunError + PanicException PyAny - PyBool - PyByteArray - PyBytes - PyCapsule - PyCode - PyComplex - PyDate + PyArithmeticError + PyAssertionError and $N others error[E0592]: duplicate definitions with name `__pymethod___new____` diff --git a/tests/ui/invalid_result_conversion.stderr b/tests/ui/invalid_result_conversion.stderr index 8da8f49fac3..a18cd6c7b30 100644 --- a/tests/ui/invalid_result_conversion.stderr +++ b/tests/ui/invalid_result_conversion.stderr @@ -5,14 +5,14 @@ error[E0277]: the trait bound `PyErr: From` is not satisfied | ^^^^^^^^^^^^^ the trait `From` is not implemented for `PyErr`, which is required by `MyError: Into` | = help: the following other types implement trait `From`: - >> - > - > - > + > + > >> >> - > - > + > + > + > + >> and $N others = note: required for `MyError` to implement `Into` = note: this error originates in the attribute macro `pyfunction` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/tests/ui/static_ref.stderr b/tests/ui/static_ref.stderr index 6004c4037e5..77c3646745e 100644 --- a/tests/ui/static_ref.stderr +++ b/tests/ui/static_ref.stderr @@ -29,9 +29,11 @@ error[E0597]: `holder_0` does not live long enough | | | | | `holder_0` dropped here while still borrowed | binding `holder_0` declared here - | argument requires that `holder_0` is borrowed for `'static` 5 | fn static_ref(list: &'static Bound<'_, PyList>) -> usize { - | ^^^^^^^ borrowed value does not live long enough + | ^^^^^^- + | | | + | | argument requires that `holder_0` is borrowed for `'static` + | borrowed value does not live long enough error: lifetime may not live long enough --> tests/ui/static_ref.rs:9:1 From 591cdb0bf83d5c5fd024c57db2fe4b33000dc1fc Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Fri, 14 Jun 2024 20:08:35 +0100 Subject: [PATCH 110/495] implement `PyModuleMethods::filename` on PyPy (#4249) --- newsfragments/4249.added.md | 1 + src/types/module.rs | 14 ++++++++++---- 2 files changed, 11 insertions(+), 4 deletions(-) create mode 100644 newsfragments/4249.added.md diff --git a/newsfragments/4249.added.md b/newsfragments/4249.added.md new file mode 100644 index 00000000000..8037f562699 --- /dev/null +++ b/newsfragments/4249.added.md @@ -0,0 +1 @@ +Implement `PyModuleMethods::filename` on PyPy. diff --git a/src/types/module.rs b/src/types/module.rs index f0ae7385f23..20f8305a677 100644 --- a/src/types/module.rs +++ b/src/types/module.rs @@ -220,7 +220,6 @@ impl PyModule { /// Returns the filename (the `__file__` attribute) of the module. /// /// May fail if the module does not have a `__file__` attribute. - #[cfg(not(PyPy))] pub fn filename(&self) -> PyResult<&str> { self.as_borrowed().filename()?.into_gil_ref().to_str() } @@ -429,7 +428,6 @@ pub trait PyModuleMethods<'py>: crate::sealed::Sealed { /// Returns the filename (the `__file__` attribute) of the module. /// /// May fail if the module does not have a `__file__` attribute. - #[cfg(not(PyPy))] fn filename(&self) -> PyResult>; /// Adds an attribute to the module. @@ -644,13 +642,22 @@ impl<'py> PyModuleMethods<'py> for Bound<'py, PyModule> { } } - #[cfg(not(PyPy))] fn filename(&self) -> PyResult> { + #[cfg(not(PyPy))] unsafe { ffi::PyModule_GetFilenameObject(self.as_ptr()) .assume_owned_or_err(self.py()) .downcast_into_unchecked() } + + #[cfg(PyPy)] + { + self.dict() + .get_item("__file__") + .map_err(|_| exceptions::PyAttributeError::new_err("__file__"))? + .downcast_into() + .map_err(PyErr::from) + } } fn add(&self, name: N, value: V) -> PyResult<()> @@ -737,7 +744,6 @@ mod tests { } #[test] - #[cfg(not(PyPy))] fn module_filename() { Python::with_gil(|py| { let site = PyModule::import_bound(py, "site").unwrap(); From 0b2f19b3c9bb6aca0a6ca9316a9f8afeb25a0cfc Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Sun, 16 Jun 2024 08:57:44 +0100 Subject: [PATCH 111/495] fix `__dict__` on Python 3.9 with limited API (#4251) * fix `__dict__` on Python 3.9 with limited API * [review] Icxolu suggestions Co-authored-by: Icxolu <10486322+Icxolu@users.noreply.github.com> * [review] Icxolu * missing import --------- Co-authored-by: Icxolu <10486322+Icxolu@users.noreply.github.com> --- newsfragments/4251.fixed.md | 1 + pytests/src/pyclasses.rs | 12 +++++ pytests/tests/test_pyclasses.py | 8 ++++ src/pyclass/create_type_object.rs | 75 ++++++++++++++++++++++--------- tests/test_class_basics.rs | 13 +++--- 5 files changed, 82 insertions(+), 27 deletions(-) create mode 100644 newsfragments/4251.fixed.md diff --git a/newsfragments/4251.fixed.md b/newsfragments/4251.fixed.md new file mode 100644 index 00000000000..5cc23c7a126 --- /dev/null +++ b/newsfragments/4251.fixed.md @@ -0,0 +1 @@ +Fix `__dict__` attribute missing for `#[pyclass(dict)]` instances when building for `abi3` on Python 3.9. diff --git a/pytests/src/pyclasses.rs b/pytests/src/pyclasses.rs index 6338596b481..f7e4681af70 100644 --- a/pytests/src/pyclasses.rs +++ b/pytests/src/pyclasses.rs @@ -66,12 +66,24 @@ impl AssertingBaseClass { #[pyclass] struct ClassWithoutConstructor; +#[pyclass(dict)] +struct ClassWithDict; + +#[pymethods] +impl ClassWithDict { + #[new] + fn new() -> Self { + ClassWithDict + } +} + #[pymodule] pub fn pyclasses(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; m.add_class::()?; + m.add_class::()?; Ok(()) } diff --git a/pytests/tests/test_pyclasses.py b/pytests/tests/test_pyclasses.py index efef178d489..0c336ecf2e7 100644 --- a/pytests/tests/test_pyclasses.py +++ b/pytests/tests/test_pyclasses.py @@ -84,3 +84,11 @@ def test_no_constructor_defined_propagates_cause(cls: Type): assert exc_info.type is TypeError assert exc_info.value.args == ("No constructor defined",) assert exc_info.value.__context__ is original_error + + +def test_dict(): + d = pyclasses.ClassWithDict() + assert d.__dict__ == {} + + d.foo = 42 + assert d.__dict__ == {"foo": 42} diff --git a/src/pyclass/create_type_object.rs b/src/pyclass/create_type_object.rs index 262d1e8ffc7..01b357763ad 100644 --- a/src/pyclass/create_type_object.rs +++ b/src/pyclass/create_type_object.rs @@ -1,5 +1,3 @@ -use pyo3_ffi::PyType_IS_GC; - use crate::{ exceptions::PyTypeError, ffi, @@ -68,7 +66,7 @@ where has_setitem: false, has_traverse: false, has_clear: false, - has_dict: false, + dict_offset: None, class_flags: 0, #[cfg(all(not(Py_3_9), not(Py_LIMITED_API)))] buffer_procs: Default::default(), @@ -121,7 +119,7 @@ struct PyTypeBuilder { has_setitem: bool, has_traverse: bool, has_clear: bool, - has_dict: bool, + dict_offset: Option, class_flags: c_ulong, // Before Python 3.9, need to patch in buffer methods manually (they don't work in slots) #[cfg(all(not(Py_3_9), not(Py_LIMITED_API)))] @@ -218,16 +216,56 @@ impl PyTypeBuilder { }) .collect::>()?; - // PyPy doesn't automatically add __dict__ getter / setter. - // PyObject_GenericGetDict not in the limited API until Python 3.10. - if self.has_dict { - #[cfg(not(any(PyPy, all(Py_LIMITED_API, not(Py_3_10)))))] + // PyPy automatically adds __dict__ getter / setter. + #[cfg(not(PyPy))] + // Supported on unlimited API for all versions, and on 3.9+ for limited API + #[cfg(any(Py_3_9, not(Py_LIMITED_API)))] + if let Some(dict_offset) = self.dict_offset { + let get_dict; + let closure; + // PyObject_GenericGetDict not in the limited API until Python 3.10. + #[cfg(any(not(Py_LIMITED_API), Py_3_10))] + { + let _ = dict_offset; + get_dict = ffi::PyObject_GenericGetDict; + closure = ptr::null_mut(); + } + + // ... so we write a basic implementation ourselves + #[cfg(not(any(not(Py_LIMITED_API), Py_3_10)))] + { + extern "C" fn get_dict_impl( + object: *mut ffi::PyObject, + closure: *mut c_void, + ) -> *mut ffi::PyObject { + unsafe { + trampoline(|_| { + let dict_offset = closure as ffi::Py_ssize_t; + // we don't support negative dict_offset here; PyO3 doesn't set it negative + assert!(dict_offset > 0); + // TODO: use `.byte_offset` on MSRV 1.75 + let dict_ptr = object + .cast::() + .offset(dict_offset) + .cast::<*mut ffi::PyObject>(); + if (*dict_ptr).is_null() { + std::ptr::write(dict_ptr, ffi::PyDict_New()); + } + Ok(ffi::_Py_XNewRef(*dict_ptr)) + }) + } + } + + get_dict = get_dict_impl; + closure = dict_offset as _; + } + property_defs.push(ffi::PyGetSetDef { name: "__dict__\0".as_ptr().cast(), - get: Some(ffi::PyObject_GenericGetDict), + get: Some(get_dict), set: Some(ffi::PyObject_GenericSetDict), doc: ptr::null(), - closure: ptr::null_mut(), + closure, }); } @@ -315,20 +353,17 @@ impl PyTypeBuilder { dict_offset: Option, #[allow(unused_variables)] weaklist_offset: Option, ) -> Self { - self.has_dict = dict_offset.is_some(); + self.dict_offset = dict_offset; #[cfg(Py_3_9)] { #[inline(always)] - fn offset_def( - name: &'static str, - offset: ffi::Py_ssize_t, - ) -> ffi::structmember::PyMemberDef { - ffi::structmember::PyMemberDef { - name: name.as_ptr() as _, - type_code: ffi::structmember::T_PYSSIZET, + fn offset_def(name: &'static str, offset: ffi::Py_ssize_t) -> ffi::PyMemberDef { + ffi::PyMemberDef { + name: name.as_ptr().cast(), + type_code: ffi::Py_T_PYSSIZET, offset, - flags: ffi::structmember::READONLY, + flags: ffi::Py_READONLY, doc: std::ptr::null_mut(), } } @@ -391,7 +426,7 @@ impl PyTypeBuilder { unsafe { self.push_slot(ffi::Py_tp_new, no_constructor_defined as *mut c_void) } } - let tp_dealloc = if self.has_traverse || unsafe { PyType_IS_GC(self.tp_base) == 1 } { + let tp_dealloc = if self.has_traverse || unsafe { ffi::PyType_IS_GC(self.tp_base) == 1 } { self.tp_dealloc_with_gc } else { self.tp_dealloc diff --git a/tests/test_class_basics.rs b/tests/test_class_basics.rs index 325b3d52c3d..bc8d2dab275 100644 --- a/tests/test_class_basics.rs +++ b/tests/test_class_basics.rs @@ -435,7 +435,7 @@ struct DunderDictSupport { } #[test] -#[cfg_attr(all(Py_LIMITED_API, not(Py_3_9)), ignore)] +#[cfg(any(Py_3_9, not(Py_LIMITED_API)))] fn dunder_dict_support() { Python::with_gil(|py| { let inst = Py::new( @@ -456,9 +456,8 @@ fn dunder_dict_support() { }); } -// Accessing inst.__dict__ only supported in limited API from Python 3.10 #[test] -#[cfg_attr(all(Py_LIMITED_API, not(Py_3_10)), ignore)] +#[cfg(any(Py_3_9, not(Py_LIMITED_API)))] fn access_dunder_dict() { Python::with_gil(|py| { let inst = Py::new( @@ -486,7 +485,7 @@ struct InheritDict { } #[test] -#[cfg_attr(all(Py_LIMITED_API, not(Py_3_9)), ignore)] +#[cfg(any(Py_3_9, not(Py_LIMITED_API)))] fn inherited_dict() { Python::with_gil(|py| { let inst = Py::new( @@ -517,7 +516,7 @@ struct WeakRefDunderDictSupport { } #[test] -#[cfg_attr(all(Py_LIMITED_API, not(Py_3_9)), ignore)] +#[cfg(any(Py_3_9, not(Py_LIMITED_API)))] fn weakref_dunder_dict_support() { Python::with_gil(|py| { let inst = Py::new( @@ -541,7 +540,7 @@ struct WeakRefSupport { } #[test] -#[cfg_attr(all(Py_LIMITED_API, not(Py_3_9)), ignore)] +#[cfg(any(Py_3_9, not(Py_LIMITED_API)))] fn weakref_support() { Python::with_gil(|py| { let inst = Py::new( @@ -566,7 +565,7 @@ struct InheritWeakRef { } #[test] -#[cfg_attr(all(Py_LIMITED_API, not(Py_3_9)), ignore)] +#[cfg(any(Py_3_9, not(Py_LIMITED_API)))] fn inherited_weakref() { Python::with_gil(|py| { let inst = Py::new( From 9648d595a5a9339f52a62821ad31f77706a09b2f Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Sun, 16 Jun 2024 09:19:21 +0100 Subject: [PATCH 112/495] implement `PartialEq` for `Bound<'py, PyString>` (#4245) * implement `PartialEq` for `Bound<'py, PyString>` * fixup conditional code * document equality semantics for `Bound<'_, PyString>` * fix doc example --- newsfragments/4245.added.md | 1 + pyo3-ffi/src/unicodeobject.rs | 9 ++ src/instance.rs | 8 +- src/types/bytearray.rs | 13 +-- src/types/module.rs | 10 +- src/types/string.rs | 176 +++++++++++++++++++++++++++++++++- tests/test_proto_methods.rs | 7 +- 7 files changed, 194 insertions(+), 30 deletions(-) create mode 100644 newsfragments/4245.added.md diff --git a/newsfragments/4245.added.md b/newsfragments/4245.added.md new file mode 100644 index 00000000000..692fb277422 --- /dev/null +++ b/newsfragments/4245.added.md @@ -0,0 +1 @@ +Implement `PartialEq` for `Bound<'py, PyString>`. diff --git a/pyo3-ffi/src/unicodeobject.rs b/pyo3-ffi/src/unicodeobject.rs index 087160a1efc..519bbf261f9 100644 --- a/pyo3-ffi/src/unicodeobject.rs +++ b/pyo3-ffi/src/unicodeobject.rs @@ -328,6 +328,15 @@ extern "C" { pub fn PyUnicode_Compare(left: *mut PyObject, right: *mut PyObject) -> c_int; #[cfg_attr(PyPy, link_name = "PyPyUnicode_CompareWithASCIIString")] pub fn PyUnicode_CompareWithASCIIString(left: *mut PyObject, right: *const c_char) -> c_int; + #[cfg(Py_3_13)] + pub fn PyUnicode_EqualToUTF8(unicode: *mut PyObject, string: *const c_char) -> c_int; + #[cfg(Py_3_13)] + pub fn PyUnicode_EqualToUTF8AndSize( + unicode: *mut PyObject, + string: *const c_char, + size: Py_ssize_t, + ) -> c_int; + pub fn PyUnicode_RichCompare( left: *mut PyObject, right: *mut PyObject, diff --git a/src/instance.rs b/src/instance.rs index bc9b68ef38e..4703dd12a94 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -2010,9 +2010,7 @@ impl PyObject { #[cfg(test)] mod tests { use super::{Bound, Py, PyObject}; - use crate::types::any::PyAnyMethods; - use crate::types::{dict::IntoPyDict, PyDict, PyString}; - use crate::types::{PyCapsule, PyStringMethods}; + use crate::types::{dict::IntoPyDict, PyAnyMethods, PyCapsule, PyDict, PyString}; use crate::{ffi, Borrowed, PyAny, PyResult, Python, ToPyObject}; #[test] @@ -2021,7 +2019,7 @@ mod tests { let obj = py.get_type_bound::().to_object(py); let assert_repr = |obj: &Bound<'_, PyAny>, expected: &str| { - assert_eq!(obj.repr().unwrap().to_cow().unwrap(), expected); + assert_eq!(obj.repr().unwrap(), expected); }; assert_repr(obj.call0(py).unwrap().bind(py), "{}"); @@ -2221,7 +2219,7 @@ a = A() let obj_unbound: Py = obj.unbind(); let obj: Bound<'_, PyString> = obj_unbound.into_bound(py); - assert_eq!(obj.to_cow().unwrap(), "hello world"); + assert_eq!(obj, "hello world"); }); } diff --git a/src/types/bytearray.rs b/src/types/bytearray.rs index 1a66c71b997..c411e830340 100644 --- a/src/types/bytearray.rs +++ b/src/types/bytearray.rs @@ -515,12 +515,8 @@ impl<'py> TryFrom<&Bound<'py, PyAny>> for Bound<'py, PyByteArray> { #[cfg(test)] mod tests { - use crate::types::any::PyAnyMethods; - use crate::types::bytearray::PyByteArrayMethods; - use crate::types::string::PyStringMethods; - use crate::types::PyByteArray; - use crate::{exceptions, Bound, PyAny}; - use crate::{PyObject, Python}; + use crate::types::{PyAnyMethods, PyByteArray, PyByteArrayMethods}; + use crate::{exceptions, Bound, PyAny, PyObject, Python}; #[test] fn test_len() { @@ -555,10 +551,7 @@ mod tests { slice[0..5].copy_from_slice(b"Hi..."); - assert_eq!( - bytearray.str().unwrap().to_cow().unwrap(), - "bytearray(b'Hi... Python')" - ); + assert_eq!(bytearray.str().unwrap(), "bytearray(b'Hi... Python')"); }); } diff --git a/src/types/module.rs b/src/types/module.rs index 20f8305a677..e866ec9cb48 100644 --- a/src/types/module.rs +++ b/src/types/module.rs @@ -37,7 +37,7 @@ impl PyModule { /// Python::with_gil(|py| -> PyResult<()> { /// let module = PyModule::new_bound(py, "my_module")?; /// - /// assert_eq!(module.name()?.to_cow()?, "my_module"); + /// assert_eq!(module.name()?, "my_module"); /// Ok(()) /// })?; /// # Ok(())} @@ -728,7 +728,7 @@ fn __name__(py: Python<'_>) -> &Bound<'_, PyString> { #[cfg(test)] mod tests { use crate::{ - types::{module::PyModuleMethods, string::PyStringMethods, PyModule}, + types::{module::PyModuleMethods, PyModule}, Python, }; @@ -736,15 +736,13 @@ mod tests { fn module_import_and_name() { Python::with_gil(|py| { let builtins = PyModule::import_bound(py, "builtins").unwrap(); - assert_eq!( - builtins.name().unwrap().to_cow().unwrap().as_ref(), - "builtins" - ); + assert_eq!(builtins.name().unwrap(), "builtins"); }) } #[test] fn module_filename() { + use crate::types::string::PyStringMethods; Python::with_gil(|py| { let site = PyModule::import_bound(py, "site").unwrap(); assert!(site diff --git a/src/types/string.rs b/src/types/string.rs index 0582a900870..8556e41dcce 100644 --- a/src/types/string.rs +++ b/src/types/string.rs @@ -123,7 +123,33 @@ impl<'a> PyStringData<'a> { /// Represents a Python `string` (a Unicode string object). /// -/// This type is immutable. +/// This type is only seen inside PyO3's smart pointers as [`Py`], [`Bound<'py, PyString>`], +/// and [`Borrowed<'a, 'py, PyString>`]. +/// +/// All functionality on this type is implemented through the [`PyStringMethods`] trait. +/// +/// # Equality +/// +/// For convenience, [`Bound<'py, PyString>`] implements [`PartialEq`] to allow comparing the +/// data in the Python string to a Rust UTF-8 string slice. +/// +/// This is not always the most appropriate way to compare Python strings, as Python string subclasses +/// may have different equality semantics. In situations where subclasses overriding equality might be +/// relevant, use [`PyAnyMethods::eq`], at cost of the additional overhead of a Python method call. +/// +/// ```rust +/// # use pyo3::prelude::*; +/// use pyo3::types::PyString; +/// +/// # Python::with_gil(|py| { +/// let py_string = PyString::new_bound(py, "foo"); +/// // via PartialEq +/// assert_eq!(py_string, "foo"); +/// +/// // via Python equality +/// assert!(py_string.as_any().eq("foo").unwrap()); +/// # }); +/// ``` #[repr(transparent)] pub struct PyString(PyAny); @@ -490,6 +516,118 @@ impl IntoPy> for &'_ Py { } } +/// Compares whether the data in the Python string is equal to the given UTF8. +/// +/// In some cases Python equality might be more appropriate; see the note on [`PyString`]. +impl PartialEq for Bound<'_, PyString> { + #[inline] + fn eq(&self, other: &str) -> bool { + self.as_borrowed() == *other + } +} + +/// Compares whether the data in the Python string is equal to the given UTF8. +/// +/// In some cases Python equality might be more appropriate; see the note on [`PyString`]. +impl PartialEq<&'_ str> for Bound<'_, PyString> { + #[inline] + fn eq(&self, other: &&str) -> bool { + self.as_borrowed() == **other + } +} + +/// Compares whether the data in the Python string is equal to the given UTF8. +/// +/// In some cases Python equality might be more appropriate; see the note on [`PyString`]. +impl PartialEq> for str { + #[inline] + fn eq(&self, other: &Bound<'_, PyString>) -> bool { + *self == other.as_borrowed() + } +} + +/// Compares whether the data in the Python string is equal to the given UTF8. +/// +/// In some cases Python equality might be more appropriate; see the note on [`PyString`]. +impl PartialEq<&'_ Bound<'_, PyString>> for str { + #[inline] + fn eq(&self, other: &&Bound<'_, PyString>) -> bool { + *self == other.as_borrowed() + } +} + +/// Compares whether the data in the Python string is equal to the given UTF8. +/// +/// In some cases Python equality might be more appropriate; see the note on [`PyString`]. +impl PartialEq> for &'_ str { + #[inline] + fn eq(&self, other: &Bound<'_, PyString>) -> bool { + **self == other.as_borrowed() + } +} + +/// Compares whether the data in the Python string is equal to the given UTF8. +/// +/// In some cases Python equality might be more appropriate; see the note on [`PyString`]. +impl PartialEq for &'_ Bound<'_, PyString> { + #[inline] + fn eq(&self, other: &str) -> bool { + self.as_borrowed() == other + } +} + +/// Compares whether the data in the Python string is equal to the given UTF8. +/// +/// In some cases Python equality might be more appropriate; see the note on [`PyString`]. +impl PartialEq for Borrowed<'_, '_, PyString> { + #[inline] + fn eq(&self, other: &str) -> bool { + #[cfg(not(Py_3_13))] + { + self.to_cow().map_or(false, |s| s == other) + } + + #[cfg(Py_3_13)] + unsafe { + ffi::PyUnicode_EqualToUTF8AndSize( + self.as_ptr(), + other.as_ptr().cast(), + other.len() as _, + ) == 1 + } + } +} + +/// Compares whether the data in the Python string is equal to the given UTF8. +/// +/// In some cases Python equality might be more appropriate; see the note on [`PyString`]. +impl PartialEq<&str> for Borrowed<'_, '_, PyString> { + #[inline] + fn eq(&self, other: &&str) -> bool { + *self == **other + } +} + +/// Compares whether the data in the Python string is equal to the given UTF8. +/// +/// In some cases Python equality might be more appropriate; see the note on [`PyString`]. +impl PartialEq> for str { + #[inline] + fn eq(&self, other: &Borrowed<'_, '_, PyString>) -> bool { + other == self + } +} + +/// Compares whether the data in the Python string is equal to the given UTF8. +/// +/// In some cases Python equality might be more appropriate; see the note on [`PyString`]. +impl PartialEq> for &'_ str { + #[inline] + fn eq(&self, other: &Borrowed<'_, '_, PyString>) -> bool { + other == self + } +} + #[cfg(test)] mod tests { use super::*; @@ -708,15 +846,15 @@ mod tests { fn test_intern_string() { Python::with_gil(|py| { let py_string1 = PyString::intern_bound(py, "foo"); - assert_eq!(py_string1.to_cow().unwrap(), "foo"); + assert_eq!(py_string1, "foo"); let py_string2 = PyString::intern_bound(py, "foo"); - assert_eq!(py_string2.to_cow().unwrap(), "foo"); + assert_eq!(py_string2, "foo"); assert_eq!(py_string1.as_ptr(), py_string2.as_ptr()); let py_string3 = PyString::intern_bound(py, "bar"); - assert_eq!(py_string3.to_cow().unwrap(), "bar"); + assert_eq!(py_string3, "bar"); assert_ne!(py_string1.as_ptr(), py_string3.as_ptr()); }); @@ -762,4 +900,34 @@ mod tests { assert_eq!(py_string.to_string_lossy(py), "🐈 Hello ���World"); }) } + + #[test] + fn test_comparisons() { + Python::with_gil(|py| { + let s = "hello, world"; + let py_string = PyString::new_bound(py, s); + + assert_eq!(py_string, "hello, world"); + + assert_eq!(py_string, s); + assert_eq!(&py_string, s); + assert_eq!(s, py_string); + assert_eq!(s, &py_string); + + assert_eq!(py_string, *s); + assert_eq!(&py_string, *s); + assert_eq!(*s, py_string); + assert_eq!(*s, &py_string); + + let py_string = py_string.as_borrowed(); + + assert_eq!(py_string, s); + assert_eq!(&py_string, s); + assert_eq!(s, py_string); + assert_eq!(s, &py_string); + + assert_eq!(py_string, *s); + assert_eq!(*s, py_string); + }) + } } diff --git a/tests/test_proto_methods.rs b/tests/test_proto_methods.rs index 5f0fa105e1f..06e0d45e4a6 100644 --- a/tests/test_proto_methods.rs +++ b/tests/test_proto_methods.rs @@ -131,7 +131,7 @@ fn test_delattr() { fn test_str() { Python::with_gil(|py| { let example_py = make_example(py); - assert_eq!(example_py.str().unwrap().to_cow().unwrap(), "5"); + assert_eq!(example_py.str().unwrap(), "5"); }) } @@ -139,10 +139,7 @@ fn test_str() { fn test_repr() { Python::with_gil(|py| { let example_py = make_example(py); - assert_eq!( - example_py.repr().unwrap().to_cow().unwrap(), - "ExampleClass(value=5)" - ); + assert_eq!(example_py.repr().unwrap(), "ExampleClass(value=5)"); }) } From 79591f2161ae58c00ecc5eff5bfaf9d924647d7d Mon Sep 17 00:00:00 2001 From: Code Apprentice Date: Sun, 16 Jun 2024 04:23:03 -0600 Subject: [PATCH 113/495] Add error messages for unsupported macro features on compilation (#4194) * First implementation * tweak error message wording * Fix boolean logic * Remove redundant parens * Add test for weakref error * Fix test * Reword error message * Add expected error output * Rinse and repeat for `dict` * Add test output file * Ignore Rust Rover config files * cargo fmt * Add newsfragment * Update newsfragments/4194.added.md Co-authored-by: David Hewitt * Use ensure_spanned! macro Co-authored-by: David Hewitt * Use ensure_spanned! macro for weakref error, too Co-authored-by: David Hewitt * Revert "Ignore Rust Rover config files" This reverts commit 6c8a2eec581ed250ec792d8465772d649b0a3199. * Update wording for error message Co-authored-by: Icxolu <10486322+Icxolu@users.noreply.github.com> * Update weakref error message, too * Refactor constant to a pyversions module * Fix compiler errors * Another wording update Co-authored-by: David Hewitt * And make weakref wording the same * Fix compiler error due to using weakref in our own code * Fix after merge * apply conditional pyclass * update conditional compilation in tests --------- Co-authored-by: cojmeister Co-authored-by: David Hewitt Co-authored-by: Icxolu <10486322+Icxolu@users.noreply.github.com> --- newsfragments/4194.added.md | 1 + pyo3-macros-backend/src/lib.rs | 1 + pyo3-macros-backend/src/pyclass.rs | 36 +++++++++++----- pyo3-macros-backend/src/pyversions.rs | 3 ++ src/tests/hygiene/pyclass.rs | 13 +++++- tests/test_class_basics.rs | 50 +++++++++++++++++++++ tests/test_compile_error.rs | 4 ++ tests/test_unsendable_dict.rs | 49 --------------------- tests/test_various.rs | 62 ++++++++++++++------------- tests/ui/abi3_dict.rs | 7 +++ tests/ui/abi3_dict.stderr | 5 +++ tests/ui/abi3_weakref.rs | 7 +++ tests/ui/abi3_weakref.stderr | 5 +++ 13 files changed, 151 insertions(+), 92 deletions(-) create mode 100644 newsfragments/4194.added.md create mode 100644 pyo3-macros-backend/src/pyversions.rs delete mode 100644 tests/test_unsendable_dict.rs create mode 100644 tests/ui/abi3_dict.rs create mode 100644 tests/ui/abi3_dict.stderr create mode 100644 tests/ui/abi3_weakref.rs create mode 100644 tests/ui/abi3_weakref.stderr diff --git a/newsfragments/4194.added.md b/newsfragments/4194.added.md new file mode 100644 index 00000000000..6f032138d25 --- /dev/null +++ b/newsfragments/4194.added.md @@ -0,0 +1 @@ +Added error messages when using `weakref` or `dict` when compiling for `abi3` for Python older than 3.9 diff --git a/pyo3-macros-backend/src/lib.rs b/pyo3-macros-backend/src/lib.rs index a9d75a2a6fe..5d7437a4295 100644 --- a/pyo3-macros-backend/src/lib.rs +++ b/pyo3-macros-backend/src/lib.rs @@ -19,6 +19,7 @@ mod pyclass; mod pyfunction; mod pyimpl; mod pymethod; +mod pyversions; mod quotes; pub use frompyobject::build_derive_from_pyobject; diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index 403e5e8e9dc..9484b3dbf26 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -1,5 +1,12 @@ use std::borrow::Cow; +use proc_macro2::{Ident, Span, TokenStream}; +use quote::{format_ident, quote, quote_spanned}; +use syn::ext::IdentExt; +use syn::parse::{Parse, ParseStream}; +use syn::punctuated::Punctuated; +use syn::{parse_quote, parse_quote_spanned, spanned::Spanned, Result, Token}; + use crate::attributes::kw::frozen; use crate::attributes::{ self, kw, take_pyo3_options, CrateAttribute, ExtendsAttribute, FreelistAttribute, @@ -14,16 +21,9 @@ use crate::pymethod::{ impl_py_getter_def, impl_py_setter_def, MethodAndMethodDef, MethodAndSlotDef, PropertyType, SlotDef, __GETITEM__, __HASH__, __INT__, __LEN__, __REPR__, __RICHCMP__, }; -use crate::utils::Ctx; use crate::utils::{self, apply_renaming_rule, PythonDoc}; -use crate::PyFunctionOptions; -use proc_macro2::{Ident, Span, TokenStream}; -use quote::{format_ident, quote, quote_spanned}; -use syn::ext::IdentExt; -use syn::parse::{Parse, ParseStream}; -use syn::parse_quote_spanned; -use syn::punctuated::Punctuated; -use syn::{parse_quote, spanned::Spanned, Result, Token}; +use crate::utils::{is_abi3, Ctx}; +use crate::{pyversions, PyFunctionOptions}; /// If the class is derived from a Rust `struct` or `enum`. #[derive(Copy, Clone, Debug, PartialEq, Eq)] @@ -180,9 +180,17 @@ impl PyClassPyO3Options { }; } + let python_version = pyo3_build_config::get().version; + match option { PyClassPyO3Option::Crate(krate) => set_option!(krate), - PyClassPyO3Option::Dict(dict) => set_option!(dict), + PyClassPyO3Option::Dict(dict) => { + ensure_spanned!( + python_version >= pyversions::PY_3_9 || !is_abi3(), + dict.span() => "`dict` requires Python >= 3.9 when using the `abi3` feature" + ); + set_option!(dict); + } PyClassPyO3Option::Eq(eq) => set_option!(eq), PyClassPyO3Option::EqInt(eq_int) => set_option!(eq_int), PyClassPyO3Option::Extends(extends) => set_option!(extends), @@ -199,7 +207,13 @@ impl PyClassPyO3Options { PyClassPyO3Option::SetAll(set_all) => set_option!(set_all), PyClassPyO3Option::Subclass(subclass) => set_option!(subclass), PyClassPyO3Option::Unsendable(unsendable) => set_option!(unsendable), - PyClassPyO3Option::Weakref(weakref) => set_option!(weakref), + PyClassPyO3Option::Weakref(weakref) => { + ensure_spanned!( + python_version >= pyversions::PY_3_9 || !is_abi3(), + weakref.span() => "`weakref` requires Python >= 3.9 when using the `abi3` feature" + ); + set_option!(weakref); + } } Ok(()) } diff --git a/pyo3-macros-backend/src/pyversions.rs b/pyo3-macros-backend/src/pyversions.rs new file mode 100644 index 00000000000..23d25bf8cce --- /dev/null +++ b/pyo3-macros-backend/src/pyversions.rs @@ -0,0 +1,3 @@ +use pyo3_build_config::PythonVersion; + +pub const PY_3_9: PythonVersion = PythonVersion { major: 3, minor: 9 }; diff --git a/src/tests/hygiene/pyclass.rs b/src/tests/hygiene/pyclass.rs index 27a6b388769..8654e538728 100644 --- a/src/tests/hygiene/pyclass.rs +++ b/src/tests/hygiene/pyclass.rs @@ -10,15 +10,24 @@ pub struct Foo; #[pyo3(crate = "crate")] pub struct Foo2; -#[crate::pyclass( +#[cfg_attr(any(Py_3_9, not(Py_LIMITED_API)), crate::pyclass( name = "ActuallyBar", freelist = 8, + unsendable, + subclass, + extends = crate::types::PyAny, + module = "Spam", weakref, + dict +))] +#[cfg_attr(not(any(Py_3_9, not(Py_LIMITED_API))), crate::pyclass( + name = "ActuallyBar", + freelist = 8, unsendable, subclass, extends = crate::types::PyAny, module = "Spam" -)] +))] #[pyo3(crate = "crate")] pub struct Bar { #[pyo3(get, set)] diff --git a/tests/test_class_basics.rs b/tests/test_class_basics.rs index bc8d2dab275..19547cffba9 100644 --- a/tests/test_class_basics.rs +++ b/tests/test_class_basics.rs @@ -428,6 +428,7 @@ fn test_tuple_struct_class() { }); } +#[cfg(any(Py_3_9, not(Py_LIMITED_API)))] #[pyclass(dict, subclass)] struct DunderDictSupport { // Make sure that dict_offset runs with non-zero sized Self @@ -479,6 +480,7 @@ fn access_dunder_dict() { } // If the base class has dict support, child class also has dict +#[cfg(any(Py_3_9, not(Py_LIMITED_API)))] #[pyclass(extends=DunderDictSupport)] struct InheritDict { _value: usize, @@ -509,6 +511,7 @@ fn inherited_dict() { }); } +#[cfg(any(Py_3_9, not(Py_LIMITED_API)))] #[pyclass(weakref, dict)] struct WeakRefDunderDictSupport { // Make sure that weaklist_offset runs with non-zero sized Self @@ -534,6 +537,7 @@ fn weakref_dunder_dict_support() { }); } +#[cfg(any(Py_3_9, not(Py_LIMITED_API)))] #[pyclass(weakref, subclass)] struct WeakRefSupport { _pad: [u8; 32], @@ -559,6 +563,7 @@ fn weakref_support() { } // If the base class has weakref support, child class also has weakref. +#[cfg(any(Py_3_9, not(Py_LIMITED_API)))] #[pyclass(extends=WeakRefSupport)] struct InheritWeakRef { _value: usize, @@ -666,3 +671,48 @@ fn drop_unsendable_elsewhere() { capture.borrow_mut(py).uninstall(py); }); } + +#[test] +#[cfg(any(Py_3_9, not(Py_LIMITED_API)))] +fn test_unsendable_dict() { + #[pyclass(dict, unsendable)] + struct UnsendableDictClass {} + + #[pymethods] + impl UnsendableDictClass { + #[new] + fn new() -> Self { + UnsendableDictClass {} + } + } + + Python::with_gil(|py| { + let inst = Py::new(py, UnsendableDictClass {}).unwrap(); + py_run!(py, inst, "assert inst.__dict__ == {}"); + }); +} + +#[test] +#[cfg(any(Py_3_9, not(Py_LIMITED_API)))] +fn test_unsendable_dict_with_weakref() { + #[pyclass(dict, unsendable, weakref)] + struct UnsendableDictClassWithWeakRef {} + + #[pymethods] + impl UnsendableDictClassWithWeakRef { + #[new] + fn new() -> Self { + Self {} + } + } + + Python::with_gil(|py| { + let inst = Py::new(py, UnsendableDictClassWithWeakRef {}).unwrap(); + py_run!(py, inst, "assert inst.__dict__ == {}"); + py_run!( + py, + inst, + "import weakref; assert weakref.ref(inst)() is inst; inst.a = 1; assert inst.a == 1" + ); + }); +} diff --git a/tests/test_compile_error.rs b/tests/test_compile_error.rs index 2b32de2fcfa..c978e413d84 100644 --- a/tests/test_compile_error.rs +++ b/tests/test_compile_error.rs @@ -67,4 +67,8 @@ fn test_compile_errors() { #[cfg(all(Py_LIMITED_API, not(feature = "experimental-async")))] // output changes with async feature t.compile_fail("tests/ui/abi3_inheritance.rs"); + #[cfg(all(Py_LIMITED_API, not(Py_3_9)))] + t.compile_fail("tests/ui/abi3_weakref.rs"); + #[cfg(all(Py_LIMITED_API, not(Py_3_9)))] + t.compile_fail("tests/ui/abi3_dict.rs"); } diff --git a/tests/test_unsendable_dict.rs b/tests/test_unsendable_dict.rs deleted file mode 100644 index a39aa1ab714..00000000000 --- a/tests/test_unsendable_dict.rs +++ /dev/null @@ -1,49 +0,0 @@ -#![cfg(feature = "macros")] - -use pyo3::prelude::*; -use pyo3::py_run; - -#[pyclass(dict, unsendable)] -struct UnsendableDictClass {} - -#[pymethods] -impl UnsendableDictClass { - #[new] - fn new() -> Self { - UnsendableDictClass {} - } -} - -#[test] -#[cfg_attr(all(Py_LIMITED_API, not(Py_3_10)), ignore)] -fn test_unsendable_dict() { - Python::with_gil(|py| { - let inst = Py::new(py, UnsendableDictClass {}).unwrap(); - py_run!(py, inst, "assert inst.__dict__ == {}"); - }); -} - -#[pyclass(dict, unsendable, weakref)] -struct UnsendableDictClassWithWeakRef {} - -#[pymethods] -impl UnsendableDictClassWithWeakRef { - #[new] - fn new() -> Self { - Self {} - } -} - -#[test] -#[cfg_attr(all(Py_LIMITED_API, not(Py_3_10)), ignore)] -fn test_unsendable_dict_with_weakref() { - Python::with_gil(|py| { - let inst = Py::new(py, UnsendableDictClassWithWeakRef {}).unwrap(); - py_run!(py, inst, "assert inst.__dict__ == {}"); - py_run!( - py, - inst, - "import weakref; assert weakref.ref(inst)() is inst; inst.a = 1; assert inst.a == 1" - ); - }); -} diff --git a/tests/test_various.rs b/tests/test_various.rs index 0e619f49a28..dfc6498159c 100644 --- a/tests/test_various.rs +++ b/tests/test_various.rs @@ -2,7 +2,7 @@ use pyo3::prelude::*; use pyo3::py_run; -use pyo3::types::{PyDict, PyTuple}; +use pyo3::types::PyTuple; use std::fmt; @@ -113,39 +113,41 @@ fn pytuple_pyclass_iter() { }); } -#[pyclass(dict, module = "test_module")] -struct PickleSupport {} - -#[pymethods] -impl PickleSupport { - #[new] - fn new() -> PickleSupport { - PickleSupport {} +#[test] +#[cfg(any(Py_3_9, not(Py_LIMITED_API)))] +fn test_pickle() { + use pyo3::types::PyDict; + + #[pyclass(dict, module = "test_module")] + struct PickleSupport {} + + #[pymethods] + impl PickleSupport { + #[new] + fn new() -> PickleSupport { + PickleSupport {} + } + + pub fn __reduce__<'py>( + slf: &Bound<'py, Self>, + py: Python<'py>, + ) -> PyResult<(PyObject, Bound<'py, PyTuple>, PyObject)> { + let cls = slf.to_object(py).getattr(py, "__class__")?; + let dict = slf.to_object(py).getattr(py, "__dict__")?; + Ok((cls, PyTuple::empty_bound(py), dict)) + } } - pub fn __reduce__<'py>( - slf: &Bound<'py, Self>, - py: Python<'py>, - ) -> PyResult<(PyObject, Bound<'py, PyTuple>, PyObject)> { - let cls = slf.to_object(py).getattr(py, "__class__")?; - let dict = slf.to_object(py).getattr(py, "__dict__")?; - Ok((cls, PyTuple::empty_bound(py), dict)) + fn add_module(module: Bound<'_, PyModule>) -> PyResult<()> { + PyModule::import_bound(module.py(), "sys")? + .dict() + .get_item("modules") + .unwrap() + .unwrap() + .downcast::()? + .set_item(module.name()?, module) } -} -fn add_module(module: Bound<'_, PyModule>) -> PyResult<()> { - PyModule::import_bound(module.py(), "sys")? - .dict() - .get_item("modules") - .unwrap() - .unwrap() - .downcast::()? - .set_item(module.name()?, module) -} - -#[test] -#[cfg_attr(all(Py_LIMITED_API, not(Py_3_10)), ignore)] -fn test_pickle() { Python::with_gil(|py| { let module = PyModule::new_bound(py, "test_module").unwrap(); module.add_class::().unwrap(); diff --git a/tests/ui/abi3_dict.rs b/tests/ui/abi3_dict.rs new file mode 100644 index 00000000000..764a4d415a7 --- /dev/null +++ b/tests/ui/abi3_dict.rs @@ -0,0 +1,7 @@ +//! With abi3, dict not supported until python 3.9 or greater +use pyo3::prelude::*; + +#[pyclass(dict)] +struct TestClass {} + +fn main() {} diff --git a/tests/ui/abi3_dict.stderr b/tests/ui/abi3_dict.stderr new file mode 100644 index 00000000000..5887d5aa84a --- /dev/null +++ b/tests/ui/abi3_dict.stderr @@ -0,0 +1,5 @@ +error: `dict` requires Python >= 3.9 when using the `abi3` feature + --> tests/ui/abi3_dict.rs:4:11 + | +4 | #[pyclass(dict)] + | ^^^^ diff --git a/tests/ui/abi3_weakref.rs b/tests/ui/abi3_weakref.rs new file mode 100644 index 00000000000..f45b2258c96 --- /dev/null +++ b/tests/ui/abi3_weakref.rs @@ -0,0 +1,7 @@ +//! With abi3, weakref not supported until python 3.9 or greater +use pyo3::prelude::*; + +#[pyclass(weakref)] +struct TestClass {} + +fn main() {} diff --git a/tests/ui/abi3_weakref.stderr b/tests/ui/abi3_weakref.stderr new file mode 100644 index 00000000000..b8ef3936cdb --- /dev/null +++ b/tests/ui/abi3_weakref.stderr @@ -0,0 +1,5 @@ +error: `weakref` requires Python >= 3.9 when using the `abi3` feature + --> tests/ui/abi3_weakref.rs:4:11 + | +4 | #[pyclass(weakref)] + | ^^^^^^^ From baae9291cc22e333bd0d16acd79863df4046cfa0 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Sun, 16 Jun 2024 20:05:55 -0400 Subject: [PATCH 114/495] Update tests for numpy 2.0 (#4258) --- newsfragments/4258.added.md | 1 + pyo3-macros-backend/src/pyimpl.rs | 1 + pytests/tests/test_misc.py | 6 +++++- src/types/boolobject.rs | 2 +- 4 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 newsfragments/4258.added.md diff --git a/newsfragments/4258.added.md b/newsfragments/4258.added.md new file mode 100644 index 00000000000..0ceea11f91e --- /dev/null +++ b/newsfragments/4258.added.md @@ -0,0 +1 @@ +Added support for `bool` conversion with `numpy` 2.0's `numpy.bool` type diff --git a/pyo3-macros-backend/src/pyimpl.rs b/pyo3-macros-backend/src/pyimpl.rs index 0cb7631a4df..27188254557 100644 --- a/pyo3-macros-backend/src/pyimpl.rs +++ b/pyo3-macros-backend/src/pyimpl.rs @@ -219,6 +219,7 @@ fn impl_py_methods( ) -> TokenStream { let Ctx { pyo3_path } = ctx; quote! { + #[allow(unknown_lints, non_local_definitions)] impl #pyo3_path::impl_::pyclass::PyMethods<#ty> for #pyo3_path::impl_::pyclass::PyClassImplCollector<#ty> { diff --git a/pytests/tests/test_misc.py b/pytests/tests/test_misc.py index 88af735e861..fc8e1095705 100644 --- a/pytests/tests/test_misc.py +++ b/pytests/tests/test_misc.py @@ -54,7 +54,11 @@ def test_import_in_subinterpreter_forbidden(): def test_type_full_name_includes_module(): numpy = pytest.importorskip("numpy") - assert pyo3_pytests.misc.get_type_full_name(numpy.bool_(True)) == "numpy.bool_" + # For numpy 1.x and 2.x + assert pyo3_pytests.misc.get_type_full_name(numpy.bool_(True)) in [ + "numpy.bool", + "numpy.bool_", + ] def test_accepts_numpy_bool(): diff --git a/src/types/boolobject.rs b/src/types/boolobject.rs index 9b5aa659fdf..52465ef305f 100644 --- a/src/types/boolobject.rs +++ b/src/types/boolobject.rs @@ -114,7 +114,7 @@ impl FromPyObject<'_> for bool { if obj .get_type() .name() - .map_or(false, |name| name == "numpy.bool_") + .map_or(false, |name| name == "numpy.bool_" || name == "numpy.bool") { let missing_conversion = |obj: &Bound<'_, PyAny>| { PyTypeError::new_err(format!( From ddff8bea25e1f08716ebd9df3e6a20453b8ba02f Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Sun, 16 Jun 2024 21:28:20 -0400 Subject: [PATCH 115/495] Stabilize declarative modules (#4257) --- Cargo.toml | 4 ---- guide/src/features.md | 6 ------ guide/src/module.md | 9 +-------- newsfragments/4257.changed.md | 1 + pyo3-macros/Cargo.toml | 1 - pyo3-macros/src/lib.rs | 9 +-------- tests/test_append_to_inittab.rs | 3 --- tests/test_compile_error.rs | 4 ---- tests/test_declarative_module.rs | 2 +- tests/ui/pymodule_missing_docs.rs | 1 - 10 files changed, 4 insertions(+), 36 deletions(-) create mode 100644 newsfragments/4257.changed.md diff --git a/Cargo.toml b/Cargo.toml index ffc87bb83af..d46b742b61d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -75,9 +75,6 @@ experimental-async = ["macros", "pyo3-macros/experimental-async"] # and IntoPy traits experimental-inspect = [] -# Enables annotating Rust inline modules with #[pymodule] to build Python modules declaratively -experimental-declarative-modules = ["pyo3-macros/experimental-declarative-modules", "macros"] - # Enables macros: #[pyclass], #[pymodule], #[pyfunction] etc. macros = ["pyo3-macros", "indoc", "unindent"] @@ -125,7 +122,6 @@ full = [ "chrono-tz", "either", "experimental-async", - "experimental-declarative-modules", "experimental-inspect", "eyre", "hashbrown", diff --git a/guide/src/features.md b/guide/src/features.md index 6a25d40cedc..0536b456a33 100644 --- a/guide/src/features.md +++ b/guide/src/features.md @@ -57,12 +57,6 @@ This feature adds support for `async fn` in `#[pyfunction]` and `#[pymethods]`. The feature has some unfinished refinements and performance improvements. To help finish this off, see [issue #1632](https://github.com/PyO3/pyo3/issues/1632) and its associated draft PRs. -### `experimental-declarative-modules` - -This feature allows to declare Python modules using `#[pymodule] mod my_module { ... }` syntax. - -The feature has some unfinished refinements and edge cases. To help finish this off, see [issue #3900](https://github.com/PyO3/pyo3/issues/3900). - ### `experimental-inspect` This feature adds the `pyo3::inspect` module, as well as `IntoPy::type_output` and `FromPyObject::type_input` APIs to produce Python type "annotations" for Rust types. diff --git a/guide/src/module.md b/guide/src/module.md index 51d3ef914f0..8c6049270cb 100644 --- a/guide/src/module.md +++ b/guide/src/module.md @@ -106,14 +106,12 @@ submodules by using `from parent_module import child_module`. For more informati It is not necessary to add `#[pymodule]` on nested modules, which is only required on the top-level module. -## Declarative modules (experimental) +## Declarative modules Another syntax based on Rust inline modules is also available to declare modules. -The `experimental-declarative-modules` feature must be enabled to use it. For example: ```rust -# #[cfg(feature = "experimental-declarative-modules")] # mod declarative_module_test { use pyo3::prelude::*; @@ -157,7 +155,6 @@ For nested modules, the name of the parent module is automatically added. In the following example, the `Unit` class will have for `module` `my_extension.submodule` because it is properly nested but the `Ext` class will have for `module` the default `builtins` because it not nested. ```rust -# #[cfg(feature = "experimental-declarative-modules")] # mod declarative_module_module_attr_test { use pyo3::prelude::*; @@ -184,7 +181,3 @@ mod my_extension { ``` It is possible to customize the `module` value for a `#[pymodule]` with the `#[pyo3(module = "MY_MODULE")]` option. -Some changes are planned to this feature before stabilization, like automatically -filling submodules into `sys.modules` to allow easier imports (see [issue #759](https://github.com/PyO3/pyo3/issues/759)). -Macro names might also change. -See [issue #3900](https://github.com/PyO3/pyo3/issues/3900) to track this feature progress. diff --git a/newsfragments/4257.changed.md b/newsfragments/4257.changed.md new file mode 100644 index 00000000000..dee4a7ae13d --- /dev/null +++ b/newsfragments/4257.changed.md @@ -0,0 +1 @@ +The `experimental-declarative-modules` feature is now stabilized and available by default diff --git a/pyo3-macros/Cargo.toml b/pyo3-macros/Cargo.toml index e4b550cfb8e..789b8095b3f 100644 --- a/pyo3-macros/Cargo.toml +++ b/pyo3-macros/Cargo.toml @@ -16,7 +16,6 @@ proc-macro = true [features] multiple-pymethods = [] experimental-async = ["pyo3-macros-backend/experimental-async"] -experimental-declarative-modules = [] gil-refs = ["pyo3-macros-backend/gil-refs"] [dependencies] diff --git a/pyo3-macros/src/lib.rs b/pyo3-macros/src/lib.rs index 64756a1c73b..8dbf2782d5b 100644 --- a/pyo3-macros/src/lib.rs +++ b/pyo3-macros/src/lib.rs @@ -37,14 +37,7 @@ use syn::{parse::Nothing, parse_macro_input, Item}; pub fn pymodule(args: TokenStream, input: TokenStream) -> TokenStream { parse_macro_input!(args as Nothing); match parse_macro_input!(input as Item) { - Item::Mod(module) => if cfg!(feature = "experimental-declarative-modules") { - pymodule_module_impl(module) - } else { - Err(syn::Error::new_spanned( - module, - "#[pymodule] requires the 'experimental-declarative-modules' feature to be used on Rust modules.", - )) - }, + Item::Mod(module) => pymodule_module_impl(module), Item::Fn(function) => pymodule_function_impl(function), unsupported => Err(syn::Error::new_spanned( unsupported, diff --git a/tests/test_append_to_inittab.rs b/tests/test_append_to_inittab.rs index da35298b4d9..94deb16a128 100644 --- a/tests/test_append_to_inittab.rs +++ b/tests/test_append_to_inittab.rs @@ -13,7 +13,6 @@ fn module_fn_with_functions(m: &Bound<'_, PyModule>) -> PyResult<()> { Ok(()) } -#[cfg(feature = "experimental-declarative-modules")] #[pymodule] mod module_mod_with_functions { #[pymodule_export] @@ -27,7 +26,6 @@ fn test_module_append_to_inittab() { append_to_inittab!(module_fn_with_functions); - #[cfg(feature = "experimental-declarative-modules")] append_to_inittab!(module_mod_with_functions); Python::with_gil(|py| { @@ -43,7 +41,6 @@ assert module_fn_with_functions.foo() == 123 .unwrap(); }); - #[cfg(feature = "experimental-declarative-modules")] Python::with_gil(|py| { py.run_bound( r#" diff --git a/tests/test_compile_error.rs b/tests/test_compile_error.rs index c978e413d84..9e8b3b1a593 100644 --- a/tests/test_compile_error.rs +++ b/tests/test_compile_error.rs @@ -52,13 +52,9 @@ fn test_compile_errors() { t.compile_fail("tests/ui/not_send2.rs"); t.compile_fail("tests/ui/get_set_all.rs"); t.compile_fail("tests/ui/traverse.rs"); - #[cfg(feature = "experimental-declarative-modules")] t.compile_fail("tests/ui/invalid_pymodule_in_root.rs"); - #[cfg(feature = "experimental-declarative-modules")] t.compile_fail("tests/ui/invalid_pymodule_glob.rs"); - #[cfg(feature = "experimental-declarative-modules")] t.compile_fail("tests/ui/invalid_pymodule_trait.rs"); - #[cfg(feature = "experimental-declarative-modules")] t.compile_fail("tests/ui/invalid_pymodule_two_pymodule_init.rs"); #[cfg(feature = "experimental-async")] #[cfg(any(not(Py_LIMITED_API), Py_3_10))] // to avoid PyFunctionArgument for &str diff --git a/tests/test_declarative_module.rs b/tests/test_declarative_module.rs index 0858f84e04a..061d0337285 100644 --- a/tests/test_declarative_module.rs +++ b/tests/test_declarative_module.rs @@ -1,4 +1,4 @@ -#![cfg(feature = "experimental-declarative-modules")] +#![cfg(feature = "macros")] use pyo3::create_exception; use pyo3::exceptions::PyException; diff --git a/tests/ui/pymodule_missing_docs.rs b/tests/ui/pymodule_missing_docs.rs index ed7d772fafd..d8bf2eb4d2f 100644 --- a/tests/ui/pymodule_missing_docs.rs +++ b/tests/ui/pymodule_missing_docs.rs @@ -9,7 +9,6 @@ pub fn python_module(_m: &Bound<'_, PyModule>) -> PyResult<()> { Ok(()) } -#[cfg(feature = "experimental-declarative-modules")] /// Some module documentation #[pymodule] pub mod declarative_python_module {} From 0e142f05dd06344be14059246f5f2e43f734e931 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Tue, 18 Jun 2024 19:09:36 +0100 Subject: [PATCH 116/495] add `c_str!` macro to create `&'static CStr` (#4255) * add `c_str!` macro to create `&'static CStr` * newsfragment, just export as `pyo3::ffi::c_str` * fix doc link * fix doc * further `c_str!` based cleanups * [review]: mejrs Co-authored-by: Bruno Kolenbrander <59372212+mejrs@users.noreply.github.com> * rustfmt * build fixes * clippy * allow lint on MSRV * fix GraalPy import --------- Co-authored-by: Bruno Kolenbrander <59372212+mejrs@users.noreply.github.com> --- examples/sequential/src/id.rs | 21 ++++--- examples/sequential/src/module.rs | 12 ++-- examples/sequential/tests/test.rs | 20 +++---- examples/string-sum/src/lib.rs | 18 ++---- guide/src/class.md | 10 ++-- newsfragments/4255.added.md | 1 + newsfragments/4255.changed.md | 1 + pyo3-ffi/README.md | 26 +++------ pyo3-ffi/src/abstract_.rs | 2 +- pyo3-ffi/src/datetime.rs | 40 ++++++------- pyo3-ffi/src/lib.rs | 73 +++++++++++++++++------ pyo3-ffi/src/pyerrors.rs | 2 +- pyo3-macros-backend/src/konst.rs | 7 ++- pyo3-macros-backend/src/method.rs | 14 +++-- pyo3-macros-backend/src/module.rs | 7 ++- pyo3-macros-backend/src/pyclass.rs | 18 +++--- pyo3-macros-backend/src/pyfunction.rs | 2 +- pyo3-macros-backend/src/pyimpl.rs | 2 +- pyo3-macros-backend/src/pymethod.rs | 32 +++++------ pyo3-macros-backend/src/utils.rs | 20 ++++--- src/buffer.rs | 83 +++++++++++++-------------- src/conversions/num_rational.rs | 21 ++----- src/err/err_state.rs | 4 +- src/exceptions.rs | 4 +- src/impl_/pyclass.rs | 7 +-- src/impl_/pyclass/lazy_type_object.rs | 9 +-- src/impl_/pymethods.rs | 82 +++++++++----------------- src/impl_/pymodule.rs | 23 ++++---- src/internal_tricks.rs | 36 +----------- src/macros.rs | 2 +- src/marker.rs | 5 +- src/pyclass/create_type_object.rs | 75 ++++++++++-------------- src/types/function.rs | 61 ++++++++++---------- src/types/string.rs | 10 ++-- tests/test_buffer.rs | 5 +- tests/test_pyfunction.rs | 18 ++++-- 36 files changed, 357 insertions(+), 416 deletions(-) create mode 100644 newsfragments/4255.added.md create mode 100644 newsfragments/4255.changed.md diff --git a/examples/sequential/src/id.rs b/examples/sequential/src/id.rs index d80e84b4eab..fa72bb091c7 100644 --- a/examples/sequential/src/id.rs +++ b/examples/sequential/src/id.rs @@ -1,5 +1,6 @@ use core::sync::atomic::{AtomicU64, Ordering}; use core::{mem, ptr}; +use std::ffi::CString; use std::os::raw::{c_char, c_int, c_uint, c_ulonglong, c_void}; use pyo3_ffi::*; @@ -27,10 +28,10 @@ unsafe extern "C" fn id_new( 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::(), - ); + // We use pyo3-ffi's `c_str!` macro to create null-terminated literals because + // Rust's string literals are not null-terminated + // On Rust 1.77 or newer you can use `c"text"` instead. + PyErr_SetString(PyExc_TypeError, c_str!("Id() takes no arguments").as_ptr()); return ptr::null_mut(); } @@ -81,8 +82,12 @@ unsafe extern "C" fn id_richcompare( 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::()); + let msg = CString::new(&*format!( + "unrecognized richcompare opcode {}", + unrecognized + )) + .unwrap(); + PyErr_SetString(PyExc_SystemError, msg.as_ptr()); return ptr::null_mut(); } }; @@ -101,7 +106,7 @@ static mut SLOTS: &[PyType_Slot] = &[ }, PyType_Slot { slot: Py_tp_doc, - pfunc: "An id that is increased every time an instance is created\0".as_ptr() + pfunc: c_str!("An id that is increased every time an instance is created").as_ptr() as *mut c_void, }, PyType_Slot { @@ -123,7 +128,7 @@ static mut SLOTS: &[PyType_Slot] = &[ ]; pub static mut ID_SPEC: PyType_Spec = PyType_Spec { - name: "sequential.Id\0".as_ptr().cast::(), + name: c_str!("sequential.Id").as_ptr(), basicsize: mem::size_of::() as c_int, itemsize: 0, flags: (Py_TPFLAGS_DEFAULT | Py_TPFLAGS_IMMUTABLETYPE) as c_uint, diff --git a/examples/sequential/src/module.rs b/examples/sequential/src/module.rs index 5552baf3368..5e71f07a865 100644 --- a/examples/sequential/src/module.rs +++ b/examples/sequential/src/module.rs @@ -1,13 +1,11 @@ use core::{mem, ptr}; use pyo3_ffi::*; -use std::os::raw::{c_char, c_int, c_void}; +use std::os::raw::{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_name: c_str!("sequential").as_ptr(), + m_doc: c_str!("A library for generating sequential ids, written in Rust.").as_ptr(), 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 }, @@ -42,13 +40,13 @@ unsafe extern "C" fn sequential_exec(module: *mut PyObject) -> c_int { if id_type.is_null() { PyErr_SetString( PyExc_SystemError, - "cannot locate type object\0".as_ptr().cast::(), + c_str!("cannot locate type object").as_ptr(), ); return -1; } (*state).id_type = id_type.cast::(); - PyModule_AddObjectRef(module, "Id\0".as_ptr().cast::(), id_type) + PyModule_AddObjectRef(module, c_str!("Id").as_ptr(), id_type) } unsafe extern "C" fn sequential_traverse( diff --git a/examples/sequential/tests/test.rs b/examples/sequential/tests/test.rs index 6076edd4974..f2a08433cea 100644 --- a/examples/sequential/tests/test.rs +++ b/examples/sequential/tests/test.rs @@ -5,11 +5,13 @@ use std::thread; use pyo3_ffi::*; use sequential::PyInit_sequential; -static COMMAND: &'static str = " +static COMMAND: &'static str = c_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); @@ -19,10 +21,7 @@ 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), - ); + let ret = PyImport_AppendInittab(c_str!("sequential").as_ptr(), Some(PyInit_sequential)); if ret == -1 { return Err("could not add module to inittab".into()); } @@ -122,11 +121,8 @@ unsafe fn fetch() -> String { fn run_code() -> Result { unsafe { - let code_obj = Py_CompileString( - COMMAND.as_ptr().cast::(), - "program\0".as_ptr().cast::(), - Py_file_input, - ); + let code_obj = + Py_CompileString(COMMAND.as_ptr(), c_str!("program").as_ptr(), Py_file_input); if code_obj.is_null() { return Err(fetch()); } @@ -138,7 +134,7 @@ fn run_code() -> Result { } else { Py_DECREF(res_ptr); } - let sum = PyDict_GetItemString(globals, "s\0".as_ptr().cast::()); /* borrowed reference */ + let sum = PyDict_GetItemString(globals, c_str!("s").as_ptr()); /* borrowed reference */ if sum.is_null() { Py_DECREF(globals); return Err("globals did not have `s`".into()); diff --git a/examples/string-sum/src/lib.rs b/examples/string-sum/src/lib.rs index 91072418038..ce71ab38f87 100644 --- a/examples/string-sum/src/lib.rs +++ b/examples/string-sum/src/lib.rs @@ -5,10 +5,8 @@ use pyo3_ffi::*; static mut MODULE_DEF: PyModuleDef = PyModuleDef { m_base: PyModuleDef_HEAD_INIT, - m_name: "string_sum\0".as_ptr().cast::(), - m_doc: "A Python module written in Rust.\0" - .as_ptr() - .cast::(), + m_name: c_str!("string_sum").as_ptr(), + m_doc: c_str!("A Python module written in Rust.").as_ptr(), m_size: 0, m_methods: unsafe { METHODS as *const [PyMethodDef] as *mut PyMethodDef }, m_slots: std::ptr::null_mut(), @@ -19,14 +17,12 @@ static mut MODULE_DEF: PyModuleDef = PyModuleDef { static mut METHODS: &[PyMethodDef] = &[ PyMethodDef { - ml_name: "sum_as_string\0".as_ptr().cast::(), + ml_name: c_str!("sum_as_string").as_ptr(), ml_meth: PyMethodDefPointer { _PyCFunctionFast: sum_as_string, }, ml_flags: METH_FASTCALL, - ml_doc: "returns the sum of two integers as a string\0" - .as_ptr() - .cast::(), + ml_doc: c_str!("returns the sum of two integers as a string").as_ptr(), }, // A zeroed PyMethodDef to mark the end of the array. PyMethodDef::zeroed(), @@ -93,9 +89,7 @@ pub unsafe extern "C" fn sum_as_string( if nargs != 2 { PyErr_SetString( PyExc_TypeError, - "sum_as_string expected 2 positional arguments\0" - .as_ptr() - .cast::(), + c_str!("sum_as_string expected 2 positional arguments").as_ptr(), ); return std::ptr::null_mut(); } @@ -119,7 +113,7 @@ pub unsafe extern "C" fn sum_as_string( None => { PyErr_SetString( PyExc_OverflowError, - "arguments too large to add\0".as_ptr().cast::(), + c_str!("arguments too large to add").as_ptr(), ); std::ptr::null_mut() } diff --git a/guide/src/class.md b/guide/src/class.md index ab0c82fc88b..94f3f333581 100644 --- a/guide/src/class.md +++ b/guide/src/class.md @@ -330,8 +330,8 @@ or [`PyRefMut`] instead of `&mut self`. Then you can access a parent class by `self_.as_super()` as `&PyRef`, or by `self_.into_super()` as `PyRef` (and similar for the `PyRefMut` case). For convenience, `self_.as_ref()` can also be used to get `&Self::BaseClass` -directly; however, this approach does not let you access base clases higher in the -inheritance hierarchy, for which you would need to chain multiple `as_super` or +directly; however, this approach does not let you access base clases higher in the +inheritance hierarchy, for which you would need to chain multiple `as_super` or `into_super` calls. ```rust @@ -400,7 +400,7 @@ impl SubSubClass { let val2 = self_.as_super().val2; (val1, val2, self_.val3) } - + fn double_values(mut self_: PyRefMut<'_, Self>) { self_.as_super().as_super().val1 *= 2; self_.as_super().val2 *= 2; @@ -1187,7 +1187,7 @@ Python::with_gil(|py| { }) ``` -Ordering of enum variants is optionally added using `#[pyo3(ord)]`. +Ordering of enum variants is optionally added using `#[pyo3(ord)]`. *Note: Implementation of the `PartialOrd` trait is required when passing the `ord` argument. If not implemented, a compile time error is raised.* ```rust @@ -1443,7 +1443,7 @@ impl pyo3::impl_::pyclass::PyClassImpl for MyClass { static DOC: pyo3::sync::GILOnceCell<::std::borrow::Cow<'static, ::std::ffi::CStr>> = pyo3::sync::GILOnceCell::new(); DOC.get_or_try_init(py, || { let collector = PyClassImplCollector::::new(); - build_pyclass_doc(::NAME, "\0", collector.new_text_signature()) + build_pyclass_doc(::NAME, pyo3::ffi::c_str!(""), collector.new_text_signature()) }).map(::std::ops::Deref::deref) } } diff --git a/newsfragments/4255.added.md b/newsfragments/4255.added.md new file mode 100644 index 00000000000..c70c5da279d --- /dev/null +++ b/newsfragments/4255.added.md @@ -0,0 +1 @@ +Add `pyo3_ffi::c_str` macro to create `&'static CStr` on Rust versions which don't have 1.77's `c""` literals. diff --git a/newsfragments/4255.changed.md b/newsfragments/4255.changed.md new file mode 100644 index 00000000000..185bd5cc39d --- /dev/null +++ b/newsfragments/4255.changed.md @@ -0,0 +1 @@ +`PyCFunction::new`, `PyCFunction::new_with_keywords` and `PyCFunction::new_closure` now take `&'static CStr` name and doc arguments (previously was `&'static str`). diff --git a/pyo3-ffi/README.md b/pyo3-ffi/README.md index 283a7072357..200c78cec14 100644 --- a/pyo3-ffi/README.md +++ b/pyo3-ffi/README.md @@ -51,10 +51,8 @@ use pyo3_ffi::*; static mut MODULE_DEF: PyModuleDef = PyModuleDef { m_base: PyModuleDef_HEAD_INIT, - m_name: "string_sum\0".as_ptr().cast::(), - m_doc: "A Python module written in Rust.\0" - .as_ptr() - .cast::(), + m_name: c_str!("string_sum").as_ptr(), + m_doc: c_str!("A Python module written in Rust.").as_ptr(), m_size: 0, m_methods: unsafe { METHODS.as_mut_ptr().cast() }, m_slots: std::ptr::null_mut(), @@ -65,14 +63,12 @@ static mut MODULE_DEF: PyModuleDef = PyModuleDef { static mut METHODS: [PyMethodDef; 2] = [ PyMethodDef { - ml_name: "sum_as_string\0".as_ptr().cast::(), + ml_name: c_str!("sum_as_string").as_ptr(), ml_meth: PyMethodDefPointer { _PyCFunctionFast: sum_as_string, }, ml_flags: METH_FASTCALL, - ml_doc: "returns the sum of two integers as a string\0" - .as_ptr() - .cast::(), + ml_doc: c_str!("returns the sum of two integers as a string").as_ptr(), }, // A zeroed PyMethodDef to mark the end of the array. PyMethodDef::zeroed() @@ -93,9 +89,7 @@ pub unsafe extern "C" fn sum_as_string( if nargs != 2 { PyErr_SetString( PyExc_TypeError, - "sum_as_string() expected 2 positional arguments\0" - .as_ptr() - .cast::(), + c_str!("sum_as_string() expected 2 positional arguments").as_ptr(), ); return std::ptr::null_mut(); } @@ -104,9 +98,7 @@ pub unsafe extern "C" fn sum_as_string( if PyLong_Check(arg1) == 0 { PyErr_SetString( PyExc_TypeError, - "sum_as_string() expected an int for positional argument 1\0" - .as_ptr() - .cast::(), + c_str!("sum_as_string() expected an int for positional argument 1").as_ptr(), ); return std::ptr::null_mut(); } @@ -120,9 +112,7 @@ pub unsafe extern "C" fn sum_as_string( if PyLong_Check(arg2) == 0 { PyErr_SetString( PyExc_TypeError, - "sum_as_string() expected an int for positional argument 2\0" - .as_ptr() - .cast::(), + c_str!("sum_as_string() expected an int for positional argument 2").as_ptr(), ); return std::ptr::null_mut(); } @@ -140,7 +130,7 @@ pub unsafe extern "C" fn sum_as_string( None => { PyErr_SetString( PyExc_OverflowError, - "arguments too large to add\0".as_ptr().cast::(), + c_str!("arguments too large to add").as_ptr(), ); std::ptr::null_mut() } diff --git a/pyo3-ffi/src/abstract_.rs b/pyo3-ffi/src/abstract_.rs index 1e0020462fb..175f9af734f 100644 --- a/pyo3-ffi/src/abstract_.rs +++ b/pyo3-ffi/src/abstract_.rs @@ -114,7 +114,7 @@ extern "C" { #[cfg(not(any(Py_3_8, PyPy)))] #[inline] pub unsafe fn PyIter_Check(o: *mut PyObject) -> c_int { - crate::PyObject_HasAttrString(crate::Py_TYPE(o).cast(), "__next__\0".as_ptr().cast()) + crate::PyObject_HasAttrString(crate::Py_TYPE(o).cast(), c_str!("__next__").as_ptr()) } extern "C" { diff --git a/pyo3-ffi/src/datetime.rs b/pyo3-ffi/src/datetime.rs index b985085fba3..5da2956c5e9 100644 --- a/pyo3-ffi/src/datetime.rs +++ b/pyo3-ffi/src/datetime.rs @@ -357,8 +357,8 @@ pub unsafe fn PyDateTime_DELTA_GET_MICROSECONDS(o: *mut PyObject) -> c_int { // but copying them seems suboptimal #[inline] #[cfg(GraalPy)] -pub unsafe fn _get_attr(obj: *mut PyObject, field: &str) -> c_int { - let result = PyObject_GetAttrString(obj, field.as_ptr().cast()); +pub unsafe fn _get_attr(obj: *mut PyObject, field: &std::ffi::CStr) -> c_int { + let result = PyObject_GetAttrString(obj, field.as_ptr()); Py_DecRef(result); // the original macros are borrowing if PyLong_Check(result) == 1 { PyLong_AsLong(result) as c_int @@ -370,55 +370,55 @@ pub unsafe fn _get_attr(obj: *mut PyObject, field: &str) -> c_int { #[inline] #[cfg(GraalPy)] pub unsafe fn PyDateTime_GET_YEAR(o: *mut PyObject) -> c_int { - _get_attr(o, "year\0") + _get_attr(o, c_str!("year")) } #[inline] #[cfg(GraalPy)] pub unsafe fn PyDateTime_GET_MONTH(o: *mut PyObject) -> c_int { - _get_attr(o, "month\0") + _get_attr(o, c_str!("month")) } #[inline] #[cfg(GraalPy)] pub unsafe fn PyDateTime_GET_DAY(o: *mut PyObject) -> c_int { - _get_attr(o, "day\0") + _get_attr(o, c_str!("day")) } #[inline] #[cfg(GraalPy)] pub unsafe fn PyDateTime_DATE_GET_HOUR(o: *mut PyObject) -> c_int { - _get_attr(o, "hour\0") + _get_attr(o, c_str!("hour")) } #[inline] #[cfg(GraalPy)] pub unsafe fn PyDateTime_DATE_GET_MINUTE(o: *mut PyObject) -> c_int { - _get_attr(o, "minute\0") + _get_attr(o, c_str!("minute")) } #[inline] #[cfg(GraalPy)] pub unsafe fn PyDateTime_DATE_GET_SECOND(o: *mut PyObject) -> c_int { - _get_attr(o, "second\0") + _get_attr(o, c_str!("second")) } #[inline] #[cfg(GraalPy)] pub unsafe fn PyDateTime_DATE_GET_MICROSECOND(o: *mut PyObject) -> c_int { - _get_attr(o, "microsecond\0") + _get_attr(o, c_str!("microsecond")) } #[inline] #[cfg(GraalPy)] pub unsafe fn PyDateTime_DATE_GET_FOLD(o: *mut PyObject) -> c_int { - _get_attr(o, "fold\0") + _get_attr(o, c_str!("fold")) } #[inline] #[cfg(GraalPy)] pub unsafe fn PyDateTime_DATE_GET_TZINFO(o: *mut PyObject) -> *mut PyObject { - let res = PyObject_GetAttrString(o, "tzinfo\0".as_ptr().cast()); + let res = PyObject_GetAttrString(o, c_str!("tzinfo").as_ptr().cast()); Py_DecRef(res); // the original macros are borrowing res } @@ -426,37 +426,37 @@ pub unsafe fn PyDateTime_DATE_GET_TZINFO(o: *mut PyObject) -> *mut PyObject { #[inline] #[cfg(GraalPy)] pub unsafe fn PyDateTime_TIME_GET_HOUR(o: *mut PyObject) -> c_int { - _get_attr(o, "hour\0") + _get_attr(o, c_str!("hour")) } #[inline] #[cfg(GraalPy)] pub unsafe fn PyDateTime_TIME_GET_MINUTE(o: *mut PyObject) -> c_int { - _get_attr(o, "minute\0") + _get_attr(o, c_str!("minute")) } #[inline] #[cfg(GraalPy)] pub unsafe fn PyDateTime_TIME_GET_SECOND(o: *mut PyObject) -> c_int { - _get_attr(o, "second\0") + _get_attr(o, c_str!("second")) } #[inline] #[cfg(GraalPy)] pub unsafe fn PyDateTime_TIME_GET_MICROSECOND(o: *mut PyObject) -> c_int { - _get_attr(o, "microsecond\0") + _get_attr(o, c_str!("microsecond")) } #[inline] #[cfg(GraalPy)] pub unsafe fn PyDateTime_TIME_GET_FOLD(o: *mut PyObject) -> c_int { - _get_attr(o, "fold\0") + _get_attr(o, c_str!("fold")) } #[inline] #[cfg(GraalPy)] pub unsafe fn PyDateTime_TIME_GET_TZINFO(o: *mut PyObject) -> *mut PyObject { - let res = PyObject_GetAttrString(o, "tzinfo\0".as_ptr().cast()); + let res = PyObject_GetAttrString(o, c_str!("tzinfo").as_ptr().cast()); Py_DecRef(res); // the original macros are borrowing res } @@ -464,19 +464,19 @@ pub unsafe fn PyDateTime_TIME_GET_TZINFO(o: *mut PyObject) -> *mut PyObject { #[inline] #[cfg(GraalPy)] pub unsafe fn PyDateTime_DELTA_GET_DAYS(o: *mut PyObject) -> c_int { - _get_attr(o, "days\0") + _get_attr(o, c_str!("days")) } #[inline] #[cfg(GraalPy)] pub unsafe fn PyDateTime_DELTA_GET_SECONDS(o: *mut PyObject) -> c_int { - _get_attr(o, "seconds\0") + _get_attr(o, c_str!("seconds")) } #[inline] #[cfg(GraalPy)] pub unsafe fn PyDateTime_DELTA_GET_MICROSECONDS(o: *mut PyObject) -> c_int { - _get_attr(o, "microseconds\0") + _get_attr(o, c_str!("microseconds")) } #[cfg(PyPy)] diff --git a/pyo3-ffi/src/lib.rs b/pyo3-ffi/src/lib.rs index 877d42dce33..c3f5225e87d 100644 --- a/pyo3-ffi/src/lib.rs +++ b/pyo3-ffi/src/lib.rs @@ -88,10 +88,8 @@ //! //! static mut MODULE_DEF: PyModuleDef = PyModuleDef { //! m_base: PyModuleDef_HEAD_INIT, -//! m_name: "string_sum\0".as_ptr().cast::(), -//! m_doc: "A Python module written in Rust.\0" -//! .as_ptr() -//! .cast::(), +//! m_name: c_str!("string_sum").as_ptr(), +//! m_doc: c_str!("A Python module written in Rust.").as_ptr(), //! m_size: 0, //! m_methods: unsafe { METHODS.as_mut_ptr().cast() }, //! m_slots: std::ptr::null_mut(), @@ -102,14 +100,12 @@ //! //! static mut METHODS: [PyMethodDef; 2] = [ //! PyMethodDef { -//! ml_name: "sum_as_string\0".as_ptr().cast::(), +//! ml_name: c_str!("sum_as_string").as_ptr(), //! ml_meth: PyMethodDefPointer { //! _PyCFunctionFast: sum_as_string, //! }, //! ml_flags: METH_FASTCALL, -//! ml_doc: "returns the sum of two integers as a string\0" -//! .as_ptr() -//! .cast::(), +//! ml_doc: c_str!("returns the sum of two integers as a string").as_ptr(), //! }, //! // A zeroed PyMethodDef to mark the end of the array. //! PyMethodDef::zeroed() @@ -130,9 +126,7 @@ //! if nargs != 2 { //! PyErr_SetString( //! PyExc_TypeError, -//! "sum_as_string() expected 2 positional arguments\0" -//! .as_ptr() -//! .cast::(), +//! c_str!("sum_as_string() expected 2 positional arguments").as_ptr(), //! ); //! return std::ptr::null_mut(); //! } @@ -141,9 +135,7 @@ //! if PyLong_Check(arg1) == 0 { //! PyErr_SetString( //! PyExc_TypeError, -//! "sum_as_string() expected an int for positional argument 1\0" -//! .as_ptr() -//! .cast::(), +//! c_str!("sum_as_string() expected an int for positional argument 1").as_ptr(), //! ); //! return std::ptr::null_mut(); //! } @@ -157,9 +149,7 @@ //! if PyLong_Check(arg2) == 0 { //! PyErr_SetString( //! PyExc_TypeError, -//! "sum_as_string() expected an int for positional argument 2\0" -//! .as_ptr() -//! .cast::(), +//! c_str!("sum_as_string() expected an int for positional argument 2").as_ptr(), //! ); //! return std::ptr::null_mut(); //! } @@ -177,7 +167,7 @@ //! None => { //! PyErr_SetString( //! PyExc_OverflowError, -//! "arguments too large to add\0".as_ptr().cast::(), +//! c_str!("arguments too large to add").as_ptr(), //! ); //! std::ptr::null_mut() //! } @@ -256,6 +246,53 @@ macro_rules! opaque_struct { }; } +/// This is a helper macro to create a `&'static CStr`. +/// +/// It can be used on all Rust versions supported by PyO3, unlike c"" literals which +/// were stabilised in Rust 1.77. +/// +/// Due to the nature of PyO3 making heavy use of C FFI interop with Python, it is +/// common for PyO3 to use CStr. +/// +/// Examples: +/// +/// ```rust +/// use std::ffi::CStr; +/// +/// const HELLO: &CStr = pyo3_ffi::c_str!("hello"); +/// static WORLD: &CStr = pyo3_ffi::c_str!("world"); +/// ``` +#[macro_export] +macro_rules! c_str { + ($s:expr) => {{ + const _: () = { + assert!( + $crate::str_contains_no_nul($s), + "string contains null bytes" + ); + }; + // SAFETY: the string is checked to not contain null bytes + #[allow(unsafe_op_in_unsafe_fn, unused_unsafe)] // MSRV 1.63 needs these allows + unsafe { + ::std::ffi::CStr::from_bytes_with_nul_unchecked(concat!($s, "\0").as_bytes()) + } + }}; +} + +#[doc(hidden)] +pub const fn str_contains_no_nul(s: &str) -> bool { + let bytes = s.as_bytes(); + let len = s.len(); + let mut i = 0; + while i < len { + if bytes[i] == 0 { + return false; + } + i += 1; + } + true +} + pub use self::abstract_::*; pub use self::bltinmodule::*; pub use self::boolobject::*; diff --git a/pyo3-ffi/src/pyerrors.rs b/pyo3-ffi/src/pyerrors.rs index 9da00ea390e..6c9313c4ab0 100644 --- a/pyo3-ffi/src/pyerrors.rs +++ b/pyo3-ffi/src/pyerrors.rs @@ -101,7 +101,7 @@ pub unsafe fn PyUnicodeDecodeError_Create( ) -> *mut PyObject { crate::_PyObject_CallFunction_SizeT( PyExc_UnicodeDecodeError, - b"sy#nns\0".as_ptr().cast::(), + c_str!("sy#nns").as_ptr(), encoding, object, length, diff --git a/pyo3-macros-backend/src/konst.rs b/pyo3-macros-backend/src/konst.rs index 9a41a2b7178..e7d8d554cae 100644 --- a/pyo3-macros-backend/src/konst.rs +++ b/pyo3-macros-backend/src/konst.rs @@ -29,9 +29,10 @@ impl ConstSpec<'_> { } /// Null-terminated Python name - pub fn null_terminated_python_name(&self) -> TokenStream { - let name = format!("{}\0", self.python_name()); - quote!({#name}) + pub fn null_terminated_python_name(&self, ctx: &Ctx) -> TokenStream { + let Ctx { pyo3_path } = ctx; + let name = self.python_name().to_string(); + quote!(#pyo3_path::ffi::c_str!(#name)) } } diff --git a/pyo3-macros-backend/src/method.rs b/pyo3-macros-backend/src/method.rs index c0e38bf8416..745426371c3 100644 --- a/pyo3-macros-backend/src/method.rs +++ b/pyo3-macros-backend/src/method.rs @@ -472,8 +472,12 @@ impl<'a> FnSpec<'a> { }) } - pub fn null_terminated_python_name(&self) -> syn::LitStr { - syn::LitStr::new(&format!("{}\0", self.python_name), self.python_name.span()) + pub fn null_terminated_python_name(&self, ctx: &Ctx) -> TokenStream { + let Ctx { pyo3_path } = ctx; + let span = self.python_name.span(); + let pyo3_path = pyo3_path.to_tokens_spanned(span); + let name = self.python_name.to_string(); + quote_spanned!(self.python_name.span() => #pyo3_path::ffi::c_str!(#name)) } fn parse_fn_type( @@ -830,7 +834,7 @@ impl<'a> FnSpec<'a> { /// calling convention. pub fn get_methoddef(&self, wrapper: impl ToTokens, doc: &PythonDoc, ctx: &Ctx) -> TokenStream { let Ctx { pyo3_path } = ctx; - let python_name = self.null_terminated_python_name(); + let python_name = self.null_terminated_python_name(ctx); match self.convention { CallingConvention::Noargs => quote! { #pyo3_path::impl_::pymethods::PyMethodDef::noargs( @@ -903,11 +907,11 @@ impl<'a> FnSpec<'a> { } /// Forwards to [utils::get_doc] with the text signature of this spec. - pub fn get_doc(&self, attrs: &[syn::Attribute]) -> PythonDoc { + pub fn get_doc(&self, attrs: &[syn::Attribute], ctx: &Ctx) -> PythonDoc { let text_signature = self .text_signature_call_signature() .map(|sig| format!("{}{}", self.python_name, sig)); - utils::get_doc(attrs, text_signature) + utils::get_doc(attrs, text_signature, ctx) } /// Creates the parenthesised arguments list for `__text_signature__` snippet based on this spec's signature diff --git a/pyo3-macros-backend/src/module.rs b/pyo3-macros-backend/src/module.rs index c1e46276544..0383046e0c4 100644 --- a/pyo3-macros-backend/src/module.rs +++ b/pyo3-macros-backend/src/module.rs @@ -92,7 +92,7 @@ pub fn pymodule_module_impl(mut module: syn::ItemMod) -> Result { let options = PyModuleOptions::from_attrs(attrs)?; let ctx = &Ctx::new(&options.krate); let Ctx { pyo3_path } = ctx; - let doc = get_doc(attrs, None); + let doc = get_doc(attrs, None, ctx); let name = options.name.unwrap_or_else(|| ident.unraw()); let full_name = if let Some(module) = &options.module { format!("{}.{}", module.value.value(), name) @@ -332,7 +332,7 @@ pub fn pymodule_function_impl(mut function: syn::ItemFn) -> Result let ident = &function.sig.ident; let name = options.name.unwrap_or_else(|| ident.unraw()); let vis = &function.vis; - let doc = get_doc(&function.attrs, None); + let doc = get_doc(&function.attrs, None, ctx); let initialization = module_initialization(&name, ctx); @@ -402,10 +402,11 @@ pub fn pymodule_function_impl(mut function: syn::ItemFn) -> Result fn module_initialization(name: &syn::Ident, ctx: &Ctx) -> TokenStream { let Ctx { pyo3_path } = ctx; let pyinit_symbol = format!("PyInit_{}", name); + let name = name.to_string(); quote! { #[doc(hidden)] - pub const __PYO3_NAME: &'static str = concat!(stringify!(#name), "\0"); + pub const __PYO3_NAME: &'static ::std::ffi::CStr = #pyo3_path::ffi::c_str!(#name); pub(super) struct MakeDef; #[doc(hidden)] diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index 9484b3dbf26..3e40977e4af 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -1,7 +1,7 @@ use std::borrow::Cow; use proc_macro2::{Ident, Span, TokenStream}; -use quote::{format_ident, quote, quote_spanned}; +use quote::{format_ident, quote, quote_spanned, ToTokens}; use syn::ext::IdentExt; use syn::parse::{Parse, ParseStream}; use syn::punctuated::Punctuated; @@ -21,9 +21,10 @@ use crate::pymethod::{ impl_py_getter_def, impl_py_setter_def, MethodAndMethodDef, MethodAndSlotDef, PropertyType, SlotDef, __GETITEM__, __HASH__, __INT__, __LEN__, __REPR__, __RICHCMP__, }; +use crate::pyversions; use crate::utils::{self, apply_renaming_rule, PythonDoc}; use crate::utils::{is_abi3, Ctx}; -use crate::{pyversions, PyFunctionOptions}; +use crate::PyFunctionOptions; /// If the class is derived from a Rust `struct` or `enum`. #[derive(Copy, Clone, Debug, PartialEq, Eq)] @@ -225,9 +226,9 @@ pub fn build_py_class( methods_type: PyClassMethodsType, ) -> syn::Result { args.options.take_pyo3_options(&mut class.attrs)?; - let doc = utils::get_doc(&class.attrs, None); let ctx = &Ctx::new(&args.options.krate); + let doc = utils::get_doc(&class.attrs, None, ctx); if let Some(lt) = class.generics.lifetimes().next() { bail_spanned!( @@ -465,7 +466,7 @@ pub fn build_py_enum( bail_spanned!(enum_.brace_token.span.join() => "#[pyclass] can't be used on enums without any variants"); } - let doc = utils::get_doc(&enum_.attrs, None); + let doc = utils::get_doc(&enum_.attrs, None, ctx); let enum_ = PyClassEnum::new(enum_)?; impl_enum(enum_, &args, doc, method_type, ctx) } @@ -1403,7 +1404,7 @@ pub fn gen_complex_enum_variant_attr( let member = &spec.rust_ident; let wrapper_ident = format_ident!("__pymethod_variant_cls_{}__", member); let deprecations = &spec.attributes.deprecations; - let python_name = &spec.null_terminated_python_name(); + let python_name = spec.null_terminated_python_name(ctx); let variant_cls = format_ident!("{}_{}", cls, member); let associated_method = quote! { @@ -1580,7 +1581,7 @@ fn complex_enum_variant_field_getter<'a>( let property_type = crate::pymethod::PropertyType::Function { self_type: &self_type, spec: &spec, - doc: crate::get_doc(&[], None), + doc: crate::get_doc(&[], None, ctx), }; let getter = crate::pymethod::impl_py_getter_def(variant_cls_type, property_type, ctx)?; @@ -2014,7 +2015,10 @@ impl<'a> PyClassImplsBuilder<'a> { fn impl_pyclassimpl(&self, ctx: &Ctx) -> Result { let Ctx { pyo3_path } = ctx; let cls = self.cls; - let doc = self.doc.as_ref().map_or(quote! {"\0"}, |doc| quote! {#doc}); + let doc = self.doc.as_ref().map_or( + quote! {#pyo3_path::ffi::c_str!("")}, + PythonDoc::to_token_stream, + ); let is_basetype = self.attr.options.subclass.is_some(); let base = match &self.attr.options.extends { Some(extends_attr) => extends_attr.value.clone(), diff --git a/pyo3-macros-backend/src/pyfunction.rs b/pyo3-macros-backend/src/pyfunction.rs index a5f090a0e2c..147193d18dc 100644 --- a/pyo3-macros-backend/src/pyfunction.rs +++ b/pyo3-macros-backend/src/pyfunction.rs @@ -260,7 +260,7 @@ pub fn impl_wrap_pyfunction( let wrapper_ident = format_ident!("__pyfunction_{}", spec.name); let wrapper = spec.get_wrapper_function(&wrapper_ident, None, ctx)?; - let methoddef = spec.get_methoddef(wrapper_ident, &spec.get_doc(&func.attrs), ctx); + let methoddef = spec.get_methoddef(wrapper_ident, &spec.get_doc(&func.attrs, ctx), ctx); let wrapped_pyfunction = quote! { diff --git a/pyo3-macros-backend/src/pyimpl.rs b/pyo3-macros-backend/src/pyimpl.rs index 27188254557..a1242d49f10 100644 --- a/pyo3-macros-backend/src/pyimpl.rs +++ b/pyo3-macros-backend/src/pyimpl.rs @@ -186,7 +186,7 @@ pub fn gen_py_const(cls: &syn::Type, spec: &ConstSpec<'_>, ctx: &Ctx) -> MethodA let member = &spec.rust_ident; let wrapper_ident = format_ident!("__pymethod_{}__", member); let deprecations = &spec.attributes.deprecations; - let python_name = &spec.null_terminated_python_name(); + let python_name = spec.null_terminated_python_name(ctx); let Ctx { pyo3_path } = ctx; let associated_method = quote! { diff --git a/pyo3-macros-backend/src/pymethod.rs b/pyo3-macros-backend/src/pymethod.rs index 013b15010bf..735b55a169d 100644 --- a/pyo3-macros-backend/src/pymethod.rs +++ b/pyo3-macros-backend/src/pymethod.rs @@ -227,21 +227,21 @@ pub fn gen_py_method( (_, FnType::Fn(_)) => GeneratedPyMethod::Method(impl_py_method_def( cls, spec, - &spec.get_doc(meth_attrs), + &spec.get_doc(meth_attrs, ctx), None, ctx, )?), (_, FnType::FnClass(_)) => GeneratedPyMethod::Method(impl_py_method_def( cls, spec, - &spec.get_doc(meth_attrs), + &spec.get_doc(meth_attrs, ctx), Some(quote!(#pyo3_path::ffi::METH_CLASS)), ctx, )?), (_, FnType::FnStatic) => GeneratedPyMethod::Method(impl_py_method_def( cls, spec, - &spec.get_doc(meth_attrs), + &spec.get_doc(meth_attrs, ctx), Some(quote!(#pyo3_path::ffi::METH_STATIC)), ctx, )?), @@ -255,7 +255,7 @@ pub fn gen_py_method( PropertyType::Function { self_type, spec, - doc: spec.get_doc(meth_attrs), + doc: spec.get_doc(meth_attrs, ctx), }, ctx, )?), @@ -264,7 +264,7 @@ pub fn gen_py_method( PropertyType::Function { self_type, spec, - doc: spec.get_doc(meth_attrs), + doc: spec.get_doc(meth_attrs, ctx), }, ctx, )?), @@ -499,7 +499,7 @@ fn impl_py_class_attribute( }; let wrapper_ident = format_ident!("__pymethod_{}__", name); - let python_name = spec.null_terminated_python_name(); + let python_name = spec.null_terminated_python_name(ctx); let body = quotes::ok_wrap(fncall, ctx); let associated_method = quote! { @@ -560,8 +560,8 @@ pub fn impl_py_setter_def( ctx: &Ctx, ) -> Result { let Ctx { pyo3_path } = ctx; - let python_name = property_type.null_terminated_python_name()?; - let doc = property_type.doc(); + let python_name = property_type.null_terminated_python_name(ctx)?; + let doc = property_type.doc(ctx); let mut holders = Holders::new(); let setter_impl = match property_type { PropertyType::Descriptor { @@ -746,8 +746,8 @@ pub fn impl_py_getter_def( ctx: &Ctx, ) -> Result { let Ctx { pyo3_path } = ctx; - let python_name = property_type.null_terminated_python_name()?; - let doc = property_type.doc(); + let python_name = property_type.null_terminated_python_name(ctx)?; + let doc = property_type.doc(ctx); let mut holders = Holders::new(); let body = match property_type { @@ -870,7 +870,8 @@ pub enum PropertyType<'a> { } impl PropertyType<'_> { - fn null_terminated_python_name(&self) -> Result { + fn null_terminated_python_name(&self, ctx: &Ctx) -> Result { + let Ctx { pyo3_path } = ctx; match self { PropertyType::Descriptor { field, @@ -885,23 +886,22 @@ impl PropertyType<'_> { if let Some(rule) = renaming_rule { name = utils::apply_renaming_rule(*rule, &name); } - name.push('\0'); name } (None, None) => { bail_spanned!(field.span() => "`get` and `set` with tuple struct fields require `name`"); } }; - Ok(syn::LitStr::new(&name, field.span())) + Ok(quote_spanned!(field.span() => #pyo3_path::ffi::c_str!(#name))) } - PropertyType::Function { spec, .. } => Ok(spec.null_terminated_python_name()), + PropertyType::Function { spec, .. } => Ok(spec.null_terminated_python_name(ctx)), } } - fn doc(&self) -> Cow<'_, PythonDoc> { + fn doc(&self, ctx: &Ctx) -> Cow<'_, PythonDoc> { match self { PropertyType::Descriptor { field, .. } => { - Cow::Owned(utils::get_doc(&field.attrs, None)) + Cow::Owned(utils::get_doc(&field.attrs, None, ctx)) } PropertyType::Function { doc, .. } => Cow::Borrowed(doc), } diff --git a/pyo3-macros-backend/src/utils.rs b/pyo3-macros-backend/src/utils.rs index a4c2c5e8a3b..1586379ad10 100644 --- a/pyo3-macros-backend/src/utils.rs +++ b/pyo3-macros-backend/src/utils.rs @@ -1,5 +1,5 @@ use proc_macro2::{Span, TokenStream}; -use quote::ToTokens; +use quote::{quote, ToTokens}; use syn::{punctuated::Punctuated, Token}; use crate::attributes::{CrateAttribute, RenamingRule}; @@ -81,7 +81,12 @@ pub struct PythonDoc(TokenStream); /// If this doc is for a callable, the provided `text_signature` can be passed to prepend /// this to the documentation suitable for Python to extract this into the `__text_signature__` /// attribute. -pub fn get_doc(attrs: &[syn::Attribute], mut text_signature: Option) -> PythonDoc { +pub fn get_doc( + attrs: &[syn::Attribute], + mut text_signature: Option, + ctx: &Ctx, +) -> PythonDoc { + let Ctx { pyo3_path } = ctx; // insert special divider between `__text_signature__` and doc // (assume text_signature is itself well-formed) if let Some(text_signature) = &mut text_signature { @@ -120,7 +125,7 @@ pub fn get_doc(attrs: &[syn::Attribute], mut text_signature: Option) -> } } - if !parts.is_empty() { + let tokens = if !parts.is_empty() { // Doc contained macro pieces - return as `concat!` expression if !current_part.is_empty() { parts.push(current_part.to_token_stream()); @@ -133,15 +138,14 @@ pub fn get_doc(attrs: &[syn::Attribute], mut text_signature: Option) -> syn::token::Bracket(Span::call_site()).surround(&mut tokens, |tokens| { parts.to_tokens(tokens); syn::token::Comma(Span::call_site()).to_tokens(tokens); - syn::LitStr::new("\0", Span::call_site()).to_tokens(tokens); }); - PythonDoc(tokens) + tokens } else { // Just a string doc - return directly with nul terminator - current_part.push('\0'); - PythonDoc(current_part.to_token_stream()) - } + current_part.to_token_stream() + }; + PythonDoc(quote!(#pyo3_path::ffi::c_str!(#tokens))) } impl quote::ToTokens for PythonDoc { diff --git a/src/buffer.rs b/src/buffer.rs index 558fb5e9c8d..85e0e4ce990 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -352,7 +352,7 @@ impl PyBuffer { #[inline] pub fn format(&self) -> &CStr { if self.0.format.is_null() { - CStr::from_bytes_with_nul(b"B\0").unwrap() + ffi::c_str!("B") } else { unsafe { CStr::from_ptr(self.0.format) } } @@ -723,125 +723,124 @@ mod tests { fn test_element_type_from_format() { use super::ElementType; use super::ElementType::*; - use std::ffi::CStr; use std::mem::size_of; use std::os::raw; - for (cstr, expected) in &[ + for (cstr, expected) in [ // @ prefix goes to native_element_type_from_type_char ( - "@b\0", + ffi::c_str!("@b"), SignedInteger { bytes: size_of::(), }, ), ( - "@c\0", + ffi::c_str!("@c"), UnsignedInteger { bytes: size_of::(), }, ), ( - "@b\0", + ffi::c_str!("@b"), SignedInteger { bytes: size_of::(), }, ), ( - "@B\0", + ffi::c_str!("@B"), UnsignedInteger { bytes: size_of::(), }, ), - ("@?\0", Bool), + (ffi::c_str!("@?"), Bool), ( - "@h\0", + ffi::c_str!("@h"), SignedInteger { bytes: size_of::(), }, ), ( - "@H\0", + ffi::c_str!("@H"), UnsignedInteger { bytes: size_of::(), }, ), ( - "@i\0", + ffi::c_str!("@i"), SignedInteger { bytes: size_of::(), }, ), ( - "@I\0", + ffi::c_str!("@I"), UnsignedInteger { bytes: size_of::(), }, ), ( - "@l\0", + ffi::c_str!("@l"), SignedInteger { bytes: size_of::(), }, ), ( - "@L\0", + ffi::c_str!("@L"), UnsignedInteger { bytes: size_of::(), }, ), ( - "@q\0", + ffi::c_str!("@q"), SignedInteger { bytes: size_of::(), }, ), ( - "@Q\0", + ffi::c_str!("@Q"), UnsignedInteger { bytes: size_of::(), }, ), ( - "@n\0", + ffi::c_str!("@n"), SignedInteger { bytes: size_of::(), }, ), ( - "@N\0", + ffi::c_str!("@N"), UnsignedInteger { bytes: size_of::(), }, ), - ("@e\0", Float { bytes: 2 }), - ("@f\0", Float { bytes: 4 }), - ("@d\0", Float { bytes: 8 }), - ("@z\0", Unknown), + (ffi::c_str!("@e"), Float { bytes: 2 }), + (ffi::c_str!("@f"), Float { bytes: 4 }), + (ffi::c_str!("@d"), Float { bytes: 8 }), + (ffi::c_str!("@z"), Unknown), // = prefix goes to standard_element_type_from_type_char - ("=b\0", SignedInteger { bytes: 1 }), - ("=c\0", UnsignedInteger { bytes: 1 }), - ("=B\0", UnsignedInteger { bytes: 1 }), - ("=?\0", Bool), - ("=h\0", SignedInteger { bytes: 2 }), - ("=H\0", UnsignedInteger { bytes: 2 }), - ("=l\0", SignedInteger { bytes: 4 }), - ("=l\0", SignedInteger { bytes: 4 }), - ("=I\0", UnsignedInteger { bytes: 4 }), - ("=L\0", UnsignedInteger { bytes: 4 }), - ("=q\0", SignedInteger { bytes: 8 }), - ("=Q\0", UnsignedInteger { bytes: 8 }), - ("=e\0", Float { bytes: 2 }), - ("=f\0", Float { bytes: 4 }), - ("=d\0", Float { bytes: 8 }), - ("=z\0", Unknown), - ("=0\0", Unknown), + (ffi::c_str!("=b"), SignedInteger { bytes: 1 }), + (ffi::c_str!("=c"), UnsignedInteger { bytes: 1 }), + (ffi::c_str!("=B"), UnsignedInteger { bytes: 1 }), + (ffi::c_str!("=?"), Bool), + (ffi::c_str!("=h"), SignedInteger { bytes: 2 }), + (ffi::c_str!("=H"), UnsignedInteger { bytes: 2 }), + (ffi::c_str!("=l"), SignedInteger { bytes: 4 }), + (ffi::c_str!("=l"), SignedInteger { bytes: 4 }), + (ffi::c_str!("=I"), UnsignedInteger { bytes: 4 }), + (ffi::c_str!("=L"), UnsignedInteger { bytes: 4 }), + (ffi::c_str!("=q"), SignedInteger { bytes: 8 }), + (ffi::c_str!("=Q"), UnsignedInteger { bytes: 8 }), + (ffi::c_str!("=e"), Float { bytes: 2 }), + (ffi::c_str!("=f"), Float { bytes: 4 }), + (ffi::c_str!("=d"), Float { bytes: 8 }), + (ffi::c_str!("=z"), Unknown), + (ffi::c_str!("=0"), Unknown), // unknown prefix -> Unknown - (":b\0", Unknown), + (ffi::c_str!(":b"), Unknown), ] { assert_eq!( - ElementType::from_format(CStr::from_bytes_with_nul(cstr.as_bytes()).unwrap()), - *expected, + ElementType::from_format(cstr), + expected, "element from format &Cstr: {:?}", cstr, ); diff --git a/src/conversions/num_rational.rs b/src/conversions/num_rational.rs index 2129234dc4f..e3a0c7e6d3a 100644 --- a/src/conversions/num_rational.rs +++ b/src/conversions/num_rational.rs @@ -64,28 +64,15 @@ macro_rules! rational_conversion { impl<'py> FromPyObject<'py> for Ratio<$int> { fn extract_bound(obj: &Bound<'py, PyAny>) -> PyResult { let py = obj.py(); - let py_numerator_obj = unsafe { - Bound::from_owned_ptr_or_err( - py, - ffi::PyObject_GetAttrString(obj.as_ptr(), "numerator\0".as_ptr().cast()), - ) - }; - let py_denominator_obj = unsafe { - Bound::from_owned_ptr_or_err( - py, - ffi::PyObject_GetAttrString(obj.as_ptr(), "denominator\0".as_ptr().cast()), - ) - }; + let py_numerator_obj = obj.getattr(crate::intern!(py, "numerator"))?; + let py_denominator_obj = obj.getattr(crate::intern!(py, "denominator"))?; let numerator_owned = unsafe { - Bound::from_owned_ptr_or_err( - py, - ffi::PyNumber_Long(py_numerator_obj?.as_ptr()), - )? + Bound::from_owned_ptr_or_err(py, ffi::PyNumber_Long(py_numerator_obj.as_ptr()))? }; let denominator_owned = unsafe { Bound::from_owned_ptr_or_err( py, - ffi::PyNumber_Long(py_denominator_obj?.as_ptr()), + ffi::PyNumber_Long(py_denominator_obj.as_ptr()), )? }; let rs_numerator: $int = numerator_owned.extract()?; diff --git a/src/err/err_state.rs b/src/err/err_state.rs index 14345b275c9..dc07294a0fa 100644 --- a/src/err/err_state.rs +++ b/src/err/err_state.rs @@ -240,9 +240,7 @@ fn raise_lazy(py: Python<'_>, lazy: Box) { if ffi::PyExceptionClass_Check(ptype.as_ptr()) == 0 { ffi::PyErr_SetString( PyTypeError::type_object_raw(py).cast(), - "exceptions must derive from BaseException\0" - .as_ptr() - .cast(), + ffi::c_str!("exceptions must derive from BaseException").as_ptr(), ) } else { ffi::PyErr_SetObject(ptype.as_ptr(), pvalue.as_ptr()) diff --git a/src/exceptions.rs b/src/exceptions.rs index 82bf3b668c6..496d614fc20 100644 --- a/src/exceptions.rs +++ b/src/exceptions.rs @@ -736,10 +736,10 @@ impl PyUnicodeDecodeError { let pos = err.valid_up_to(); PyUnicodeDecodeError::new_bound( py, - CStr::from_bytes_with_nul(b"utf-8\0").unwrap(), + ffi::c_str!("utf-8"), input, pos..(pos + 1), - CStr::from_bytes_with_nul(b"invalid utf-8\0").unwrap(), + ffi::c_str!("invalid utf-8"), ) } } diff --git a/src/impl_/pyclass.rs b/src/impl_/pyclass.rs index 84c00acdd74..acfce37e6cf 100644 --- a/src/impl_/pyclass.rs +++ b/src/impl_/pyclass.rs @@ -5,7 +5,6 @@ use crate::{ ffi, impl_::freelist::FreeList, impl_::pycell::{GetBorrowChecker, PyClassMutability, PyClassObjectLayout}, - internal_tricks::extract_c_string, pyclass_init::PyObjectInit, types::any::PyAnyMethods, types::PyBool, @@ -214,7 +213,7 @@ pub trait PyClassImpl: Sized + 'static { /// specialization in to the `#[pyclass]` macro from the `#[pymethods]` macro. pub fn build_pyclass_doc( class_name: &'static str, - doc: &'static str, + doc: &'static CStr, text_signature: Option<&'static str>, ) -> PyResult> { if let Some(text_signature) = text_signature { @@ -222,12 +221,12 @@ pub fn build_pyclass_doc( "{}{}\n--\n\n{}", class_name, text_signature, - doc.trim_end_matches('\0') + doc.to_str().unwrap(), )) .map_err(|_| PyValueError::new_err("class doc cannot contain nul bytes"))?; Ok(Cow::Owned(doc)) } else { - extract_c_string(doc, "class doc cannot contain nul bytes") + Ok(Cow::Borrowed(doc)) } } diff --git a/src/impl_/pyclass/lazy_type_object.rs b/src/impl_/pyclass/lazy_type_object.rs index f83fa4c5186..7afaec8a99b 100644 --- a/src/impl_/pyclass/lazy_type_object.rs +++ b/src/impl_/pyclass/lazy_type_object.rs @@ -1,5 +1,4 @@ use std::{ - borrow::Cow, cell::RefCell, ffi::CStr, marker::PhantomData, @@ -151,10 +150,8 @@ impl LazyTypeObjectInner { for class_items in items_iter { for def in class_items.methods { if let PyMethodDefType::ClassAttribute(attr) = def { - let key = attr.attribute_c_string().unwrap(); - match (attr.meth)(py) { - Ok(val) => items.push((key, val)), + Ok(val) => items.push((attr.name, val)), Err(err) => { return Err(wrap_in_runtime_error( py, @@ -162,7 +159,7 @@ impl LazyTypeObjectInner { format!( "An error occurred while initializing `{}.{}`", name, - attr.name.trim_end_matches('\0') + attr.name.to_str().unwrap() ), )) } @@ -198,7 +195,7 @@ impl LazyTypeObjectInner { fn initialize_tp_dict( py: Python<'_>, type_object: *mut ffi::PyObject, - items: Vec<(Cow<'static, CStr>, PyObject)>, + items: Vec<(&'static CStr, PyObject)>, ) -> PyResult<()> { // We hold the GIL: the dictionary update can be considered atomic from // the POV of other threads. diff --git a/src/impl_/pymethods.rs b/src/impl_/pymethods.rs index 44b2af25650..2b63d1e4ae8 100644 --- a/src/impl_/pymethods.rs +++ b/src/impl_/pymethods.rs @@ -2,7 +2,6 @@ use crate::callback::IntoPyCallbackOutput; use crate::exceptions::PyStopAsyncIteration; use crate::gil::LockGIL; use crate::impl_::panic::PanicTrap; -use crate::internal_tricks::extract_c_string; use crate::pycell::{PyBorrowError, PyBorrowMutError}; use crate::pyclass::boolean_struct::False; use crate::types::any::PyAnyMethods; @@ -12,7 +11,6 @@ use crate::{ ffi, Borrowed, Bound, DowncastError, Py, PyAny, PyClass, PyClassInitializer, PyErr, PyObject, PyRef, PyRefMut, PyResult, PyTraverseError, PyTypeCheck, PyVisit, Python, }; -use std::borrow::Cow; use std::ffi::CStr; use std::fmt; use std::os::raw::{c_int, c_void}; @@ -84,36 +82,30 @@ pub type PyClassAttributeFactory = for<'p> fn(Python<'p>) -> PyResult; #[derive(Clone, Debug)] pub struct PyMethodDef { - pub(crate) ml_name: &'static str, + pub(crate) ml_name: &'static CStr, pub(crate) ml_meth: PyMethodType, pub(crate) ml_flags: c_int, - pub(crate) ml_doc: &'static str, + pub(crate) ml_doc: &'static CStr, } #[derive(Copy, Clone)] pub struct PyClassAttributeDef { - pub(crate) name: &'static str, + pub(crate) name: &'static CStr, pub(crate) meth: PyClassAttributeFactory, } -impl PyClassAttributeDef { - pub(crate) fn attribute_c_string(&self) -> PyResult> { - extract_c_string(self.name, "class attribute name cannot contain nul bytes") - } -} - #[derive(Clone)] pub struct PyGetterDef { - pub(crate) name: &'static str, + pub(crate) name: &'static CStr, pub(crate) meth: Getter, - pub(crate) doc: &'static str, + pub(crate) doc: &'static CStr, } #[derive(Clone)] pub struct PySetterDef { - pub(crate) name: &'static str, + pub(crate) name: &'static CStr, pub(crate) meth: Setter, - pub(crate) doc: &'static str, + pub(crate) doc: &'static CStr, } unsafe impl Sync for PyMethodDef {} @@ -125,44 +117,44 @@ unsafe impl Sync for PySetterDef {} impl PyMethodDef { /// Define a function with no `*args` and `**kwargs`. pub const fn noargs( - name: &'static str, + ml_name: &'static CStr, cfunction: ffi::PyCFunction, - doc: &'static str, + ml_doc: &'static CStr, ) -> Self { Self { - ml_name: name, + ml_name, ml_meth: PyMethodType::PyCFunction(cfunction), ml_flags: ffi::METH_NOARGS, - ml_doc: doc, + ml_doc, } } /// Define a function that can take `*args` and `**kwargs`. pub const fn cfunction_with_keywords( - name: &'static str, + ml_name: &'static CStr, cfunction: ffi::PyCFunctionWithKeywords, - doc: &'static str, + ml_doc: &'static CStr, ) -> Self { Self { - ml_name: name, + ml_name, ml_meth: PyMethodType::PyCFunctionWithKeywords(cfunction), ml_flags: ffi::METH_VARARGS | ffi::METH_KEYWORDS, - ml_doc: doc, + ml_doc, } } /// Define a function that can take `*args` and `**kwargs`. #[cfg(not(Py_LIMITED_API))] pub const fn fastcall_cfunction_with_keywords( - name: &'static str, + ml_name: &'static CStr, cfunction: ffi::_PyCFunctionFastWithKeywords, - doc: &'static str, + ml_doc: &'static CStr, ) -> Self { Self { - ml_name: name, + ml_name, ml_meth: PyMethodType::PyCFunctionFastWithKeywords(cfunction), ml_flags: ffi::METH_FASTCALL | ffi::METH_KEYWORDS, - ml_doc: doc, + ml_doc, } } @@ -172,7 +164,7 @@ impl PyMethodDef { } /// Convert `PyMethodDef` to Python method definition struct `ffi::PyMethodDef` - pub(crate) fn as_method_def(&self) -> PyResult<(ffi::PyMethodDef, PyMethodDefDestructor)> { + pub(crate) fn as_method_def(&self) -> ffi::PyMethodDef { let meth = match self.ml_meth { PyMethodType::PyCFunction(meth) => ffi::PyMethodDefPointer { PyCFunction: meth }, PyMethodType::PyCFunctionWithKeywords(meth) => ffi::PyMethodDefPointer { @@ -184,22 +176,18 @@ impl PyMethodDef { }, }; - let name = get_name(self.ml_name)?; - let doc = get_doc(self.ml_doc)?; - let def = ffi::PyMethodDef { - ml_name: name.as_ptr(), + ffi::PyMethodDef { + ml_name: self.ml_name.as_ptr(), ml_meth: meth, ml_flags: self.ml_flags, - ml_doc: doc.as_ptr(), - }; - let destructor = PyMethodDefDestructor { name, doc }; - Ok((def, destructor)) + ml_doc: self.ml_doc.as_ptr(), + } } } impl PyClassAttributeDef { /// Define a class attribute. - pub const fn new(name: &'static str, meth: PyClassAttributeFactory) -> Self { + pub const fn new(name: &'static CStr, meth: PyClassAttributeFactory) -> Self { Self { name, meth } } } @@ -222,7 +210,7 @@ pub(crate) type Setter = impl PyGetterDef { /// Define a getter. - pub const fn new(name: &'static str, getter: Getter, doc: &'static str) -> Self { + pub const fn new(name: &'static CStr, getter: Getter, doc: &'static CStr) -> Self { Self { name, meth: getter, @@ -233,7 +221,7 @@ impl PyGetterDef { impl PySetterDef { /// Define a setter. - pub const fn new(name: &'static str, setter: Setter, doc: &'static str) -> Self { + pub const fn new(name: &'static CStr, setter: Setter, doc: &'static CStr) -> Self { Self { name, meth: setter, @@ -284,22 +272,6 @@ where retval } -pub(crate) struct PyMethodDefDestructor { - // These members are just to avoid leaking CStrings when possible - #[allow(dead_code)] - name: Cow<'static, CStr>, - #[allow(dead_code)] - doc: Cow<'static, CStr>, -} - -pub(crate) fn get_name(name: &'static str) -> PyResult> { - extract_c_string(name, "function name cannot contain NUL byte.") -} - -pub(crate) fn get_doc(doc: &'static str) -> PyResult> { - extract_c_string(doc, "function doc cannot contain NUL byte.") -} - // Autoref-based specialization for handling `__next__` returning `Option` pub struct IterBaseTag; diff --git a/src/impl_/pymodule.rs b/src/impl_/pymodule.rs index 0c3d8951fc9..b05652bced8 100644 --- a/src/impl_/pymodule.rs +++ b/src/impl_/pymodule.rs @@ -1,6 +1,6 @@ //! Implementation details of `#[pymodule]` which need to be accessible from proc-macro generated code. -use std::{cell::UnsafeCell, marker::PhantomData}; +use std::{cell::UnsafeCell, ffi::CStr, marker::PhantomData}; #[cfg(all( not(any(PyPy, GraalPy)), @@ -49,12 +49,9 @@ unsafe impl Sync for ModuleDef {} impl ModuleDef { /// Make new module definition with given module name. - /// - /// # Safety - /// `name` and `doc` must be null-terminated strings. pub const unsafe fn new( - name: &'static str, - doc: &'static str, + name: &'static CStr, + doc: &'static CStr, initializer: ModuleInitializer, ) -> Self { const INIT: ffi::PyModuleDef = ffi::PyModuleDef { @@ -70,8 +67,8 @@ impl ModuleDef { }; let ffi_def = UnsafeCell::new(ffi::PyModuleDef { - m_name: name.as_ptr().cast(), - m_doc: doc.as_ptr().cast(), + m_name: name.as_ptr(), + m_doc: doc.as_ptr(), ..INIT }); @@ -215,10 +212,12 @@ impl PyAddToModule for ModuleDef { mod tests { use std::{ borrow::Cow, + ffi::CStr, sync::atomic::{AtomicBool, Ordering}, }; use crate::{ + ffi, types::{any::PyAnyMethods, module::PyModuleMethods, PyModule}, Bound, PyResult, Python, }; @@ -229,8 +228,8 @@ mod tests { fn module_init() { static MODULE_DEF: ModuleDef = unsafe { ModuleDef::new( - "test_module\0", - "some doc\0", + ffi::c_str!("test_module"), + ffi::c_str!("some doc"), ModuleInitializer(|m| { m.add("SOME_CONSTANT", 42)?; Ok(()) @@ -270,8 +269,8 @@ mod tests { fn module_def_new() { // To get coverage for ModuleDef::new() need to create a non-static ModuleDef, however init // etc require static ModuleDef, so this test needs to be separated out. - static NAME: &str = "test_module\0"; - static DOC: &str = "some doc\0"; + static NAME: &CStr = ffi::c_str!("test_module"); + static DOC: &CStr = ffi::c_str!("some doc"); static INIT_CALLED: AtomicBool = AtomicBool::new(false); diff --git a/src/internal_tricks.rs b/src/internal_tricks.rs index a8873dda007..0ee424f9db4 100644 --- a/src/internal_tricks.rs +++ b/src/internal_tricks.rs @@ -1,13 +1,4 @@ -use std::{ - borrow::Cow, - ffi::{CStr, CString}, -}; - -use crate::{ - exceptions::PyValueError, - ffi::{Py_ssize_t, PY_SSIZE_T_MAX}, - PyResult, -}; +use crate::ffi::{Py_ssize_t, PY_SSIZE_T_MAX}; pub struct PrivateMarker; macro_rules! private_decl { @@ -193,31 +184,6 @@ pub(crate) fn slice_index_order_fail(index: usize, end: usize) -> ! { panic!("slice index starts at {} but ends at {}", index, end); } -pub(crate) fn extract_c_string( - src: &'static str, - err_msg: &'static str, -) -> PyResult> { - let bytes = src.as_bytes(); - let cow = match bytes { - [] => { - // Empty string, we can trivially refer to a static "\0" string - Cow::Borrowed(unsafe { CStr::from_bytes_with_nul_unchecked(b"\0") }) - } - [.., 0] => { - // Last byte is a nul; try to create as a CStr - let c_str = - CStr::from_bytes_with_nul(bytes).map_err(|_| PyValueError::new_err(err_msg))?; - Cow::Borrowed(c_str) - } - _ => { - // Allocate a new CString for this - let c_string = CString::new(bytes).map_err(|_| PyValueError::new_err(err_msg))?; - Cow::Owned(c_string) - } - }; - Ok(cow) -} - // TODO: use ptr::from_ref on MSRV 1.76 #[inline] pub(crate) const fn ptr_from_ref(t: &T) -> *const T { diff --git a/src/macros.rs b/src/macros.rs index 9316b871390..ab91d577ac5 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -214,7 +214,7 @@ macro_rules! append_to_inittab { ); } $crate::ffi::PyImport_AppendInittab( - $module::__PYO3_NAME.as_ptr().cast(), + $module::__PYO3_NAME.as_ptr(), ::std::option::Option::Some($module::__pyo3_init), ); } diff --git a/src/marker.rs b/src/marker.rs index 62d8a89ba53..dae4fcab44d 100644 --- a/src/marker.rs +++ b/src/marker.rs @@ -652,7 +652,7 @@ impl<'py> Python<'py> { ) -> PyResult> { let code = CString::new(code)?; unsafe { - let mptr = ffi::PyImport_AddModule("__main__\0".as_ptr().cast()); + let mptr = ffi::PyImport_AddModule(ffi::c_str!("__main__").as_ptr()); if mptr.is_null() { return Err(PyErr::fetch(self)); } @@ -685,7 +685,8 @@ impl<'py> Python<'py> { } } - let code_obj = ffi::Py_CompileString(code.as_ptr(), "\0".as_ptr() as _, start); + let code_obj = + ffi::Py_CompileString(code.as_ptr(), ffi::c_str!("").as_ptr(), start); if code_obj.is_null() { return Err(PyErr::fetch(self)); } diff --git a/src/pyclass/create_type_object.rs b/src/pyclass/create_type_object.rs index 01b357763ad..fd1d2b34998 100644 --- a/src/pyclass/create_type_object.rs +++ b/src/pyclass/create_type_object.rs @@ -7,7 +7,7 @@ use crate::{ assign_sequence_item_from_mapping, get_sequence_item_from_mapping, tp_dealloc, tp_dealloc_with_gc, PyClassItemsIter, }, - pymethods::{get_doc, get_name, Getter, Setter}, + pymethods::{Getter, Setter}, trampoline::trampoline, }, internal_tricks::ptr_from_ref, @@ -15,7 +15,6 @@ use crate::{ Py, PyClass, PyGetterDef, PyMethodDefType, PyResult, PySetterDef, PyTypeInfo, Python, }; use std::{ - borrow::Cow, collections::HashMap, ffi::{CStr, CString}, os::raw::{c_char, c_int, c_ulong, c_void}, @@ -103,7 +102,7 @@ type PyTypeBuilderCleanup = Box; struct PyTypeBuilder { slots: Vec, method_defs: Vec, - getset_builders: HashMap<&'static str, GetSetDefBuilder>, + getset_builders: HashMap<&'static CStr, GetSetDefBuilder>, /// Used to patch the type objects for the things there's no /// PyType_FromSpec API for... there's no reason this should work, /// except for that it does and we have tests. @@ -173,32 +172,25 @@ impl PyTypeBuilder { fn pymethod_def(&mut self, def: &PyMethodDefType) { match def { - PyMethodDefType::Getter(getter) => { - self.getset_builders - .entry(getter.name) - .or_default() - .add_getter(getter); - } - PyMethodDefType::Setter(setter) => { - self.getset_builders - .entry(setter.name) - .or_default() - .add_setter(setter); - } + PyMethodDefType::Getter(getter) => self + .getset_builders + .entry(getter.name) + .or_default() + .add_getter(getter), + PyMethodDefType::Setter(setter) => self + .getset_builders + .entry(setter.name) + .or_default() + .add_setter(setter), PyMethodDefType::Method(def) | PyMethodDefType::Class(def) - | PyMethodDefType::Static(def) => { - let (def, destructor) = def.as_method_def().unwrap(); - // FIXME: stop leaking destructor - std::mem::forget(destructor); - self.method_defs.push(def); - } + | PyMethodDefType::Static(def) => self.method_defs.push(def.as_method_def()), // These class attributes are added after the type gets created by LazyStaticType PyMethodDefType::ClassAttribute(_) => {} } } - fn finalize_methods_and_properties(&mut self) -> PyResult> { + fn finalize_methods_and_properties(&mut self) -> Vec { let method_defs: Vec = std::mem::take(&mut self.method_defs); // Safety: Py_tp_methods expects a raw vec of PyMethodDef unsafe { self.push_raw_vec_slot(ffi::Py_tp_methods, method_defs) }; @@ -210,11 +202,11 @@ impl PyTypeBuilder { .getset_builders .iter() .map(|(name, builder)| { - let (def, destructor) = builder.as_get_set_def(name)?; + let (def, destructor) = builder.as_get_set_def(name); getset_destructors.push(destructor); - Ok(def) + def }) - .collect::>()?; + .collect(); // PyPy automatically adds __dict__ getter / setter. #[cfg(not(PyPy))] @@ -261,7 +253,7 @@ impl PyTypeBuilder { } property_defs.push(ffi::PyGetSetDef { - name: "__dict__\0".as_ptr().cast(), + name: ffi::c_str!("__dict__").as_ptr(), get: Some(get_dict), set: Some(ffi::PyObject_GenericSetDict), doc: ptr::null(), @@ -300,7 +292,7 @@ impl PyTypeBuilder { } } - Ok(getset_destructors) + getset_destructors } fn set_is_basetype(mut self, is_basetype: bool) -> Self { @@ -358,7 +350,7 @@ impl PyTypeBuilder { #[cfg(Py_3_9)] { #[inline(always)] - fn offset_def(name: &'static str, offset: ffi::Py_ssize_t) -> ffi::PyMemberDef { + fn offset_def(name: &'static CStr, offset: ffi::Py_ssize_t) -> ffi::PyMemberDef { ffi::PyMemberDef { name: name.as_ptr().cast(), type_code: ffi::Py_T_PYSSIZET, @@ -372,12 +364,15 @@ impl PyTypeBuilder { // __dict__ support if let Some(dict_offset) = dict_offset { - members.push(offset_def("__dictoffset__\0", dict_offset)); + members.push(offset_def(ffi::c_str!("__dictoffset__"), dict_offset)); } // weakref support if let Some(weaklist_offset) = weaklist_offset { - members.push(offset_def("__weaklistoffset__\0", weaklist_offset)); + members.push(offset_def( + ffi::c_str!("__weaklistoffset__"), + weaklist_offset, + )); } // Safety: Py_tp_members expects a raw vec of PyMemberDef @@ -417,7 +412,7 @@ impl PyTypeBuilder { // on some platforms (like windows) #![allow(clippy::useless_conversion)] - let getset_destructors = self.finalize_methods_and_properties()?; + let getset_destructors = self.finalize_methods_and_properties(); unsafe { self.push_slot(ffi::Py_tp_base, self.tp_base) } @@ -531,7 +526,7 @@ unsafe extern "C" fn no_constructor_defined( #[derive(Default)] struct GetSetDefBuilder { - doc: Option<&'static str>, + doc: Option<&'static CStr>, getter: Option, setter: Option, } @@ -555,13 +550,7 @@ impl GetSetDefBuilder { self.setter = Some(setter.meth) } - fn as_get_set_def( - &self, - name: &'static str, - ) -> PyResult<(ffi::PyGetSetDef, GetSetDefDestructor)> { - let name = get_name(name)?; - let doc = self.doc.map(get_doc).transpose()?; - + fn as_get_set_def(&self, name: &'static CStr) -> (ffi::PyGetSetDef, GetSetDefDestructor) { let getset_type = match (self.getter, self.setter) { (Some(getter), None) => GetSetDefType::Getter(getter), (None, Some(setter)) => GetSetDefType::Setter(setter), @@ -573,20 +562,16 @@ impl GetSetDefBuilder { } }; - let getset_def = getset_type.create_py_get_set_def(&name, doc.as_deref()); + let getset_def = getset_type.create_py_get_set_def(name, self.doc); let destructor = GetSetDefDestructor { - name, - doc, closure: getset_type, }; - Ok((getset_def, destructor)) + (getset_def, destructor) } } #[allow(dead_code)] // a stack of fields which are purely to cache until dropped struct GetSetDefDestructor { - name: Cow<'static, CStr>, - doc: Option>, closure: GetSetDefType, } diff --git a/src/types/function.rs b/src/types/function.rs index a127b4e0574..c2eec04d42f 100644 --- a/src/types/function.rs +++ b/src/types/function.rs @@ -8,7 +8,7 @@ use crate::types::module::PyModuleMethods; use crate::PyNativeType; use crate::{ ffi, - impl_::pymethods::{self, PyMethodDef, PyMethodDefDestructor}, + impl_::pymethods::{self, PyMethodDef}, types::{PyCapsule, PyDict, PyModule, PyString, PyTuple}, }; use crate::{Bound, IntoPy, Py, PyAny, PyResult, Python}; @@ -30,8 +30,8 @@ impl PyCFunction { )] pub fn new_with_keywords<'a>( fun: ffi::PyCFunctionWithKeywords, - name: &'static str, - doc: &'static str, + name: &'static CStr, + doc: &'static CStr, py_or_module: PyFunctionArguments<'a>, ) -> PyResult<&'a Self> { let (py, module) = py_or_module.into_py_and_maybe_module(); @@ -44,11 +44,14 @@ impl PyCFunction { } /// Create a new built-in function with keywords (*args and/or **kwargs). + /// + /// To create `name` and `doc` static strings on Rust versions older than 1.77 (which added c"" literals), + /// use the [`c_str!`](crate::ffi::c_str) macro. pub fn new_with_keywords_bound<'py>( py: Python<'py>, fun: ffi::PyCFunctionWithKeywords, - name: &'static str, - doc: &'static str, + name: &'static CStr, + doc: &'static CStr, module: Option<&Bound<'py, PyModule>>, ) -> PyResult> { Self::internal_new( @@ -66,8 +69,8 @@ impl PyCFunction { )] pub fn new<'a>( fun: ffi::PyCFunction, - name: &'static str, - doc: &'static str, + name: &'static CStr, + doc: &'static CStr, py_or_module: PyFunctionArguments<'a>, ) -> PyResult<&'a Self> { let (py, module) = py_or_module.into_py_and_maybe_module(); @@ -80,11 +83,14 @@ impl PyCFunction { } /// Create a new built-in function which takes no arguments. + /// + /// To create `name` and `doc` static strings on Rust versions older than 1.77 (which added c"" literals), + /// use the [`c_str!`](crate::ffi::c_str) macro. pub fn new_bound<'py>( py: Python<'py>, fun: ffi::PyCFunction, - name: &'static str, - doc: &'static str, + name: &'static CStr, + doc: &'static CStr, module: Option<&Bound<'py, PyModule>>, ) -> PyResult> { Self::internal_new(py, &PyMethodDef::noargs(name, fun, doc), module) @@ -98,8 +104,8 @@ impl PyCFunction { )] pub fn new_closure<'a, F, R>( py: Python<'a>, - name: Option<&'static str>, - doc: Option<&'static str>, + name: Option<&'static CStr>, + doc: Option<&'static CStr>, closure: F, ) -> PyResult<&'a PyCFunction> where @@ -131,29 +137,27 @@ impl PyCFunction { /// ``` pub fn new_closure_bound<'py, F, R>( py: Python<'py>, - name: Option<&'static str>, - doc: Option<&'static str>, + name: Option<&'static CStr>, + doc: Option<&'static CStr>, closure: F, ) -> PyResult> where F: Fn(&Bound<'_, PyTuple>, Option<&Bound<'_, PyDict>>) -> R + Send + 'static, R: crate::callback::IntoPyCallbackOutput<*mut ffi::PyObject>, { - let method_def = pymethods::PyMethodDef::cfunction_with_keywords( - name.unwrap_or("pyo3-closure\0"), - run_closure::, - doc.unwrap_or("\0"), - ); - let (def, def_destructor) = method_def.as_method_def()?; + let name = name.unwrap_or(ffi::c_str!("pyo3-closure")); + let doc = doc.unwrap_or(ffi::c_str!("")); + let method_def = + pymethods::PyMethodDef::cfunction_with_keywords(name, run_closure::, doc); + let def = method_def.as_method_def(); let capsule = PyCapsule::new_bound( py, ClosureDestructor:: { closure, def: UnsafeCell::new(def), - def_destructor, }, - Some(closure_capsule_name().to_owned()), + Some(CLOSURE_CAPSULE_NAME.to_owned()), )?; // Safety: just created the capsule with type ClosureDestructor above @@ -178,11 +182,10 @@ impl PyCFunction { } else { (std::ptr::null_mut(), None) }; - let (def, destructor) = method_def.as_method_def()?; + let def = method_def.as_method_def(); - // FIXME: stop leaking the def and destructor + // FIXME: stop leaking the def let def = Box::into_raw(Box::new(def)); - std::mem::forget(destructor); let module_name_ptr = module_name .as_ref() @@ -196,10 +199,7 @@ impl PyCFunction { } } -fn closure_capsule_name() -> &'static CStr { - // TODO replace this with const CStr once MSRV new enough - CStr::from_bytes_with_nul(b"pyo3-closure\0").unwrap() -} +static CLOSURE_CAPSULE_NAME: &CStr = ffi::c_str!("pyo3-closure"); unsafe extern "C" fn run_closure( capsule_ptr: *mut ffi::PyObject, @@ -218,7 +218,7 @@ where kwargs, |py, capsule_ptr, args, kwargs| { let boxed_fn: &ClosureDestructor = - &*(ffi::PyCapsule_GetPointer(capsule_ptr, closure_capsule_name().as_ptr()) + &*(ffi::PyCapsule_GetPointer(capsule_ptr, CLOSURE_CAPSULE_NAME.as_ptr()) as *mut ClosureDestructor); let args = Bound::ref_from_ptr(py, &args).downcast_unchecked::(); let kwargs = Bound::ref_from_ptr_or_opt(py, &kwargs) @@ -235,9 +235,6 @@ struct ClosureDestructor { // Wrapped in UnsafeCell because Python C-API wants a *mut pointer // to this member. def: UnsafeCell, - // Used to destroy the cstrings in `def`, if necessary. - #[allow(dead_code)] - def_destructor: PyMethodDefDestructor, } // Safety: F is send and none of the fields are ever mutated diff --git a/src/types/string.rs b/src/types/string.rs index 8556e41dcce..828e0024bda 100644 --- a/src/types/string.rs +++ b/src/types/string.rs @@ -78,7 +78,7 @@ impl<'a> PyStringData<'a> { Err(PyUnicodeDecodeError::new_bound( py, - CStr::from_bytes_with_nul(b"utf-16\0").unwrap(), + ffi::c_str!("utf-16"), self.as_bytes(), 0..self.as_bytes().len(), CStr::from_bytes_with_nul(&message).unwrap(), @@ -90,10 +90,10 @@ impl<'a> PyStringData<'a> { Some(s) => Ok(Cow::Owned(s)), None => Err(PyUnicodeDecodeError::new_bound( py, - CStr::from_bytes_with_nul(b"utf-32\0").unwrap(), + ffi::c_str!("utf-32"), self.as_bytes(), 0..self.as_bytes().len(), - CStr::from_bytes_with_nul(b"error converting utf-32\0").unwrap(), + ffi::c_str!("error converting utf-32"), )? .into()), }, @@ -414,8 +414,8 @@ impl<'a> Borrowed<'a, '_, PyString> { let bytes = unsafe { ffi::PyUnicode_AsEncodedString( ptr, - b"utf-8\0".as_ptr().cast(), - b"surrogatepass\0".as_ptr().cast(), + ffi::c_str!("utf-8").as_ptr(), + ffi::c_str!("surrogatepass").as_ptr(), ) .assume_owned(py) .downcast_into_unchecked::() diff --git a/tests/test_buffer.rs b/tests/test_buffer.rs index 0b3da881884..9e2a6a4d1c5 100644 --- a/tests/test_buffer.rs +++ b/tests/test_buffer.rs @@ -3,7 +3,6 @@ use pyo3::{buffer::PyBuffer, exceptions::PyBufferError, ffi, prelude::*}; use std::{ - ffi::CStr, os::raw::{c_int, c_void}, ptr, }; @@ -48,7 +47,7 @@ impl TestBufferErrors { (*view).readonly = 1; (*view).itemsize = std::mem::size_of::() as isize; - let msg = CStr::from_bytes_with_nul(b"I\0").unwrap(); + let msg = ffi::c_str!("I"); (*view).format = msg.as_ptr() as *mut _; (*view).ndim = 1; @@ -72,7 +71,7 @@ impl TestBufferErrors { (*view).itemsize += 1; } IncorrectFormat => { - (*view).format = CStr::from_bytes_with_nul(b"B\0").unwrap().as_ptr() as _; + (*view).format = ffi::c_str!("B").as_ptr() as _; } IncorrectAlignment => (*view).buf = (*view).buf.add(1), } diff --git a/tests/test_pyfunction.rs b/tests/test_pyfunction.rs index 9028f71a232..f3a04a53a95 100644 --- a/tests/test_pyfunction.rs +++ b/tests/test_pyfunction.rs @@ -4,6 +4,7 @@ use std::collections::HashMap; #[cfg(not(Py_LIMITED_API))] use pyo3::buffer::PyBuffer; +use pyo3::ffi::c_str; use pyo3::prelude::*; #[cfg(not(Py_LIMITED_API))] use pyo3::types::PyDateTime; @@ -344,8 +345,8 @@ fn test_pycfunction_new() { let py_fn = PyCFunction::new_bound( py, c_fn, - "py_fn", - "py_fn for test (this is the docstring)", + c_str!("py_fn"), + c_str!("py_fn for test (this is the docstring)"), None, ) .unwrap(); @@ -402,8 +403,8 @@ fn test_pycfunction_new_with_keywords() { let py_fn = PyCFunction::new_with_keywords_bound( py, c_fn, - "py_fn", - "py_fn for test (this is the docstring)", + c_str!("py_fn"), + c_str!("py_fn for test (this is the docstring)"), None, ) .unwrap(); @@ -443,8 +444,13 @@ fn test_closure() { Ok(res) }) }; - let closure_py = - PyCFunction::new_closure_bound(py, Some("test_fn"), Some("test_fn doc"), f).unwrap(); + let closure_py = PyCFunction::new_closure_bound( + py, + Some(c_str!("test_fn")), + Some(c_str!("test_fn doc")), + f, + ) + .unwrap(); py_assert!(py, closure_py, "closure_py(42) == [43]"); py_assert!(py, closure_py, "closure_py.__name__ == 'test_fn'"); From e6b2216b04511bddc76a4e2c47bb4bc5d8340669 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?W=C3=81NG=20Xu=C4=9Bru=C3=AC?= Date: Thu, 20 Jun 2024 16:16:06 +0800 Subject: [PATCH 117/495] Add several missing wrappers to PyAnyMethods (#4264) --- newsfragments/4264.changed.md | 1 + src/types/any.rs | 52 +++++++++++++++++++++++++++++++++++ tests/test_arithmetics.rs | 44 +++++++++++++++++++++++++++++ 3 files changed, 97 insertions(+) create mode 100644 newsfragments/4264.changed.md diff --git a/newsfragments/4264.changed.md b/newsfragments/4264.changed.md new file mode 100644 index 00000000000..a3e89c6f98c --- /dev/null +++ b/newsfragments/4264.changed.md @@ -0,0 +1 @@ +Added `PyAnyMethods::{bitnot, matmul, floor_div, rem, divmod}` for completeness. diff --git a/src/types/any.rs b/src/types/any.rs index f2a86ff528d..c8c6d67e534 100644 --- a/src/types/any.rs +++ b/src/types/any.rs @@ -1143,6 +1143,9 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed { /// Equivalent to the Python expression `abs(self)`. fn abs(&self) -> PyResult>; + /// Computes `~self`. + fn bitnot(&self) -> PyResult>; + /// Tests whether this object is less than another. /// /// This is equivalent to the Python expression `self < other`. @@ -1200,11 +1203,31 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed { where O: ToPyObject; + /// Computes `self @ other`. + fn matmul(&self, other: O) -> PyResult> + where + O: ToPyObject; + /// Computes `self / other`. fn div(&self, other: O) -> PyResult> where O: ToPyObject; + /// Computes `self // other`. + fn floor_div(&self, other: O) -> PyResult> + where + O: ToPyObject; + + /// Computes `self % other`. + fn rem(&self, other: O) -> PyResult> + where + O: ToPyObject; + + /// Computes `divmod(self, other)`. + fn divmod(&self, other: O) -> PyResult> + where + O: ToPyObject; + /// Computes `self << other`. fn lshift(&self, other: O) -> PyResult> where @@ -1898,6 +1921,14 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { inner(self) } + fn bitnot(&self) -> PyResult> { + fn inner<'py>(any: &Bound<'py, PyAny>) -> PyResult> { + unsafe { ffi::PyNumber_Invert(any.as_ptr()).assume_owned_or_err(any.py()) } + } + + inner(self) + } + fn lt(&self, other: O) -> PyResult where O: ToPyObject, @@ -1949,13 +1980,34 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { implement_binop!(add, PyNumber_Add, "+"); implement_binop!(sub, PyNumber_Subtract, "-"); implement_binop!(mul, PyNumber_Multiply, "*"); + implement_binop!(matmul, PyNumber_MatrixMultiply, "@"); implement_binop!(div, PyNumber_TrueDivide, "/"); + implement_binop!(floor_div, PyNumber_FloorDivide, "//"); + implement_binop!(rem, PyNumber_Remainder, "%"); implement_binop!(lshift, PyNumber_Lshift, "<<"); implement_binop!(rshift, PyNumber_Rshift, ">>"); implement_binop!(bitand, PyNumber_And, "&"); implement_binop!(bitor, PyNumber_Or, "|"); implement_binop!(bitxor, PyNumber_Xor, "^"); + /// Computes `divmod(self, other)`. + fn divmod(&self, other: O) -> PyResult> + where + O: ToPyObject, + { + fn inner<'py>( + any: &Bound<'py, PyAny>, + other: Bound<'_, PyAny>, + ) -> PyResult> { + unsafe { + ffi::PyNumber_Divmod(any.as_ptr(), other.as_ptr()).assume_owned_or_err(any.py()) + } + } + + let py = self.py(); + inner(self, other.to_object(py).into_bound(py)) + } + /// Computes `self ** other % modulus` (`pow(self, other, modulus)`). /// `py.None()` may be passed for the `modulus`. fn pow(&self, other: O1, modulus: O2) -> PyResult> diff --git a/tests/test_arithmetics.rs b/tests/test_arithmetics.rs index 007f42a79e8..0cee2f9cf84 100644 --- a/tests/test_arithmetics.rs +++ b/tests/test_arithmetics.rs @@ -35,6 +35,10 @@ impl UnaryArithmetic { Self::new(self.inner.abs()) } + fn __invert__(&self) -> Self { + Self::new(self.inner.recip()) + } + #[pyo3(signature=(_ndigits=None))] fn __round__(&self, _ndigits: Option) -> Self { Self::new(self.inner.round()) @@ -48,8 +52,18 @@ fn unary_arithmetic() { py_run!(py, c, "assert repr(-c) == 'UA(-2.7)'"); py_run!(py, c, "assert repr(+c) == 'UA(2.7)'"); py_run!(py, c, "assert repr(abs(c)) == 'UA(2.7)'"); + py_run!(py, c, "assert repr(~c) == 'UA(0.37037037037037035)'"); py_run!(py, c, "assert repr(round(c)) == 'UA(3)'"); py_run!(py, c, "assert repr(round(c, 1)) == 'UA(3)'"); + + let c: Bound<'_, PyAny> = c.extract(py).unwrap(); + assert_py_eq!(c.neg().unwrap().repr().unwrap().as_any(), "UA(-2.7)"); + assert_py_eq!(c.pos().unwrap().repr().unwrap().as_any(), "UA(2.7)"); + assert_py_eq!(c.abs().unwrap().repr().unwrap().as_any(), "UA(2.7)"); + assert_py_eq!( + c.bitnot().unwrap().repr().unwrap().as_any(), + "UA(0.37037037037037035)" + ); }); } @@ -179,10 +193,26 @@ impl BinaryArithmetic { format!("BA * {:?}", rhs) } + fn __matmul__(&self, rhs: &Bound<'_, PyAny>) -> String { + format!("BA @ {:?}", rhs) + } + fn __truediv__(&self, rhs: &Bound<'_, PyAny>) -> String { format!("BA / {:?}", rhs) } + fn __floordiv__(&self, rhs: &Bound<'_, PyAny>) -> String { + format!("BA // {:?}", rhs) + } + + fn __mod__(&self, rhs: &Bound<'_, PyAny>) -> String { + format!("BA % {:?}", rhs) + } + + fn __divmod__(&self, rhs: &Bound<'_, PyAny>) -> String { + format!("divmod(BA, {:?})", rhs) + } + fn __lshift__(&self, rhs: &Bound<'_, PyAny>) -> String { format!("BA << {:?}", rhs) } @@ -217,6 +247,11 @@ fn binary_arithmetic() { py_run!(py, c, "assert c + 1 == 'BA + 1'"); py_run!(py, c, "assert c - 1 == 'BA - 1'"); py_run!(py, c, "assert c * 1 == 'BA * 1'"); + py_run!(py, c, "assert c @ 1 == 'BA @ 1'"); + py_run!(py, c, "assert c / 1 == 'BA / 1'"); + py_run!(py, c, "assert c // 1 == 'BA // 1'"); + py_run!(py, c, "assert c % 1 == 'BA % 1'"); + py_run!(py, c, "assert divmod(c, 1) == 'divmod(BA, 1)'"); py_run!(py, c, "assert c << 1 == 'BA << 1'"); py_run!(py, c, "assert c >> 1 == 'BA >> 1'"); py_run!(py, c, "assert c & 1 == 'BA & 1'"); @@ -230,6 +265,11 @@ fn binary_arithmetic() { py_expect_exception!(py, c, "1 + c", PyTypeError); py_expect_exception!(py, c, "1 - c", PyTypeError); py_expect_exception!(py, c, "1 * c", PyTypeError); + py_expect_exception!(py, c, "1 @ c", PyTypeError); + py_expect_exception!(py, c, "1 / c", PyTypeError); + py_expect_exception!(py, c, "1 // c", PyTypeError); + py_expect_exception!(py, c, "1 % c", PyTypeError); + py_expect_exception!(py, c, "divmod(1, c)", PyTypeError); py_expect_exception!(py, c, "1 << c", PyTypeError); py_expect_exception!(py, c, "1 >> c", PyTypeError); py_expect_exception!(py, c, "1 & c", PyTypeError); @@ -243,7 +283,11 @@ fn binary_arithmetic() { assert_py_eq!(c.add(&c).unwrap(), "BA + BA"); assert_py_eq!(c.sub(&c).unwrap(), "BA - BA"); assert_py_eq!(c.mul(&c).unwrap(), "BA * BA"); + assert_py_eq!(c.matmul(&c).unwrap(), "BA @ BA"); assert_py_eq!(c.div(&c).unwrap(), "BA / BA"); + assert_py_eq!(c.floor_div(&c).unwrap(), "BA // BA"); + assert_py_eq!(c.rem(&c).unwrap(), "BA % BA"); + assert_py_eq!(c.divmod(&c).unwrap(), "divmod(BA, BA)"); assert_py_eq!(c.lshift(&c).unwrap(), "BA << BA"); assert_py_eq!(c.rshift(&c).unwrap(), "BA >> BA"); assert_py_eq!(c.bitand(&c).unwrap(), "BA & BA"); From b25b3b3a7b5e68fa7ccb4c1f093df49f33132206 Mon Sep 17 00:00:00 2001 From: Bruno Kolenbrander <59372212+mejrs@users.noreply.github.com> Date: Thu, 20 Jun 2024 11:23:40 +0200 Subject: [PATCH 118/495] Improve the span and message for return types of pymethod/functions (#4220) * Improve the span and message for return types of pymethod/functions * Don't pass the span * fixup trybuild output --------- Co-authored-by: David Hewitt --- pyo3-macros-backend/src/deprecations.rs | 2 +- pyo3-macros-backend/src/frompyobject.rs | 12 +++--- pyo3-macros-backend/src/konst.rs | 2 +- pyo3-macros-backend/src/method.rs | 31 ++++++++++---- pyo3-macros-backend/src/module.rs | 14 +++---- pyo3-macros-backend/src/params.rs | 10 ++--- pyo3-macros-backend/src/pyclass.rs | 50 +++++++++++------------ pyo3-macros-backend/src/pyfunction.rs | 4 +- pyo3-macros-backend/src/pyimpl.rs | 14 +++---- pyo3-macros-backend/src/pymethod.rs | 38 +++++++++-------- pyo3-macros-backend/src/quotes.rs | 20 ++++++--- pyo3-macros-backend/src/utils.rs | 28 ++++++++++--- src/conversion.rs | 10 ++++- src/impl_/wrap.rs | 9 ++++ tests/ui/invalid_result_conversion.stderr | 7 ++-- tests/ui/missing_intopy.stderr | 12 +++--- 16 files changed, 162 insertions(+), 101 deletions(-) diff --git a/pyo3-macros-backend/src/deprecations.rs b/pyo3-macros-backend/src/deprecations.rs index 4db40cc86f7..68375900c10 100644 --- a/pyo3-macros-backend/src/deprecations.rs +++ b/pyo3-macros-backend/src/deprecations.rs @@ -32,7 +32,7 @@ impl<'ctx> Deprecations<'ctx> { impl<'ctx> ToTokens for Deprecations<'ctx> { fn to_tokens(&self, tokens: &mut TokenStream) { - let Self(deprecations, Ctx { pyo3_path }) = self; + let Self(deprecations, Ctx { pyo3_path, .. }) = self; for (deprecation, span) in deprecations { let pyo3_path = pyo3_path.to_tokens_spanned(*span); diff --git a/pyo3-macros-backend/src/frompyobject.rs b/pyo3-macros-backend/src/frompyobject.rs index 7f26e5b14fc..d7767174e62 100644 --- a/pyo3-macros-backend/src/frompyobject.rs +++ b/pyo3-macros-backend/src/frompyobject.rs @@ -45,7 +45,7 @@ impl<'a> Enum<'a> { /// Build derivation body for enums. fn build(&self, ctx: &Ctx) -> (TokenStream, TokenStream) { - let Ctx { pyo3_path } = ctx; + let Ctx { pyo3_path, .. } = ctx; let mut var_extracts = Vec::new(); let mut variant_names = Vec::new(); let mut error_names = Vec::new(); @@ -263,7 +263,7 @@ impl<'a> Container<'a> { from_py_with: &Option, ctx: &Ctx, ) -> (TokenStream, TokenStream) { - let Ctx { pyo3_path } = ctx; + let Ctx { pyo3_path, .. } = ctx; let self_ty = &self.path; let struct_name = self.name(); if let Some(ident) = field_ident { @@ -329,7 +329,7 @@ impl<'a> Container<'a> { struct_fields: &[TupleStructField], ctx: &Ctx, ) -> (TokenStream, TokenStream) { - let Ctx { pyo3_path } = ctx; + let Ctx { pyo3_path, .. } = ctx; let self_ty = &self.path; let struct_name = &self.name(); let field_idents: Vec<_> = (0..struct_fields.len()) @@ -382,7 +382,7 @@ impl<'a> Container<'a> { struct_fields: &[NamedStructField<'_>], ctx: &Ctx, ) -> (TokenStream, TokenStream) { - let Ctx { pyo3_path } = ctx; + let Ctx { pyo3_path, .. } = ctx; let self_ty = &self.path; let struct_name = &self.name(); let mut fields: Punctuated = Punctuated::new(); @@ -670,8 +670,8 @@ pub fn build_derive_from_pyobject(tokens: &DeriveInput) -> Result { .push(parse_quote!(#gen_ident: FromPyObject<#lt_param>)) } let options = ContainerOptions::from_attrs(&tokens.attrs)?; - let ctx = &Ctx::new(&options.krate); - let Ctx { pyo3_path } = &ctx; + let ctx = &Ctx::new(&options.krate, None); + let Ctx { pyo3_path, .. } = &ctx; let (derives, from_py_with_deprecations) = match &tokens.data { syn::Data::Enum(en) => { diff --git a/pyo3-macros-backend/src/konst.rs b/pyo3-macros-backend/src/konst.rs index e7d8d554cae..2a7667dd5f2 100644 --- a/pyo3-macros-backend/src/konst.rs +++ b/pyo3-macros-backend/src/konst.rs @@ -30,7 +30,7 @@ impl ConstSpec<'_> { /// Null-terminated Python name pub fn null_terminated_python_name(&self, ctx: &Ctx) -> TokenStream { - let Ctx { pyo3_path } = ctx; + let Ctx { pyo3_path, .. } = ctx; let name = self.python_name().to_string(); quote!(#pyo3_path::ffi::c_str!(#name)) } diff --git a/pyo3-macros-backend/src/method.rs b/pyo3-macros-backend/src/method.rs index 745426371c3..294cd4db969 100644 --- a/pyo3-macros-backend/src/method.rs +++ b/pyo3-macros-backend/src/method.rs @@ -224,7 +224,7 @@ impl FnType { holders: &mut Holders, ctx: &Ctx, ) -> TokenStream { - let Ctx { pyo3_path } = ctx; + let Ctx { pyo3_path, .. } = ctx; match self { FnType::Getter(st) | FnType::Setter(st) | FnType::Fn(st) => { let mut receiver = st.receiver( @@ -281,7 +281,7 @@ pub enum ExtractErrorMode { impl ExtractErrorMode { pub fn handle_error(self, extract: TokenStream, ctx: &Ctx) -> TokenStream { - let Ctx { pyo3_path } = ctx; + let Ctx { pyo3_path, .. } = ctx; match self { ExtractErrorMode::Raise => quote! { #extract? }, ExtractErrorMode::NotImplemented => quote! { @@ -306,7 +306,7 @@ impl SelfType { // main macro callsite. let py = syn::Ident::new("py", Span::call_site()); let slf = syn::Ident::new("_slf", Span::call_site()); - let Ctx { pyo3_path } = ctx; + let Ctx { pyo3_path, .. } = ctx; match self { SelfType::Receiver { span, mutable } => { let method = if *mutable { @@ -473,7 +473,7 @@ impl<'a> FnSpec<'a> { } pub fn null_terminated_python_name(&self, ctx: &Ctx) -> TokenStream { - let Ctx { pyo3_path } = ctx; + let Ctx { pyo3_path, .. } = ctx; let span = self.python_name.span(); let pyo3_path = pyo3_path.to_tokens_spanned(span); let name = self.python_name.to_string(); @@ -600,7 +600,10 @@ impl<'a> FnSpec<'a> { cls: Option<&syn::Type>, ctx: &Ctx, ) -> Result { - let Ctx { pyo3_path } = ctx; + let Ctx { + pyo3_path, + output_span, + } = ctx; let mut cancel_handle_iter = self .signature .arguments @@ -703,7 +706,18 @@ impl<'a> FnSpec<'a> { } } }; - quotes::map_result_into_ptr(quotes::ok_wrap(call, ctx), ctx) + + // We must assign the output_span to the return value of the call, + // but *not* of the call itself otherwise the spans get really weird + let ret_expr = quote! { let ret = #call; }; + let ret_var = quote_spanned! {*output_span=> ret }; + let return_conversion = quotes::map_result_into_ptr(quotes::ok_wrap(ret_var, ctx), ctx); + quote! { + { + #ret_expr + #return_conversion + } + } }; let func_name = &self.name; @@ -731,7 +745,6 @@ impl<'a> FnSpec<'a> { let call = rust_call(args, &mut holders); let check_gil_refs = holders.check_gil_refs(); let init_holders = holders.init_holders(ctx); - quote! { unsafe fn #ident<'py>( py: #pyo3_path::Python<'py>, @@ -804,7 +817,7 @@ impl<'a> FnSpec<'a> { let self_arg = self .tp .self_arg(cls, ExtractErrorMode::Raise, &mut holders, ctx); - let call = quote! { #rust_name(#self_arg #(#args),*) }; + let call = quote_spanned! {*output_span=> #rust_name(#self_arg #(#args),*) }; let init_holders = holders.init_holders(ctx); let check_gil_refs = holders.check_gil_refs(); quote! { @@ -833,7 +846,7 @@ impl<'a> FnSpec<'a> { /// Return a `PyMethodDef` constructor for this function, matching the selected /// calling convention. pub fn get_methoddef(&self, wrapper: impl ToTokens, doc: &PythonDoc, ctx: &Ctx) -> TokenStream { - let Ctx { pyo3_path } = ctx; + let Ctx { pyo3_path, .. } = ctx; let python_name = self.null_terminated_python_name(ctx); match self.convention { CallingConvention::Noargs => quote! { diff --git a/pyo3-macros-backend/src/module.rs b/pyo3-macros-backend/src/module.rs index 0383046e0c4..3443507bb3a 100644 --- a/pyo3-macros-backend/src/module.rs +++ b/pyo3-macros-backend/src/module.rs @@ -90,8 +90,8 @@ pub fn pymodule_module_impl(mut module: syn::ItemMod) -> Result { bail_spanned!(module.span() => "`#[pymodule]` can only be used on inline modules") }; let options = PyModuleOptions::from_attrs(attrs)?; - let ctx = &Ctx::new(&options.krate); - let Ctx { pyo3_path } = ctx; + let ctx = &Ctx::new(&options.krate, None); + let Ctx { pyo3_path, .. } = ctx; let doc = get_doc(attrs, None, ctx); let name = options.name.unwrap_or_else(|| ident.unraw()); let full_name = if let Some(module) = &options.module { @@ -326,9 +326,9 @@ pub fn pymodule_module_impl(mut module: syn::ItemMod) -> Result { pub fn pymodule_function_impl(mut function: syn::ItemFn) -> Result { let options = PyModuleOptions::from_attrs(&mut function.attrs)?; process_functions_in_module(&options, &mut function)?; - let ctx = &Ctx::new(&options.krate); + let ctx = &Ctx::new(&options.krate, None); let stmts = std::mem::take(&mut function.block.stmts); - let Ctx { pyo3_path } = ctx; + let Ctx { pyo3_path, .. } = ctx; let ident = &function.sig.ident; let name = options.name.unwrap_or_else(|| ident.unraw()); let vis = &function.vis; @@ -400,7 +400,7 @@ pub fn pymodule_function_impl(mut function: syn::ItemFn) -> Result } fn module_initialization(name: &syn::Ident, ctx: &Ctx) -> TokenStream { - let Ctx { pyo3_path } = ctx; + let Ctx { pyo3_path, .. } = ctx; let pyinit_symbol = format!("PyInit_{}", name); let name = name.to_string(); @@ -424,8 +424,8 @@ fn module_initialization(name: &syn::Ident, ctx: &Ctx) -> TokenStream { /// Finds and takes care of the #[pyfn(...)] in `#[pymodule]` fn process_functions_in_module(options: &PyModuleOptions, func: &mut syn::ItemFn) -> Result<()> { - let ctx = &Ctx::new(&options.krate); - let Ctx { pyo3_path } = ctx; + let ctx = &Ctx::new(&options.krate, None); + let Ctx { pyo3_path, .. } = ctx; let mut stmts: Vec = Vec::new(); #[cfg(feature = "gil-refs")] diff --git a/pyo3-macros-backend/src/params.rs b/pyo3-macros-backend/src/params.rs index cab9d2a7d29..f7d71b923a6 100644 --- a/pyo3-macros-backend/src/params.rs +++ b/pyo3-macros-backend/src/params.rs @@ -48,7 +48,7 @@ impl Holders { } pub fn init_holders(&self, ctx: &Ctx) -> TokenStream { - let Ctx { pyo3_path } = ctx; + let Ctx { pyo3_path, .. } = ctx; let holders = &self.holders; let gil_refs_checkers = self.gil_refs_checkers.iter().map(|checker| match checker { GilRefChecker::FunctionArg(ident) => ident, @@ -94,7 +94,7 @@ pub(crate) fn check_arg_for_gil_refs( gil_refs_checker: syn::Ident, ctx: &Ctx, ) -> TokenStream { - let Ctx { pyo3_path } = ctx; + let Ctx { pyo3_path, .. } = ctx; quote! { #pyo3_path::impl_::deprecations::inspect_type(#tokens, &#gil_refs_checker) } @@ -108,7 +108,7 @@ pub fn impl_arg_params( ctx: &Ctx, ) -> (TokenStream, Vec) { let args_array = syn::Ident::new("output", Span::call_site()); - let Ctx { pyo3_path } = ctx; + let Ctx { pyo3_path, .. } = ctx; let from_py_with = spec .signature @@ -242,7 +242,7 @@ fn impl_arg_param( holders: &mut Holders, ctx: &Ctx, ) -> TokenStream { - let Ctx { pyo3_path } = ctx; + let Ctx { pyo3_path, .. } = ctx; let args_array = syn::Ident::new("output", Span::call_site()); match arg { @@ -290,7 +290,7 @@ pub(crate) fn impl_regular_arg_param( holders: &mut Holders, ctx: &Ctx, ) -> TokenStream { - let Ctx { pyo3_path } = ctx; + let Ctx { pyo3_path, .. } = ctx; let pyo3_path = pyo3_path.to_tokens_spanned(arg.ty.span()); // Use this macro inside this function, to ensure that all code generated here is associated diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index 3e40977e4af..85732ae55ff 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -227,7 +227,7 @@ pub fn build_py_class( ) -> syn::Result { args.options.take_pyo3_options(&mut class.attrs)?; - let ctx = &Ctx::new(&args.options.krate); + let ctx = &Ctx::new(&args.options.krate, None); let doc = utils::get_doc(&class.attrs, None, ctx); if let Some(lt) = class.generics.lifetimes().next() { @@ -383,7 +383,7 @@ fn impl_class( methods_type: PyClassMethodsType, ctx: &Ctx, ) -> syn::Result { - let Ctx { pyo3_path } = ctx; + let Ctx { pyo3_path, .. } = ctx; let pytypeinfo_impl = impl_pytypeinfo(cls, args, None, ctx); let (default_richcmp, default_richcmp_slot) = @@ -457,7 +457,7 @@ pub fn build_py_enum( ) -> syn::Result { args.options.take_pyo3_options(&mut enum_.attrs)?; - let ctx = &Ctx::new(&args.options.krate); + let ctx = &Ctx::new(&args.options.krate, None); if let Some(extends) = &args.options.extends { bail_spanned!(extends.span() => "enums can't extend from other classes"); } else if let Some(subclass) = &args.options.subclass { @@ -872,7 +872,7 @@ fn impl_complex_enum( methods_type: PyClassMethodsType, ctx: &Ctx, ) -> Result { - let Ctx { pyo3_path } = ctx; + let Ctx { pyo3_path, .. } = ctx; let cls = complex_enum.ident; let ty: syn::Type = syn::parse_quote!(#cls); @@ -886,7 +886,7 @@ fn impl_complex_enum( rigged_args }; - let ctx = &Ctx::new(&args.options.krate); + let ctx = &Ctx::new(&args.options.krate, None); let cls = complex_enum.ident; let variants = complex_enum.variants; let pytypeinfo = impl_pytypeinfo(cls, &args, None, ctx); @@ -1071,7 +1071,7 @@ fn impl_complex_enum_struct_variant_cls( variant: &PyClassEnumStructVariant<'_>, ctx: &Ctx, ) -> Result<(TokenStream, Vec, Vec)> { - let Ctx { pyo3_path } = ctx; + let Ctx { pyo3_path, .. } = ctx; let variant_ident = &variant.ident; let variant_cls = gen_complex_enum_variant_class_ident(enum_name, variant.ident); let variant_cls_type = parse_quote!(#variant_cls); @@ -1135,7 +1135,7 @@ fn impl_complex_enum_tuple_variant_field_getters( field_names: &mut Vec, fields_types: &mut Vec, ) -> Result<(Vec, Vec)> { - let Ctx { pyo3_path } = ctx; + let Ctx { pyo3_path, .. } = ctx; let mut field_getters = vec![]; let mut field_getter_impls = vec![]; @@ -1182,7 +1182,7 @@ fn impl_complex_enum_tuple_variant_len( variant_cls_type: &syn::Type, num_fields: usize, ) -> Result<(MethodAndSlotDef, syn::ImplItemFn)> { - let Ctx { pyo3_path } = ctx; + let Ctx { pyo3_path, .. } = ctx; let mut len_method_impl: syn::ImplItemFn = parse_quote! { fn __len__(slf: #pyo3_path::PyRef) -> #pyo3_path::PyResult { @@ -1202,7 +1202,7 @@ fn impl_complex_enum_tuple_variant_getitem( variant_cls_type: &syn::Type, num_fields: usize, ) -> Result<(MethodAndSlotDef, syn::ImplItemFn)> { - let Ctx { pyo3_path } = ctx; + let Ctx { pyo3_path, .. } = ctx; let match_arms: Vec<_> = (0..num_fields) .map(|i| { @@ -1243,7 +1243,7 @@ fn impl_complex_enum_tuple_variant_cls( variant: &PyClassEnumTupleVariant<'_>, ctx: &Ctx, ) -> Result<(TokenStream, Vec, Vec)> { - let Ctx { pyo3_path } = ctx; + let Ctx { pyo3_path, .. } = ctx; let variant_ident = &variant.ident; let variant_cls = gen_complex_enum_variant_class_ident(enum_name, variant.ident); let variant_cls_type = parse_quote!(#variant_cls); @@ -1400,7 +1400,7 @@ pub fn gen_complex_enum_variant_attr( spec: &ConstSpec<'_>, ctx: &Ctx, ) -> MethodAndMethodDef { - let Ctx { pyo3_path } = ctx; + let Ctx { pyo3_path, .. } = ctx; let member = &spec.rust_ident; let wrapper_ident = format_ident!("__pymethod_variant_cls_{}__", member); let deprecations = &spec.attributes.deprecations; @@ -1449,7 +1449,7 @@ fn complex_enum_struct_variant_new<'a>( variant: PyClassEnumStructVariant<'a>, ctx: &Ctx, ) -> Result { - let Ctx { pyo3_path } = ctx; + let Ctx { pyo3_path, .. } = ctx; let variant_cls = format_ident!("{}_{}", cls, variant.ident); let variant_cls_type: syn::Type = parse_quote!(#variant_cls); @@ -1506,7 +1506,7 @@ fn complex_enum_tuple_variant_new<'a>( variant: PyClassEnumTupleVariant<'a>, ctx: &Ctx, ) -> Result { - let Ctx { pyo3_path } = ctx; + let Ctx { pyo3_path, .. } = ctx; let variant_cls: Ident = format_ident!("{}_{}", cls, variant.ident); let variant_cls_type: syn::Type = parse_quote!(#variant_cls); @@ -1645,7 +1645,7 @@ fn impl_pytypeinfo( deprecations: Option<&Deprecations<'_>>, ctx: &Ctx, ) -> TokenStream { - let Ctx { pyo3_path } = ctx; + let Ctx { pyo3_path, .. } = ctx; let cls_name = get_class_python_name(cls, attr).to_string(); let module = if let Some(ModuleAttribute { value, .. }) = &attr.options.module { @@ -1689,7 +1689,7 @@ fn pyclass_richcmp_arms( options: &PyClassPyO3Options, ctx: &Ctx, ) -> std::result::Result { - let Ctx { pyo3_path } = ctx; + let Ctx { pyo3_path, .. } = ctx; let eq_arms = options .eq @@ -1743,7 +1743,7 @@ fn pyclass_richcmp_simple_enum( repr_type: &syn::Ident, ctx: &Ctx, ) -> Result<(Option, Option)> { - let Ctx { pyo3_path } = ctx; + let Ctx { pyo3_path, .. } = ctx; if let Some(eq_int) = options.eq_int { ensure_spanned!(options.eq.is_some(), eq_int.span() => "The `eq_int` option requires the `eq` option."); @@ -1827,7 +1827,7 @@ fn pyclass_richcmp( cls: &syn::Type, ctx: &Ctx, ) -> Result<(Option, Option)> { - let Ctx { pyo3_path } = ctx; + let Ctx { pyo3_path, .. } = ctx; if let Some(eq_int) = options.eq_int { bail_spanned!(eq_int.span() => "`eq_int` can only be used on simple enums.") } @@ -1940,7 +1940,7 @@ impl<'a> PyClassImplsBuilder<'a> { } fn impl_pyclass(&self, ctx: &Ctx) -> TokenStream { - let Ctx { pyo3_path } = ctx; + let Ctx { pyo3_path, .. } = ctx; let cls = self.cls; let frozen = if self.attr.options.frozen.is_some() { @@ -1956,7 +1956,7 @@ impl<'a> PyClassImplsBuilder<'a> { } } fn impl_extractext(&self, ctx: &Ctx) -> TokenStream { - let Ctx { pyo3_path } = ctx; + let Ctx { pyo3_path, .. } = ctx; let cls = self.cls; if self.attr.options.frozen.is_some() { quote! { @@ -1996,7 +1996,7 @@ impl<'a> PyClassImplsBuilder<'a> { } fn impl_into_py(&self, ctx: &Ctx) -> TokenStream { - let Ctx { pyo3_path } = ctx; + let Ctx { pyo3_path, .. } = ctx; let cls = self.cls; let attr = self.attr; // If #cls is not extended type, we allow Self->PyObject conversion @@ -2013,7 +2013,7 @@ impl<'a> PyClassImplsBuilder<'a> { } } fn impl_pyclassimpl(&self, ctx: &Ctx) -> Result { - let Ctx { pyo3_path } = ctx; + let Ctx { pyo3_path, .. } = ctx; let cls = self.cls; let doc = self.doc.as_ref().map_or( quote! {#pyo3_path::ffi::c_str!("")}, @@ -2184,7 +2184,7 @@ impl<'a> PyClassImplsBuilder<'a> { } fn impl_add_to_module(&self, ctx: &Ctx) -> TokenStream { - let Ctx { pyo3_path } = ctx; + let Ctx { pyo3_path, .. } = ctx; let cls = self.cls; quote! { impl #cls { @@ -2196,7 +2196,7 @@ impl<'a> PyClassImplsBuilder<'a> { fn impl_freelist(&self, ctx: &Ctx) -> TokenStream { let cls = self.cls; - let Ctx { pyo3_path } = ctx; + let Ctx { pyo3_path, .. } = ctx; self.attr.options.freelist.as_ref().map_or(quote!{}, |freelist| { let freelist = &freelist.value; @@ -2219,7 +2219,7 @@ impl<'a> PyClassImplsBuilder<'a> { } fn freelist_slots(&self, ctx: &Ctx) -> Vec { - let Ctx { pyo3_path } = ctx; + let Ctx { pyo3_path, .. } = ctx; let cls = self.cls; if self.attr.options.freelist.is_some() { @@ -2244,7 +2244,7 @@ impl<'a> PyClassImplsBuilder<'a> { } fn define_inventory_class(inventory_class_name: &syn::Ident, ctx: &Ctx) -> TokenStream { - let Ctx { pyo3_path } = ctx; + let Ctx { pyo3_path, .. } = ctx; quote! { #[doc(hidden)] pub struct #inventory_class_name { diff --git a/pyo3-macros-backend/src/pyfunction.rs b/pyo3-macros-backend/src/pyfunction.rs index 147193d18dc..25f0d5b37ae 100644 --- a/pyo3-macros-backend/src/pyfunction.rs +++ b/pyo3-macros-backend/src/pyfunction.rs @@ -205,8 +205,8 @@ pub fn impl_wrap_pyfunction( krate, } = options; - let ctx = &Ctx::new(&krate); - let Ctx { pyo3_path } = &ctx; + let ctx = &Ctx::new(&krate, Some(&func.sig)); + let Ctx { pyo3_path, .. } = &ctx; let python_name = name .as_ref() diff --git a/pyo3-macros-backend/src/pyimpl.rs b/pyo3-macros-backend/src/pyimpl.rs index a1242d49f10..dbb1fba894c 100644 --- a/pyo3-macros-backend/src/pyimpl.rs +++ b/pyo3-macros-backend/src/pyimpl.rs @@ -90,7 +90,6 @@ pub fn impl_methods( methods_type: PyClassMethodsType, options: PyImplOptions, ) -> syn::Result { - let ctx = &Ctx::new(&options.krate); let mut trait_impls = Vec::new(); let mut proto_impls = Vec::new(); let mut methods = Vec::new(); @@ -101,6 +100,7 @@ pub fn impl_methods( for iimpl in impls { match iimpl { syn::ImplItem::Fn(meth) => { + let ctx = &Ctx::new(&options.krate, Some(&meth.sig)); let mut fun_options = PyFunctionOptions::from_attrs(&mut meth.attrs)?; fun_options.krate = fun_options.krate.or_else(|| options.krate.clone()); match pymethod::gen_py_method(ty, &mut meth.sig, &mut meth.attrs, fun_options, ctx)? @@ -129,6 +129,7 @@ pub fn impl_methods( } } syn::ImplItem::Const(konst) => { + let ctx = &Ctx::new(&options.krate, None); let attributes = ConstAttributes::from_attrs(&mut konst.attrs, ctx)?; if attributes.is_class_attr { let spec = ConstSpec { @@ -159,11 +160,10 @@ pub fn impl_methods( _ => {} } } + let ctx = &Ctx::new(&options.krate, None); add_shared_proto_slots(ty, &mut proto_impls, implemented_proto_fragments, ctx); - let ctx = &Ctx::new(&options.krate); - let items = match methods_type { PyClassMethodsType::Specialization => impl_py_methods(ty, methods, proto_impls, ctx), PyClassMethodsType::Inventory => submit_methods_inventory(ty, methods, proto_impls, ctx), @@ -187,7 +187,7 @@ pub fn gen_py_const(cls: &syn::Type, spec: &ConstSpec<'_>, ctx: &Ctx) -> MethodA let wrapper_ident = format_ident!("__pymethod_{}__", member); let deprecations = &spec.attributes.deprecations; let python_name = spec.null_terminated_python_name(ctx); - let Ctx { pyo3_path } = ctx; + let Ctx { pyo3_path, .. } = ctx; let associated_method = quote! { fn #wrapper_ident(py: #pyo3_path::Python<'_>) -> #pyo3_path::PyResult<#pyo3_path::PyObject> { @@ -217,7 +217,7 @@ fn impl_py_methods( proto_impls: Vec, ctx: &Ctx, ) -> TokenStream { - let Ctx { pyo3_path } = ctx; + let Ctx { pyo3_path, .. } = ctx; quote! { #[allow(unknown_lints, non_local_definitions)] impl #pyo3_path::impl_::pyclass::PyMethods<#ty> @@ -240,7 +240,7 @@ fn add_shared_proto_slots( mut implemented_proto_fragments: HashSet, ctx: &Ctx, ) { - let Ctx { pyo3_path } = ctx; + let Ctx { pyo3_path, .. } = ctx; macro_rules! try_add_shared_slot { ($slot:ident, $($fragments:literal),*) => {{ let mut implemented = false; @@ -298,7 +298,7 @@ fn submit_methods_inventory( proto_impls: Vec, ctx: &Ctx, ) -> TokenStream { - let Ctx { pyo3_path } = ctx; + let Ctx { pyo3_path, .. } = ctx; quote! { #pyo3_path::inventory::submit! { type Inventory = <#ty as #pyo3_path::impl_::pyclass::PyClassImpl>::Inventory; diff --git a/pyo3-macros-backend/src/pymethod.rs b/pyo3-macros-backend/src/pymethod.rs index 735b55a169d..059475248eb 100644 --- a/pyo3-macros-backend/src/pymethod.rs +++ b/pyo3-macros-backend/src/pymethod.rs @@ -196,7 +196,7 @@ pub fn gen_py_method( ensure_function_options_valid(&options)?; let method = PyMethod::parse(sig, meth_attrs, options, ctx)?; let spec = &method.spec; - let Ctx { pyo3_path } = ctx; + let Ctx { pyo3_path, .. } = ctx; Ok(match (method.kind, &spec.tp) { // Class attributes go before protos so that class attributes can be used to set proto @@ -318,7 +318,7 @@ pub fn impl_py_method_def( flags: Option, ctx: &Ctx, ) -> Result { - let Ctx { pyo3_path } = ctx; + let Ctx { pyo3_path, .. } = ctx; let wrapper_ident = format_ident!("__pymethod_{}__", spec.python_name); let associated_method = spec.get_wrapper_function(&wrapper_ident, Some(cls), ctx)?; let add_flags = flags.map(|flags| quote!(.flags(#flags))); @@ -343,7 +343,7 @@ pub fn impl_py_method_def_new( spec: &FnSpec<'_>, ctx: &Ctx, ) -> Result { - let Ctx { pyo3_path } = ctx; + let Ctx { pyo3_path, .. } = ctx; let wrapper_ident = syn::Ident::new("__pymethod___new____", Span::call_site()); let associated_method = spec.get_wrapper_function(&wrapper_ident, Some(cls), ctx)?; // Use just the text_signature_call_signature() because the class' Python name @@ -393,7 +393,7 @@ pub fn impl_py_method_def_new( } fn impl_call_slot(cls: &syn::Type, mut spec: FnSpec<'_>, ctx: &Ctx) -> Result { - let Ctx { pyo3_path } = ctx; + let Ctx { pyo3_path, .. } = ctx; // HACK: __call__ proto slot must always use varargs calling convention, so change the spec. // Probably indicates there's a refactoring opportunity somewhere. @@ -433,7 +433,7 @@ fn impl_traverse_slot( spec: &FnSpec<'_>, ctx: &Ctx, ) -> syn::Result { - let Ctx { pyo3_path } = ctx; + let Ctx { pyo3_path, .. } = ctx; if let (Some(py_arg), _) = split_off_python_arg(&spec.signature.arguments) { return Err(syn::Error::new_spanned(py_arg.ty, "__traverse__ may not take `Python`. \ Usually, an implementation of `__traverse__(&self, visit: PyVisit<'_>) -> Result<(), PyTraverseError>` \ @@ -484,7 +484,7 @@ fn impl_py_class_attribute( spec: &FnSpec<'_>, ctx: &Ctx, ) -> syn::Result { - let Ctx { pyo3_path } = ctx; + let Ctx { pyo3_path, .. } = ctx; let (py_arg, args) = split_off_python_arg(&spec.signature.arguments); ensure_spanned!( args.is_empty(), @@ -559,7 +559,7 @@ pub fn impl_py_setter_def( property_type: PropertyType<'_>, ctx: &Ctx, ) -> Result { - let Ctx { pyo3_path } = ctx; + let Ctx { pyo3_path, .. } = ctx; let python_name = property_type.null_terminated_python_name(ctx)?; let doc = property_type.doc(ctx); let mut holders = Holders::new(); @@ -745,7 +745,7 @@ pub fn impl_py_getter_def( property_type: PropertyType<'_>, ctx: &Ctx, ) -> Result { - let Ctx { pyo3_path } = ctx; + let Ctx { pyo3_path, .. } = ctx; let python_name = property_type.null_terminated_python_name(ctx)?; let doc = property_type.doc(ctx); @@ -871,7 +871,7 @@ pub enum PropertyType<'a> { impl PropertyType<'_> { fn null_terminated_python_name(&self, ctx: &Ctx) -> Result { - let Ctx { pyo3_path } = ctx; + let Ctx { pyo3_path, .. } = ctx; match self { PropertyType::Descriptor { field, @@ -913,7 +913,7 @@ pub const __REPR__: SlotDef = SlotDef::new("Py_tp_repr", "reprfunc"); pub const __HASH__: SlotDef = SlotDef::new("Py_tp_hash", "hashfunc") .ret_ty(Ty::PyHashT) .return_conversion(TokenGenerator( - |Ctx { pyo3_path }: &Ctx| quote! { #pyo3_path::callback::HashCallbackOutput }, + |Ctx { pyo3_path, .. }: &Ctx| quote! { #pyo3_path::callback::HashCallbackOutput }, )); pub const __RICHCMP__: SlotDef = SlotDef::new("Py_tp_richcompare", "richcmpfunc") .extract_error_mode(ExtractErrorMode::NotImplemented) @@ -1036,7 +1036,11 @@ enum Ty { impl Ty { fn ffi_type(self, ctx: &Ctx) -> TokenStream { - let Ctx { pyo3_path } = ctx; + let Ctx { + pyo3_path, + output_span, + } = ctx; + let pyo3_path = pyo3_path.to_tokens_spanned(*output_span); match self { Ty::Object | Ty::MaybeNullObject => quote! { *mut #pyo3_path::ffi::PyObject }, Ty::NonNullObject => quote! { ::std::ptr::NonNull<#pyo3_path::ffi::PyObject> }, @@ -1057,7 +1061,7 @@ impl Ty { holders: &mut Holders, ctx: &Ctx, ) -> TokenStream { - let Ctx { pyo3_path } = ctx; + let Ctx { pyo3_path, .. } = ctx; match self { Ty::Object => extract_object( extract_error_mode, @@ -1122,7 +1126,7 @@ fn extract_object( source_ptr: TokenStream, ctx: &Ctx, ) -> TokenStream { - let Ctx { pyo3_path } = ctx; + let Ctx { pyo3_path, .. } = ctx; let gil_refs_checker = holders.push_gil_refs_checker(arg.ty().span()); let name = arg.name().unraw().to_string(); @@ -1162,7 +1166,7 @@ enum ReturnMode { impl ReturnMode { fn return_call_output(&self, call: TokenStream, ctx: &Ctx, holders: &Holders) -> TokenStream { - let Ctx { pyo3_path } = ctx; + let Ctx { pyo3_path, .. } = ctx; let check_gil_refs = holders.check_gil_refs(); match self { ReturnMode::Conversion(conversion) => { @@ -1265,7 +1269,7 @@ impl SlotDef { method_name: &str, ctx: &Ctx, ) -> Result { - let Ctx { pyo3_path } = ctx; + let Ctx { pyo3_path, .. } = ctx; let SlotDef { slot, func_ty, @@ -1345,7 +1349,7 @@ fn generate_method_body( return_mode: Option<&ReturnMode>, ctx: &Ctx, ) -> Result { - let Ctx { pyo3_path } = ctx; + let Ctx { pyo3_path, .. } = ctx; let self_arg = spec .tp .self_arg(Some(cls), extract_error_mode, holders, ctx); @@ -1397,7 +1401,7 @@ impl SlotFragmentDef { spec: &FnSpec<'_>, ctx: &Ctx, ) -> Result { - let Ctx { pyo3_path } = ctx; + let Ctx { pyo3_path, .. } = ctx; let SlotFragmentDef { fragment, arguments, diff --git a/pyo3-macros-backend/src/quotes.rs b/pyo3-macros-backend/src/quotes.rs index ceef23fb034..6f6f64dad20 100644 --- a/pyo3-macros-backend/src/quotes.rs +++ b/pyo3-macros-backend/src/quotes.rs @@ -1,23 +1,31 @@ use crate::utils::Ctx; use proc_macro2::TokenStream; -use quote::quote; +use quote::{quote, quote_spanned}; pub(crate) fn some_wrap(obj: TokenStream, ctx: &Ctx) -> TokenStream { - let Ctx { pyo3_path } = ctx; + let Ctx { pyo3_path, .. } = ctx; quote! { #pyo3_path::impl_::wrap::SomeWrap::wrap(#obj) } } pub(crate) fn ok_wrap(obj: TokenStream, ctx: &Ctx) -> TokenStream { - let Ctx { pyo3_path } = ctx; - quote! { + let Ctx { + pyo3_path, + output_span, + } = ctx; + let pyo3_path = pyo3_path.to_tokens_spanned(*output_span); + quote_spanned! {*output_span=> #pyo3_path::impl_::wrap::OkWrap::wrap(#obj) .map_err(::core::convert::Into::<#pyo3_path::PyErr>::into) } } pub(crate) fn map_result_into_ptr(result: TokenStream, ctx: &Ctx) -> TokenStream { - let Ctx { pyo3_path } = ctx; - quote! { #pyo3_path::impl_::wrap::map_result_into_ptr(py, #result) } + let Ctx { + pyo3_path, + output_span, + } = ctx; + let pyo3_path = pyo3_path.to_tokens_spanned(*output_span); + quote_spanned! {*output_span=> #pyo3_path::impl_::wrap::map_result_into_ptr(py, #result) } } diff --git a/pyo3-macros-backend/src/utils.rs b/pyo3-macros-backend/src/utils.rs index 1586379ad10..22b16010480 100644 --- a/pyo3-macros-backend/src/utils.rs +++ b/pyo3-macros-backend/src/utils.rs @@ -1,9 +1,9 @@ +use crate::attributes::{CrateAttribute, RenamingRule}; use proc_macro2::{Span, TokenStream}; use quote::{quote, ToTokens}; +use syn::spanned::Spanned; use syn::{punctuated::Punctuated, Token}; -use crate::attributes::{CrateAttribute, RenamingRule}; - /// Macro inspired by `anyhow::anyhow!` to create a compiler error with the given span. macro_rules! err_spanned { ($span:expr => $msg:expr) => { @@ -86,7 +86,7 @@ pub fn get_doc( mut text_signature: Option, ctx: &Ctx, ) -> PythonDoc { - let Ctx { pyo3_path } = ctx; + let Ctx { pyo3_path, .. } = ctx; // insert special divider between `__text_signature__` and doc // (assume text_signature is itself well-formed) if let Some(text_signature) = &mut text_signature { @@ -162,17 +162,35 @@ pub fn unwrap_ty_group(mut ty: &syn::Type) -> &syn::Type { } pub struct Ctx { + /// Where we can find the pyo3 crate pub pyo3_path: PyO3CratePath, + + /// If we are in a pymethod or pyfunction, + /// this will be the span of the return type + pub output_span: Span, } impl Ctx { - pub(crate) fn new(attr: &Option) -> Self { + pub(crate) fn new(attr: &Option, signature: Option<&syn::Signature>) -> Self { let pyo3_path = match attr { Some(attr) => PyO3CratePath::Given(attr.value.0.clone()), None => PyO3CratePath::Default, }; - Self { pyo3_path } + let output_span = if let Some(syn::Signature { + output: syn::ReturnType::Type(_, output_type), + .. + }) = &signature + { + output_type.span() + } else { + Span::call_site() + }; + + Self { + pyo3_path, + output_span, + } } } diff --git a/src/conversion.rs b/src/conversion.rs index 44dbc3c7eed..6a089e186bc 100644 --- a/src/conversion.rs +++ b/src/conversion.rs @@ -152,7 +152,15 @@ pub trait ToPyObject { /// # } /// ``` /// Python code will see this as any of the `int`, `string` or `None` objects. -#[doc(alias = "IntoPyCallbackOutput")] +#[cfg_attr( + diagnostic_namespace, + diagnostic::on_unimplemented( + message = "`{Self}` cannot be converted to a Python object", + note = "`IntoPy` is automatically implemented by the `#[pyclass]` macro", + note = "if you do not wish to have a corresponding Python type, implement it manually", + note = "if you do not own `{Self}` you can perform a manual conversion to one of the types in `pyo3::types::*`" + ) +)] pub trait IntoPy: Sized { /// Performs the conversion. fn into_py(self, py: Python<'_>) -> T; diff --git a/src/impl_/wrap.rs b/src/impl_/wrap.rs index 2110d8411d0..b6a6a4a804b 100644 --- a/src/impl_/wrap.rs +++ b/src/impl_/wrap.rs @@ -20,6 +20,15 @@ impl SomeWrap for Option { } /// Used to wrap the result of `#[pyfunction]` and `#[pymethods]`. +#[cfg_attr( + diagnostic_namespace, + diagnostic::on_unimplemented( + message = "`{Self}` cannot be converted to a Python object", + note = "`IntoPy` is automatically implemented by the `#[pyclass]` macro", + note = "if you do not wish to have a corresponding Python type, implement `IntoPy` manually", + note = "if you do not own `{Self}` you can perform a manual conversion to one of the types in `pyo3::types::*`" + ) +)] pub trait OkWrap { type Error; fn wrap(self) -> Result; diff --git a/tests/ui/invalid_result_conversion.stderr b/tests/ui/invalid_result_conversion.stderr index a18cd6c7b30..b34c6396f00 100644 --- a/tests/ui/invalid_result_conversion.stderr +++ b/tests/ui/invalid_result_conversion.stderr @@ -1,8 +1,8 @@ error[E0277]: the trait bound `PyErr: From` is not satisfied - --> tests/ui/invalid_result_conversion.rs:21:1 + --> tests/ui/invalid_result_conversion.rs:22:25 | -21 | #[pyfunction] - | ^^^^^^^^^^^^^ the trait `From` is not implemented for `PyErr`, which is required by `MyError: Into` +22 | fn should_not_work() -> Result<(), MyError> { + | ^^^^^^ the trait `From` is not implemented for `PyErr`, which is required by `MyError: Into` | = help: the following other types implement trait `From`: > @@ -15,4 +15,3 @@ error[E0277]: the trait bound `PyErr: From` is not satisfied >> and $N others = note: required for `MyError` to implement `Into` - = note: this error originates in the attribute macro `pyfunction` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/tests/ui/missing_intopy.stderr b/tests/ui/missing_intopy.stderr index c0a60143671..e781b38fc86 100644 --- a/tests/ui/missing_intopy.stderr +++ b/tests/ui/missing_intopy.stderr @@ -1,9 +1,11 @@ -error[E0277]: the trait bound `Blah: OkWrap` is not satisfied - --> tests/ui/missing_intopy.rs:3:1 +error[E0277]: `Blah` cannot be converted to a Python object + --> tests/ui/missing_intopy.rs:4:14 | -3 | #[pyo3::pyfunction] - | ^^^^^^^^^^^^^^^^^^^ the trait `IntoPy>` is not implemented for `Blah`, which is required by `Blah: OkWrap<_>` +4 | fn blah() -> Blah{ + | ^^^^ the trait `IntoPy>` is not implemented for `Blah`, which is required by `Blah: OkWrap<_>` | + = note: `IntoPy` is automatically implemented by the `#[pyclass]` macro + = note: if you do not wish to have a corresponding Python type, implement `IntoPy` manually + = note: if you do not own `Blah` you can perform a manual conversion to one of the types in `pyo3::types::*` = help: the trait `OkWrap` is implemented for `Result` = note: required for `Blah` to implement `OkWrap` - = note: this error originates in the attribute macro `pyo3::pyfunction` (in Nightly builds, run with -Z macro-backtrace for more info) From 30add032b54001df2083ccac9e1d5ef6a6e5b9ac Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Thu, 20 Jun 2024 22:41:16 +0100 Subject: [PATCH 119/495] improve code generated by `c_str!` macro (#4270) * improve code generated by `c_str!` macro * fix clippy --- pyo3-ffi/src/lib.rs | 38 ++++++++++++++++++-------------------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/pyo3-ffi/src/lib.rs b/pyo3-ffi/src/lib.rs index c3f5225e87d..3f6d6732bf3 100644 --- a/pyo3-ffi/src/lib.rs +++ b/pyo3-ffi/src/lib.rs @@ -264,35 +264,33 @@ macro_rules! opaque_struct { /// ``` #[macro_export] macro_rules! c_str { - ($s:expr) => {{ - const _: () = { - assert!( - $crate::str_contains_no_nul($s), - "string contains null bytes" - ); - }; - // SAFETY: the string is checked to not contain null bytes - #[allow(unsafe_op_in_unsafe_fn, unused_unsafe)] // MSRV 1.63 needs these allows - unsafe { - ::std::ffi::CStr::from_bytes_with_nul_unchecked(concat!($s, "\0").as_bytes()) - } - }}; + ($s:expr) => { + $crate::_cstr_from_utf8_with_nul_checked(concat!($s, "\0")) + }; } +/// Private helper for `c_str!` macro. #[doc(hidden)] -pub const fn str_contains_no_nul(s: &str) -> bool { +pub const fn _cstr_from_utf8_with_nul_checked(s: &str) -> &CStr { + // TODO: Replace this implementation with `CStr::from_bytes_with_nul` when MSRV above 1.72. let bytes = s.as_bytes(); - let len = s.len(); + let len = bytes.len(); + assert!( + !bytes.is_empty() && bytes[bytes.len() - 1] == b'\0', + "string is not nul-terminated" + ); let mut i = 0; - while i < len { - if bytes[i] == 0 { - return false; - } + let non_null_len = len - 1; + while i < non_null_len { + assert!(bytes[i] != b'\0', "string contains null bytes"); i += 1; } - true + + unsafe { CStr::from_bytes_with_nul_unchecked(bytes) } } +use std::ffi::CStr; + pub use self::abstract_::*; pub use self::bltinmodule::*; pub use self::boolobject::*; From a983b2fe7b4dbc22727ce1f5c22fd985af6bc1d1 Mon Sep 17 00:00:00 2001 From: Bruno Kolenbrander <59372212+mejrs@users.noreply.github.com> Date: Thu, 20 Jun 2024 23:56:17 +0200 Subject: [PATCH 120/495] Bump diagnostic_namespace rust version (#4268) --- pyo3-build-config/src/lib.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pyo3-build-config/src/lib.rs b/pyo3-build-config/src/lib.rs index 2b5e76e4b95..21e057d8166 100644 --- a/pyo3-build-config/src/lib.rs +++ b/pyo3-build-config/src/lib.rs @@ -141,7 +141,9 @@ pub fn print_feature_cfgs() { println!("cargo:rustc-cfg=invalid_from_utf8_lint"); } - if rustc_minor_version >= 78 { + // Actually this is available on 1.78, but we should avoid + // https://github.com/rust-lang/rust/issues/124651 just in case + if rustc_minor_version >= 79 { println!("cargo:rustc-cfg=diagnostic_namespace"); } } From ca82681615f01bc2d673639f02f93859e98f7686 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Fri, 21 Jun 2024 08:02:31 +0100 Subject: [PATCH 121/495] ci: minor cleanups following 1.63 MSRV (#4239) * ci: minor cleanups following 1.63 MSRV * correct `invalid_pymethods_duplicates` UI test * fix `nightly` feature --- .github/workflows/build.yml | 22 +++-- .github/workflows/ci.yml | 10 -- Cargo.toml | 2 +- guide/src/features.md | 4 +- noxfile.py | 7 +- pyo3-ffi/src/methodobject.rs | 3 +- src/conversions/chrono.rs | 7 -- src/conversions/std/array.rs | 2 +- src/marker.rs | 1 + tests/ui/invalid_pymethods_duplicates.rs | 2 + tests/ui/invalid_pymethods_duplicates.stderr | 97 ++------------------ 11 files changed, 31 insertions(+), 126 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 40d4a0012fd..e952592da3a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -16,9 +16,6 @@ on: rust-target: required: true type: string - extra-features: - required: true - type: string jobs: build: @@ -62,6 +59,10 @@ jobs: name: Ignore changed error messages when using trybuild run: echo "TRYBUILD=overwrite" >> "$GITHUB_ENV" + - if: inputs.rust == 'nightly' + name: Prepare to test on nightly rust + run: echo "MAYBE_NIGHTLY=nightly" >> "$GITHUB_ENV" + - name: Build docs run: nox -s docs @@ -88,26 +89,31 @@ jobs: - name: Build (all additive features) if: ${{ !startsWith(inputs.python-version, 'graalpy') }} - run: cargo build --lib --tests --no-default-features --features "full ${{ inputs.extra-features }}" + run: cargo build --lib --tests --no-default-features --features "multiple-pymethods full $MAYBE_NIGHTLY" - if: ${{ startsWith(inputs.python-version, 'pypy') }} name: Build PyPy (abi3-py37) - run: cargo build --lib --tests --no-default-features --features "abi3-py37 full ${{ inputs.extra-features }}" + run: cargo build --lib --tests --no-default-features --features "multiple-pymethods abi3-py37 full $MAYBE_NIGHTLY" # Run tests (except on PyPy, because no embedding API). - if: ${{ !startsWith(inputs.python-version, 'pypy') && !startsWith(inputs.python-version, 'graalpy') }} name: Test - run: cargo test --no-default-features --features "full ${{ inputs.extra-features }}" + run: cargo test --no-default-features --features "full $MAYBE_NIGHTLY" + + # Repeat, with multiple-pymethods feature enabled (it's not entirely additive) + - if: ${{ !startsWith(inputs.python-version, 'pypy') && !startsWith(inputs.python-version, 'graalpy') }} + name: Test + run: cargo test --no-default-features --features "multiple-pymethods full $MAYBE_NIGHTLY" # Run tests again, but in abi3 mode - if: ${{ !startsWith(inputs.python-version, 'pypy') && !startsWith(inputs.python-version, 'graalpy') }} name: Test (abi3) - run: cargo test --no-default-features --features "abi3 full ${{ inputs.extra-features }}" + run: cargo test --no-default-features --features "multiple-pymethods abi3 full $MAYBE_NIGHTLY" # Run tests again, for abi3-py37 (the minimal Python version) - if: ${{ (!startsWith(inputs.python-version, 'pypy') && !startsWith(inputs.python-version, 'graalpy')) && (inputs.python-version != '3.7') }} name: Test (abi3-py37) - run: cargo test --no-default-features --features "abi3-py37 full ${{ inputs.extra-features }}" + run: cargo test --no-default-features --features "multiple-pymethods abi3-py37 full $MAYBE_NIGHTLY" - name: Test proc-macro code run: cargo test --manifest-path=pyo3-macros-backend/Cargo.toml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5bf2b7ea1e8..8379232b7fb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -149,13 +149,11 @@ jobs: python-architecture: ${{ matrix.platform.python-architecture }} rust: ${{ matrix.rust }} rust-target: ${{ matrix.platform.rust-target }} - extra-features: ${{ matrix.platform.extra-features }} secrets: inherit strategy: # If one platform fails, allow the rest to keep testing if `CI-no-fail-fast` label is present fail-fast: ${{ !contains(github.event.pull_request.labels.*.name, 'CI-no-fail-fast') }} matrix: - extra-features: ["multiple-pymethods"] rust: [stable] python-version: ["3.12"] platform: @@ -197,7 +195,6 @@ jobs: python-architecture: "x64", rust-target: "x86_64-unknown-linux-gnu", } - extra-features: "nightly multiple-pymethods" build-full: if: ${{ contains(github.event.pull_request.labels.*.name, 'CI-build-full') || github.event_name != 'pull_request' }} name: python${{ matrix.python-version }}-${{ matrix.platform.python-architecture }} ${{ matrix.platform.os }} rust-${{ matrix.rust }} @@ -209,13 +206,11 @@ jobs: python-architecture: ${{ matrix.platform.python-architecture }} rust: ${{ matrix.rust }} rust-target: ${{ matrix.platform.rust-target }} - extra-features: ${{ matrix.platform.extra-features }} secrets: inherit strategy: # If one platform fails, allow the rest to keep testing if `CI-no-fail-fast` label is present fail-fast: ${{ !contains(github.event.pull_request.labels.*.name, 'CI-no-fail-fast') }} matrix: - extra-features: ["multiple-pymethods"] # Because MSRV doesn't support this rust: [stable] python-version: [ "3.7", @@ -264,7 +259,6 @@ jobs: python-architecture: "x64", rust-target: "x86_64-unknown-linux-gnu", } - extra-features: "" # Test the `nightly` feature - rust: nightly @@ -275,7 +269,6 @@ jobs: python-architecture: "x64", rust-target: "x86_64-unknown-linux-gnu", } - extra-features: "nightly multiple-pymethods" # Run rust beta to help catch toolchain regressions - rust: beta @@ -286,7 +279,6 @@ jobs: python-architecture: "x64", rust-target: "x86_64-unknown-linux-gnu", } - extra-features: "multiple-pymethods" # Test 32-bit Windows only with the latest Python version - rust: stable @@ -297,7 +289,6 @@ jobs: python-architecture: "x86", rust-target: "i686-pc-windows-msvc", } - extra-features: "multiple-pymethods" # test arm macos runner with the latest Python version # NB: if the full matrix switchess to arm, switch to x86_64 here @@ -309,7 +300,6 @@ jobs: python-architecture: "arm64", rust-target: "aarch64-apple-darwin", } - extra-features: "multiple-pymethods" valgrind: if: ${{ contains(github.event.pull_request.labels.*.name, 'CI-build-full') || github.event_name != 'pull_request' }} diff --git a/Cargo.toml b/Cargo.toml index d46b742b61d..dac3314a850 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -116,7 +116,7 @@ nightly = [] # This is mostly intended for testing purposes - activating *all* of these isn't particularly useful. full = [ "macros", - # "multiple-pymethods", # TODO re-add this when MSRV is greater than 1.62 + # "multiple-pymethods", # Not supported by wasm "anyhow", "chrono", "chrono-tz", diff --git a/guide/src/features.md b/guide/src/features.md index 0536b456a33..d801e2dd1e4 100644 --- a/guide/src/features.md +++ b/guide/src/features.md @@ -95,9 +95,9 @@ These macros require a number of dependencies which may not be needed by users w ### `multiple-pymethods` -This feature enables a dependency on `inventory`, which enables each `#[pyclass]` to have more than one `#[pymethods]` block. This feature also requires a minimum Rust version of 1.62 due to limitations in the `inventory` crate. +This feature enables each `#[pyclass]` to have more than one `#[pymethods]` block. -Most users should only need a single `#[pymethods]` per `#[pyclass]`. In addition, not all platforms (e.g. Wasm) are supported by `inventory`. For this reason this feature is not enabled by default, meaning fewer dependencies and faster compilation for the majority of users. +Most users should only need a single `#[pymethods]` per `#[pyclass]`. In addition, not all platforms (e.g. Wasm) are supported by `inventory`, which is used in the implementation of the feature. For this reason this feature is not enabled by default, meaning fewer dependencies and faster compilation for the majority of users. See [the `#[pyclass]` implementation details](class.md#implementation-details) for more information. diff --git a/noxfile.py b/noxfile.py index 2383e2f865f..96bd587bee8 100644 --- a/noxfile.py +++ b/noxfile.py @@ -663,7 +663,7 @@ def check_feature_powerset(session: nox.Session): "default", "auto-initialize", "generate-import-lib", - "multiple-pymethods", # TODO add this after MSRV 1.62 + "multiple-pymethods", # Because it's not supported on wasm } features = cargo_toml["features"] @@ -764,10 +764,9 @@ def _get_rust_default_target() -> str: @lru_cache() def _get_feature_sets() -> Tuple[Tuple[str, ...], ...]: """Returns feature sets to use for clippy job""" - rust_version = _get_rust_version() cargo_target = os.getenv("CARGO_BUILD_TARGET", "") - if rust_version[:2] >= (1, 62) and "wasm32-wasi" not in cargo_target: - # multiple-pymethods feature not supported before 1.62 or on WASI + if "wasm32-wasi" not in cargo_target: + # multiple-pymethods not supported on wasm return ( ("--no-default-features",), ( diff --git a/pyo3-ffi/src/methodobject.rs b/pyo3-ffi/src/methodobject.rs index 3ed6b770e54..74f7840ef58 100644 --- a/pyo3-ffi/src/methodobject.rs +++ b/pyo3-ffi/src/methodobject.rs @@ -186,9 +186,8 @@ impl std::fmt::Pointer for PyMethodDefPointer { } } -// TODO: This can be a const assert on Rust 1.57 const _: () = - [()][mem::size_of::() - mem::size_of::>()]; + assert!(mem::size_of::() == mem::size_of::>()); #[cfg(not(Py_3_9))] extern "C" { diff --git a/src/conversions/chrono.rs b/src/conversions/chrono.rs index 2e220681951..c50c2f7ef75 100644 --- a/src/conversions/chrono.rs +++ b/src/conversions/chrono.rs @@ -19,9 +19,6 @@ //! # Example: Convert a `datetime.datetime` to chrono's `DateTime` //! //! ```rust -//! # // `chrono::Duration` has been renamed to `chrono::TimeDelta` and its constructors changed -//! # // TODO: upgrade to Chrono 0.4.35+ after upgrading our MSRV to 1.61+ -//! # #![allow(deprecated)] //! use chrono::{DateTime, Duration, TimeZone, Utc}; //! use pyo3::{Python, ToPyObject}; //! @@ -43,10 +40,6 @@ //! } //! ``` -// `chrono::Duration` has been renamed to `chrono::TimeDelta` and its constructors changed -// TODO: upgrade to Chrono 0.4.35+ after upgrading our MSRV to 1.61+ -#![allow(deprecated)] - use crate::exceptions::{PyTypeError, PyUserWarning, PyValueError}; #[cfg(Py_LIMITED_API)] use crate::sync::GILOnceCell; diff --git a/src/conversions/std/array.rs b/src/conversions/std/array.rs index 14ccfd35413..3e575127793 100644 --- a/src/conversions/std/array.rs +++ b/src/conversions/std/array.rs @@ -75,7 +75,7 @@ where } // TODO use std::array::try_from_fn, if that stabilises: -// (https://github.com/rust-lang/rust/pull/75644) +// (https://github.com/rust-lang/rust/issues/89379) fn array_try_from_fn(mut cb: F) -> Result<[T; N], E> where F: FnMut(usize) -> Result, diff --git a/src/marker.rs b/src/marker.rs index dae4fcab44d..d3b74764fd9 100644 --- a/src/marker.rs +++ b/src/marker.rs @@ -281,6 +281,7 @@ mod nightly { // All the borrowing wrappers #[allow(deprecated)] + #[cfg(feature = "gil-refs")] impl !Ungil for crate::PyCell {} impl !Ungil for crate::PyRef<'_, T> {} impl !Ungil for crate::PyRefMut<'_, T> {} diff --git a/tests/ui/invalid_pymethods_duplicates.rs b/tests/ui/invalid_pymethods_duplicates.rs index d05d70959fc..04435dcb64a 100644 --- a/tests/ui/invalid_pymethods_duplicates.rs +++ b/tests/ui/invalid_pymethods_duplicates.rs @@ -3,6 +3,7 @@ use pyo3::prelude::*; +#[pyclass] struct TwoNew {} #[pymethods] @@ -18,6 +19,7 @@ impl TwoNew { } } +#[pyclass] struct DuplicateMethod {} #[pymethods] diff --git a/tests/ui/invalid_pymethods_duplicates.stderr b/tests/ui/invalid_pymethods_duplicates.stderr index 466e933a6dc..a68d01cf5fb 100644 --- a/tests/ui/invalid_pymethods_duplicates.stderr +++ b/tests/ui/invalid_pymethods_duplicates.stderr @@ -1,7 +1,7 @@ error[E0119]: conflicting implementations of trait `pyo3::impl_::pyclass::PyClassNewTextSignature` for type `pyo3::impl_::pyclass::PyClassImplCollector` - --> tests/ui/invalid_pymethods_duplicates.rs:8:1 + --> tests/ui/invalid_pymethods_duplicates.rs:9:1 | -8 | #[pymethods] +9 | #[pymethods] | ^^^^^^^^^^^^ | | | first implementation here @@ -9,27 +9,10 @@ error[E0119]: conflicting implementations of trait `pyo3::impl_::pyclass::PyClas | = note: this error originates in the attribute macro `pymethods` (in Nightly builds, run with -Z macro-backtrace for more info) -error[E0277]: the trait bound `TwoNew: PyTypeInfo` is not satisfied - --> tests/ui/invalid_pymethods_duplicates.rs:9:6 - | -9 | impl TwoNew { - | ^^^^^^ the trait `PyTypeInfo` is not implemented for `TwoNew` - | - = help: the following other types implement trait `PyTypeInfo`: - CancelledError - IncompleteReadError - InvalidStateError - LimitOverrunError - PanicException - PyAny - PyArithmeticError - PyAssertionError - and $N others - error[E0592]: duplicate definitions with name `__pymethod___new____` - --> tests/ui/invalid_pymethods_duplicates.rs:8:1 + --> tests/ui/invalid_pymethods_duplicates.rs:9:1 | -8 | #[pymethods] +9 | #[pymethods] | ^^^^^^^^^^^^ | | | duplicate definitions for `__pymethod___new____` @@ -38,80 +21,12 @@ error[E0592]: duplicate definitions with name `__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 + --> tests/ui/invalid_pymethods_duplicates.rs:25:1 | -23 | #[pymethods] +25 | #[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 `TwoNew: PyClass` is not satisfied - --> tests/ui/invalid_pymethods_duplicates.rs:8:1 - | -8 | #[pymethods] - | ^^^^^^^^^^^^ the trait `PyClass` is not implemented for `TwoNew` - | - = help: the trait `PyClass` is implemented for `pyo3::coroutine::Coroutine` -note: required by a bound in `PyClassInitializer` - --> src/pyclass_init.rs - | - | pub struct PyClassInitializer(PyClassInitializerImpl); - | ^^^^^^^ required by this bound in `PyClassInitializer` - = note: this error originates in the attribute macro `pymethods` (in Nightly builds, run with -Z macro-backtrace for more info) - -error[E0599]: no method named `convert` found for struct `TwoNew` in the current scope - --> tests/ui/invalid_pymethods_duplicates.rs:8:1 - | -6 | struct TwoNew {} - | ------------- method `convert` not found for this struct -7 | -8 | #[pymethods] - | ^^^^^^^^^^^^ method not found in `TwoNew` - | - = help: items from traits can only be used if the trait is implemented and in scope - = note: the following trait defines an item `convert`, perhaps you need to implement it: - candidate #1: `IntoPyCallbackOutput` - = note: this error originates in the attribute macro `pymethods` (in Nightly builds, run with -Z macro-backtrace for more info) - -error[E0277]: the trait bound `DuplicateMethod: PyClass` is not satisfied - --> tests/ui/invalid_pymethods_duplicates.rs:26:15 - | -26 | fn func_a(&self) {} - | ^ the trait `PyClass` is not implemented for `DuplicateMethod` - | - = help: the trait `PyClass` is implemented for `pyo3::coroutine::Coroutine` -note: required by a bound in `extract_pyclass_ref` - --> src/impl_/extract_argument.rs - | - | pub fn extract_pyclass_ref<'a, 'py: 'a, T: PyClass>( - | ^^^^^^^ required by this bound in `extract_pyclass_ref` - -error[E0277]: the trait bound `DuplicateMethod: PyClass` is not satisfied - --> tests/ui/invalid_pymethods_duplicates.rs:23:1 - | -23 | #[pymethods] - | ^^^^^^^^^^^^ the trait `PyClass` is not implemented for `DuplicateMethod` - | - = help: the trait `PyClass` is implemented for `pyo3::coroutine::Coroutine` -note: required by a bound in `PyRef` - --> src/pycell.rs - | - | pub struct PyRef<'p, T: PyClass> { - | ^^^^^^^ required by this bound in `PyRef` - = note: this error originates in the attribute macro `pymethods` (in Nightly builds, run with -Z macro-backtrace for more info) - -error[E0277]: the trait bound `DuplicateMethod: PyClass` is not satisfied - --> tests/ui/invalid_pymethods_duplicates.rs:29:15 - | -29 | fn func_b(&self) {} - | ^ the trait `PyClass` is not implemented for `DuplicateMethod` - | - = help: the trait `PyClass` is implemented for `pyo3::coroutine::Coroutine` -note: required by a bound in `extract_pyclass_ref` - --> src/impl_/extract_argument.rs - | - | pub fn extract_pyclass_ref<'a, 'py: 'a, T: PyClass>( - | ^^^^^^^ required by this bound in `extract_pyclass_ref` From 56341cbc81163c93fb0f231d15f08eb059c7aa5e Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Fri, 21 Jun 2024 10:49:33 +0200 Subject: [PATCH 122/495] emit c-string literals on Rust 1.77 or later (#4269) * emit c-string literals on Rust 1.77 or later * only clone `PyO3CratePath` instead of the whole `Ctx` Co-authored-by: mejrs <59372212+mejrs@users.noreply.github.com> --------- Co-authored-by: mejrs <59372212+mejrs@users.noreply.github.com> Co-authored-by: David Hewitt --- pyo3-build-config/src/lib.rs | 5 ++ pyo3-macros-backend/Cargo.toml | 5 +- pyo3-macros-backend/build.rs | 4 ++ pyo3-macros-backend/src/konst.rs | 11 ++--- pyo3-macros-backend/src/method.rs | 11 ++--- pyo3-macros-backend/src/module.rs | 8 ++-- pyo3-macros-backend/src/pyclass.rs | 4 +- pyo3-macros-backend/src/pymethod.rs | 9 ++-- pyo3-macros-backend/src/utils.rs | 74 +++++++++++++++++++++++++---- 9 files changed, 100 insertions(+), 31 deletions(-) create mode 100644 pyo3-macros-backend/build.rs diff --git a/pyo3-build-config/src/lib.rs b/pyo3-build-config/src/lib.rs index 21e057d8166..a2d4298c524 100644 --- a/pyo3-build-config/src/lib.rs +++ b/pyo3-build-config/src/lib.rs @@ -141,6 +141,10 @@ pub fn print_feature_cfgs() { println!("cargo:rustc-cfg=invalid_from_utf8_lint"); } + if rustc_minor_version >= 77 { + println!("cargo:rustc-cfg=c_str_lit"); + } + // Actually this is available on 1.78, but we should avoid // https://github.com/rust-lang/rust/issues/124651 just in case if rustc_minor_version >= 79 { @@ -167,6 +171,7 @@ pub fn print_expected_cfgs() { println!("cargo:rustc-check-cfg=cfg(pyo3_disable_reference_pool)"); println!("cargo:rustc-check-cfg=cfg(pyo3_leak_on_drop_without_reference_pool)"); println!("cargo:rustc-check-cfg=cfg(diagnostic_namespace)"); + println!("cargo:rustc-check-cfg=cfg(c_str_lit)"); // allow `Py_3_*` cfgs from the minimum supported version up to the // maximum minor version (+1 for development for the next) diff --git a/pyo3-macros-backend/Cargo.toml b/pyo3-macros-backend/Cargo.toml index 7bc0f6a2da1..db1bdd880bd 100644 --- a/pyo3-macros-backend/Cargo.toml +++ b/pyo3-macros-backend/Cargo.toml @@ -20,10 +20,13 @@ pyo3-build-config = { path = "../pyo3-build-config", version = "=0.22.0-dev", fe quote = { version = "1", default-features = false } [dependencies.syn] -version = "2" +version = "2.0.59" # for `LitCStr` default-features = false features = ["derive", "parsing", "printing", "clone-impls", "full", "extra-traits"] +[build-dependencies] +pyo3-build-config = { path = "../pyo3-build-config", version = "=0.22.0-dev" } + [lints] workspace = true diff --git a/pyo3-macros-backend/build.rs b/pyo3-macros-backend/build.rs new file mode 100644 index 00000000000..55aa0ba03c5 --- /dev/null +++ b/pyo3-macros-backend/build.rs @@ -0,0 +1,4 @@ +fn main() { + pyo3_build_config::print_expected_cfgs(); + pyo3_build_config::print_feature_cfgs(); +} diff --git a/pyo3-macros-backend/src/konst.rs b/pyo3-macros-backend/src/konst.rs index 2a7667dd5f2..3547698dc62 100644 --- a/pyo3-macros-backend/src/konst.rs +++ b/pyo3-macros-backend/src/konst.rs @@ -1,12 +1,12 @@ use std::borrow::Cow; +use std::ffi::CString; -use crate::utils::Ctx; +use crate::utils::{Ctx, LitCStr}; use crate::{ attributes::{self, get_pyo3_options, take_attributes, NameAttribute}, deprecations::Deprecations, }; -use proc_macro2::{Ident, TokenStream}; -use quote::quote; +use proc_macro2::{Ident, Span}; use syn::{ ext::IdentExt, parse::{Parse, ParseStream}, @@ -29,10 +29,9 @@ impl ConstSpec<'_> { } /// Null-terminated Python name - pub fn null_terminated_python_name(&self, ctx: &Ctx) -> TokenStream { - let Ctx { pyo3_path, .. } = ctx; + pub fn null_terminated_python_name(&self, ctx: &Ctx) -> LitCStr { let name = self.python_name().to_string(); - quote!(#pyo3_path::ffi::c_str!(#name)) + LitCStr::new(CString::new(name).unwrap(), Span::call_site(), ctx) } } diff --git a/pyo3-macros-backend/src/method.rs b/pyo3-macros-backend/src/method.rs index 294cd4db969..9e4b1ad13d5 100644 --- a/pyo3-macros-backend/src/method.rs +++ b/pyo3-macros-backend/src/method.rs @@ -1,4 +1,5 @@ use std::borrow::Cow; +use std::ffi::CString; use std::fmt::Display; use proc_macro2::{Span, TokenStream}; @@ -6,7 +7,7 @@ use quote::{format_ident, quote, quote_spanned, ToTokens}; use syn::{ext::IdentExt, spanned::Spanned, Ident, Result}; use crate::deprecations::deprecate_trailing_option_default; -use crate::utils::Ctx; +use crate::utils::{Ctx, LitCStr}; use crate::{ attributes::{FromPyWithAttribute, TextSignatureAttribute, TextSignatureAttributeValue}, deprecations::{Deprecation, Deprecations}, @@ -472,12 +473,10 @@ impl<'a> FnSpec<'a> { }) } - pub fn null_terminated_python_name(&self, ctx: &Ctx) -> TokenStream { - let Ctx { pyo3_path, .. } = ctx; - let span = self.python_name.span(); - let pyo3_path = pyo3_path.to_tokens_spanned(span); + pub fn null_terminated_python_name(&self, ctx: &Ctx) -> LitCStr { let name = self.python_name.to_string(); - quote_spanned!(self.python_name.span() => #pyo3_path::ffi::c_str!(#name)) + let name = CString::new(name).unwrap(); + LitCStr::new(name, self.python_name.span(), ctx) } fn parse_fn_type( diff --git a/pyo3-macros-backend/src/module.rs b/pyo3-macros-backend/src/module.rs index 3443507bb3a..39240aba7e8 100644 --- a/pyo3-macros-backend/src/module.rs +++ b/pyo3-macros-backend/src/module.rs @@ -7,10 +7,11 @@ use crate::{ get_doc, pyclass::PyClassPyO3Option, pyfunction::{impl_wrap_pyfunction, PyFunctionOptions}, - utils::Ctx, + utils::{Ctx, LitCStr}, }; -use proc_macro2::TokenStream; +use proc_macro2::{Span, TokenStream}; use quote::quote; +use std::ffi::CString; use syn::{ ext::IdentExt, parse::{Parse, ParseStream}, @@ -403,10 +404,11 @@ fn module_initialization(name: &syn::Ident, ctx: &Ctx) -> TokenStream { let Ctx { pyo3_path, .. } = ctx; let pyinit_symbol = format!("PyInit_{}", name); let name = name.to_string(); + let pyo3_name = LitCStr::new(CString::new(name).unwrap(), Span::call_site(), ctx); quote! { #[doc(hidden)] - pub const __PYO3_NAME: &'static ::std::ffi::CStr = #pyo3_path::ffi::c_str!(#name); + pub const __PYO3_NAME: &'static ::std::ffi::CStr = #pyo3_name; pub(super) struct MakeDef; #[doc(hidden)] diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index 85732ae55ff..01377767fdc 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -22,7 +22,7 @@ use crate::pymethod::{ SlotDef, __GETITEM__, __HASH__, __INT__, __LEN__, __REPR__, __RICHCMP__, }; use crate::pyversions; -use crate::utils::{self, apply_renaming_rule, PythonDoc}; +use crate::utils::{self, apply_renaming_rule, LitCStr, PythonDoc}; use crate::utils::{is_abi3, Ctx}; use crate::PyFunctionOptions; @@ -2016,7 +2016,7 @@ impl<'a> PyClassImplsBuilder<'a> { let Ctx { pyo3_path, .. } = ctx; let cls = self.cls; let doc = self.doc.as_ref().map_or( - quote! {#pyo3_path::ffi::c_str!("")}, + LitCStr::empty(ctx).to_token_stream(), PythonDoc::to_token_stream, ); let is_basetype = self.attr.options.subclass.is_some(); diff --git a/pyo3-macros-backend/src/pymethod.rs b/pyo3-macros-backend/src/pymethod.rs index 059475248eb..c87a312424b 100644 --- a/pyo3-macros-backend/src/pymethod.rs +++ b/pyo3-macros-backend/src/pymethod.rs @@ -1,11 +1,12 @@ use std::borrow::Cow; +use std::ffi::CString; use crate::attributes::{NameAttribute, RenamingRule}; use crate::deprecations::deprecate_trailing_option_default; use crate::method::{CallingConvention, ExtractErrorMode, PyArg}; use crate::params::{check_arg_for_gil_refs, impl_regular_arg_param, Holders}; -use crate::utils::Ctx; use crate::utils::PythonDoc; +use crate::utils::{Ctx, LitCStr}; use crate::{ method::{FnArg, FnSpec, FnType, SelfType}, pyfunction::PyFunctionOptions, @@ -870,8 +871,7 @@ pub enum PropertyType<'a> { } impl PropertyType<'_> { - fn null_terminated_python_name(&self, ctx: &Ctx) -> Result { - let Ctx { pyo3_path, .. } = ctx; + fn null_terminated_python_name(&self, ctx: &Ctx) -> Result { match self { PropertyType::Descriptor { field, @@ -892,7 +892,8 @@ impl PropertyType<'_> { bail_spanned!(field.span() => "`get` and `set` with tuple struct fields require `name`"); } }; - Ok(quote_spanned!(field.span() => #pyo3_path::ffi::c_str!(#name))) + let name = CString::new(name).unwrap(); + Ok(LitCStr::new(name, field.span(), ctx)) } PropertyType::Function { spec, .. } => Ok(spec.null_terminated_python_name(ctx)), } diff --git a/pyo3-macros-backend/src/utils.rs b/pyo3-macros-backend/src/utils.rs index 22b16010480..005884a557c 100644 --- a/pyo3-macros-backend/src/utils.rs +++ b/pyo3-macros-backend/src/utils.rs @@ -1,6 +1,7 @@ use crate::attributes::{CrateAttribute, RenamingRule}; use proc_macro2::{Span, TokenStream}; use quote::{quote, ToTokens}; +use std::ffi::CString; use syn::spanned::Spanned; use syn::{punctuated::Punctuated, Token}; @@ -67,14 +68,59 @@ pub fn option_type_argument(ty: &syn::Type) -> Option<&syn::Type> { None } +// TODO: Replace usage of this by [`syn::LitCStr`] when on MSRV 1.77 +#[derive(Clone)] +pub struct LitCStr { + lit: CString, + span: Span, + pyo3_path: PyO3CratePath, +} + +impl LitCStr { + pub fn new(lit: CString, span: Span, ctx: &Ctx) -> Self { + Self { + lit, + span, + pyo3_path: ctx.pyo3_path.clone(), + } + } + + pub fn empty(ctx: &Ctx) -> Self { + Self { + lit: CString::new("").unwrap(), + span: Span::call_site(), + pyo3_path: ctx.pyo3_path.clone(), + } + } +} + +impl quote::ToTokens for LitCStr { + fn to_tokens(&self, tokens: &mut TokenStream) { + if cfg!(c_str_lit) { + syn::LitCStr::new(&self.lit, self.span).to_tokens(tokens); + } else { + let pyo3_path = &self.pyo3_path; + let lit = self.lit.to_str().unwrap(); + tokens.extend(quote::quote_spanned!(self.span => #pyo3_path::ffi::c_str!(#lit))); + } + } +} + /// A syntax tree which evaluates to a nul-terminated docstring for Python. /// /// Typically the tokens will just be that string, but if the original docs included macro /// expressions then the tokens will be a concat!("...", "\n", "\0") expression of the strings and -/// macro parts. -/// contents such as parse the string contents. +/// macro parts. contents such as parse the string contents. +#[derive(Clone)] +pub struct PythonDoc(PythonDocKind); + #[derive(Clone)] -pub struct PythonDoc(TokenStream); +enum PythonDocKind { + LitCStr(LitCStr), + // There is currently no way to `concat!` c-string literals, we fallback to the `c_str!` macro in + // this case. + Tokens(TokenStream), +} /// Collects all #[doc = "..."] attributes into a TokenStream evaluating to a null-terminated string. /// @@ -125,7 +171,7 @@ pub fn get_doc( } } - let tokens = if !parts.is_empty() { + if !parts.is_empty() { // Doc contained macro pieces - return as `concat!` expression if !current_part.is_empty() { parts.push(current_part.to_token_stream()); @@ -140,17 +186,26 @@ pub fn get_doc( syn::token::Comma(Span::call_site()).to_tokens(tokens); }); - tokens + PythonDoc(PythonDocKind::Tokens( + quote!(#pyo3_path::ffi::c_str!(#tokens)), + )) } else { // Just a string doc - return directly with nul terminator - current_part.to_token_stream() - }; - PythonDoc(quote!(#pyo3_path::ffi::c_str!(#tokens))) + let docs = CString::new(current_part).unwrap(); + PythonDoc(PythonDocKind::LitCStr(LitCStr::new( + docs, + Span::call_site(), + ctx, + ))) + } } impl quote::ToTokens for PythonDoc { fn to_tokens(&self, tokens: &mut TokenStream) { - self.0.to_tokens(tokens) + match &self.0 { + PythonDocKind::LitCStr(lit) => lit.to_tokens(tokens), + PythonDocKind::Tokens(toks) => toks.to_tokens(tokens), + } } } @@ -194,6 +249,7 @@ impl Ctx { } } +#[derive(Clone)] pub enum PyO3CratePath { Given(syn::Path), Default, From 9ff3d237c147ba772dd65b3fc6c0d13c3ee026d5 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Fri, 21 Jun 2024 06:05:09 -0400 Subject: [PATCH 123/495] Update dependencies to reflect minimal versions (#4272) Based on testing locally with `rm -f Cargo.lock && cargo +nightly check --tests --all -Z minimal-versions` --- Cargo.toml | 2 +- pyo3-ffi-check/macro/Cargo.toml | 2 +- pyo3-macros-backend/Cargo.toml | 2 +- pyo3-macros/Cargo.toml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index dac3314a850..9ab15c1d052 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,7 @@ rust-version = "1.63" cfg-if = "1.0" libc = "0.2.62" memoffset = "0.9" -once_cell = "1" +once_cell = "1.13.0" # ffi bindings to the python interpreter, split into a separate crate so they can be used independently pyo3-ffi = { path = "pyo3-ffi", version = "=0.22.0-dev" } diff --git a/pyo3-ffi-check/macro/Cargo.toml b/pyo3-ffi-check/macro/Cargo.toml index 46395fc8a8c..dfefcd2c1e1 100644 --- a/pyo3-ffi-check/macro/Cargo.toml +++ b/pyo3-ffi-check/macro/Cargo.toml @@ -10,6 +10,6 @@ proc-macro = true [dependencies] glob = "0.3" quote = "1" -proc-macro2 = "1" +proc-macro2 = "1.0.60" scraper = "0.17" pyo3-build-config = { path = "../../pyo3-build-config" } diff --git a/pyo3-macros-backend/Cargo.toml b/pyo3-macros-backend/Cargo.toml index db1bdd880bd..54220ab462b 100644 --- a/pyo3-macros-backend/Cargo.toml +++ b/pyo3-macros-backend/Cargo.toml @@ -15,7 +15,7 @@ edition = "2021" # See https://github.com/PyO3/pyo3/pull/810 for more. [dependencies] heck = "0.5" -proc-macro2 = { version = "1", default-features = false } +proc-macro2 = { version = "1.0.60", default-features = false } pyo3-build-config = { path = "../pyo3-build-config", version = "=0.22.0-dev", features = ["resolve-config"] } quote = { version = "1", default-features = false } diff --git a/pyo3-macros/Cargo.toml b/pyo3-macros/Cargo.toml index 789b8095b3f..04c0ae6fb28 100644 --- a/pyo3-macros/Cargo.toml +++ b/pyo3-macros/Cargo.toml @@ -19,7 +19,7 @@ experimental-async = ["pyo3-macros-backend/experimental-async"] gil-refs = ["pyo3-macros-backend/gil-refs"] [dependencies] -proc-macro2 = { version = "1", default-features = false } +proc-macro2 = { version = "1.0.60", default-features = false } quote = "1" syn = { version = "2", features = ["full", "extra-traits"] } pyo3-macros-backend = { path = "../pyo3-macros-backend", version = "=0.22.0-dev" } From 41fb9572b6001fe99fcb6dc910303a997b76d5a7 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Fri, 21 Jun 2024 12:30:57 +0100 Subject: [PATCH 124/495] docs: update docstring on `Python` for `Bound` API (#4274) --- src/marker.rs | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/src/marker.rs b/src/marker.rs index d3b74764fd9..065adc7dda9 100644 --- a/src/marker.rs +++ b/src/marker.rs @@ -311,7 +311,7 @@ pub use nightly::Ungil; /// - It can be passed to functions that require a proof of holding the GIL, such as /// [`Py::clone_ref`]. /// - Its lifetime represents the scope of holding the GIL which can be used to create Rust -/// references that are bound to it, such as `&`[`PyAny`]. +/// references that are bound to it, such as [`Bound<'py, PyAny>`]. /// /// Note that there are some caveats to using it that you might need to be aware of. See the /// [Deadlocks](#deadlocks) and [Releasing and freeing memory](#releasing-and-freeing-memory) @@ -319,14 +319,17 @@ pub use nightly::Ungil; /// /// # Obtaining a Python token /// -/// The following are the recommended ways to obtain a [`Python`] token, in order of preference: +/// The following are the recommended ways to obtain a [`Python<'py>`] token, in order of preference: +/// - If you already have something with a lifetime bound to the GIL, such as [`Bound<'py, PyAny>`], you can +/// use its `.py()` method to get a token. /// - In a function or method annotated with [`#[pyfunction]`](crate::pyfunction) or [`#[pymethods]`](crate::pymethods) you can declare it /// as a parameter, and PyO3 will pass in the token when Python code calls it. -/// - If you already have something with a lifetime bound to the GIL, such as `&`[`PyAny`], you can -/// use its `.py()` method to get a token. /// - When you need to acquire the GIL yourself, such as when calling Python code from Rust, you /// should call [`Python::with_gil`] to do that and pass your code as a closure to it. /// +/// The first two options are zero-cost; [`Python::with_gil`] requires runtime checking and may need to block +/// to acquire the GIL. +/// /// # Deadlocks /// /// Note that the GIL can be temporarily released by the Python interpreter during a function call @@ -353,14 +356,8 @@ pub use nightly::Ungil; /// /// # Releasing and freeing memory /// -/// The [`Python`] type can be used to create references to variables owned by the Python +/// The [`Python<'py>`] type can be used to create references to variables owned by the Python /// interpreter, using functions such as [`Python::eval_bound`] and [`PyModule::import_bound`]. -/// -/// See the [Memory Management] chapter of the guide for more information about how PyO3 manages memory. -/// -/// [scoping rules]: https://doc.rust-lang.org/stable/book/ch04-01-what-is-ownership.html#ownership-rules -/// [`Py::clone_ref`]: crate::Py::clone_ref -/// [Memory Management]: https://pyo3.rs/main/memory.html#gil-bound-memory #[derive(Copy, Clone)] pub struct Python<'py>(PhantomData<(&'py GILGuard, NotSend)>); From 0b967d427a20122fef138a0daa49eb52946a6933 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Sat, 22 Jun 2024 00:33:34 +0100 Subject: [PATCH 125/495] use `ffi::MemberGef` for `#[pyo3(get)]` fields of `Py` (#4254) * use `ffi::MemberGef` for `#[pyo3(get)]` fields of `Py` * tidy up implementation * make it work on MSRV :( * fix docs and newsfragment * clippy * internal docs and coverage * review: mejrs --- newsfragments/4254.changed.md | 1 + pyo3-macros-backend/src/pyclass.rs | 14 +- pyo3-macros-backend/src/pyimpl.rs | 14 +- pyo3-macros-backend/src/pymethod.rs | 189 +++++++++-------- src/impl_/pyclass.rs | 293 +++++++++++++++++++++++++- src/impl_/pyclass/lazy_type_object.rs | 11 +- src/impl_/pymethods.rs | 3 + src/pycell/impl_.rs | 2 +- src/pyclass.rs | 12 +- src/pyclass/create_type_object.rs | 29 ++- tests/test_class_basics.rs | 3 - tests/test_class_conversion.rs | 4 - tests/test_getter_setter.rs | 22 ++ tests/test_methods.rs | 11 +- tests/test_no_imports.rs | 3 - tests/test_sequence.rs | 3 - tests/ui/invalid_property_args.rs | 6 + tests/ui/invalid_property_args.stderr | 18 ++ 18 files changed, 498 insertions(+), 140 deletions(-) create mode 100644 newsfragments/4254.changed.md diff --git a/newsfragments/4254.changed.md b/newsfragments/4254.changed.md new file mode 100644 index 00000000000..e58e0345696 --- /dev/null +++ b/newsfragments/4254.changed.md @@ -0,0 +1 @@ +Optimize code generated by `#[pyo3(get)]` on `#[pyclass]` fields. diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index 01377767fdc..07d9e32e528 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -1415,12 +1415,14 @@ pub fn gen_complex_enum_variant_attr( }; let method_def = quote! { - #pyo3_path::class::PyMethodDefType::ClassAttribute({ - #pyo3_path::class::PyClassAttributeDef::new( - #python_name, - #cls_type::#wrapper_ident - ) - }) + #pyo3_path::impl_::pyclass::MaybeRuntimePyMethodDef::Static( + #pyo3_path::class::PyMethodDefType::ClassAttribute({ + #pyo3_path::class::PyClassAttributeDef::new( + #python_name, + #cls_type::#wrapper_ident + ) + }) + ) }; MethodAndMethodDef { diff --git a/pyo3-macros-backend/src/pyimpl.rs b/pyo3-macros-backend/src/pyimpl.rs index dbb1fba894c..6807f90831e 100644 --- a/pyo3-macros-backend/src/pyimpl.rs +++ b/pyo3-macros-backend/src/pyimpl.rs @@ -197,12 +197,14 @@ pub fn gen_py_const(cls: &syn::Type, spec: &ConstSpec<'_>, ctx: &Ctx) -> MethodA }; let method_def = quote! { - #pyo3_path::class::PyMethodDefType::ClassAttribute({ - #pyo3_path::class::PyClassAttributeDef::new( - #python_name, - #cls::#wrapper_ident - ) - }) + #pyo3_path::impl_::pyclass::MaybeRuntimePyMethodDef::Static( + #pyo3_path::class::PyMethodDefType::ClassAttribute({ + #pyo3_path::class::PyClassAttributeDef::new( + #python_name, + #cls::#wrapper_ident + ) + }) + ) }; MethodAndMethodDef { diff --git a/pyo3-macros-backend/src/pymethod.rs b/pyo3-macros-backend/src/pymethod.rs index c87a312424b..7a9afa54755 100644 --- a/pyo3-macros-backend/src/pymethod.rs +++ b/pyo3-macros-backend/src/pymethod.rs @@ -330,7 +330,9 @@ pub fn impl_py_method_def( }; let methoddef = spec.get_methoddef(quote! { #cls::#wrapper_ident }, doc, ctx); let method_def = quote! { - #pyo3_path::class::PyMethodDefType::#methoddef_type(#methoddef #add_flags) + #pyo3_path::impl_::pyclass::MaybeRuntimePyMethodDef::Static( + #pyo3_path::class::PyMethodDefType::#methoddef_type(#methoddef #add_flags) + ) }; Ok(MethodAndMethodDef { associated_method, @@ -511,12 +513,14 @@ fn impl_py_class_attribute( }; let method_def = quote! { - #pyo3_path::class::PyMethodDefType::ClassAttribute({ - #pyo3_path::class::PyClassAttributeDef::new( - #python_name, - #cls::#wrapper_ident - ) - }) + #pyo3_path::impl_::pyclass::MaybeRuntimePyMethodDef::Static( + #pyo3_path::class::PyMethodDefType::ClassAttribute({ + #pyo3_path::class::PyClassAttributeDef::new( + #python_name, + #cls::#wrapper_ident + ) + }) + ) }; Ok(MethodAndMethodDef { @@ -701,11 +705,13 @@ pub fn impl_py_setter_def( let method_def = quote! { #cfg_attrs - #pyo3_path::class::PyMethodDefType::Setter( - #pyo3_path::class::PySetterDef::new( - #python_name, - #cls::#wrapper_ident, - #doc + #pyo3_path::impl_::pyclass::MaybeRuntimePyMethodDef::Static( + #pyo3_path::class::PyMethodDefType::Setter( + #pyo3_path::class::PySetterDef::new( + #python_name, + #cls::#wrapper_ident, + #doc + ) ) ) }; @@ -750,102 +756,107 @@ pub fn impl_py_getter_def( let python_name = property_type.null_terminated_python_name(ctx)?; let doc = property_type.doc(ctx); + let mut cfg_attrs = TokenStream::new(); + if let PropertyType::Descriptor { field, .. } = &property_type { + for attr in field + .attrs + .iter() + .filter(|attr| attr.path().is_ident("cfg")) + { + attr.to_tokens(&mut cfg_attrs); + } + } + let mut holders = Holders::new(); - let body = match property_type { + match property_type { PropertyType::Descriptor { field_index, field, .. } => { - let slf = SelfType::Receiver { - mutable: false, - span: Span::call_site(), - } - .receiver(cls, ExtractErrorMode::Raise, &mut holders, ctx); - let field_token = if let Some(ident) = &field.ident { - // named struct field + let ty = &field.ty; + let field = if let Some(ident) = &field.ident { ident.to_token_stream() } else { - // tuple struct field syn::Index::from(field_index).to_token_stream() }; - quotes::map_result_into_ptr( - quotes::ok_wrap( - quote! { - ::std::clone::Clone::clone(&(#slf.#field_token)) - }, - ctx, - ), - ctx, - ) + + // TODO: on MSRV 1.77+, we can use `::std::mem::offset_of!` here, and it should + // make it possible for the `MaybeRuntimePyMethodDef` to be a `Static` variant. + let method_def = quote_spanned! {ty.span()=> + #cfg_attrs + { + #[allow(unused_imports)] // might not be used if all probes are positve + use #pyo3_path::impl_::pyclass::Probe; + + struct Offset; + unsafe impl #pyo3_path::impl_::pyclass::OffsetCalculator<#cls, #ty> for Offset { + fn offset() -> usize { + #pyo3_path::impl_::pyclass::class_offset::<#cls>() + + #pyo3_path::impl_::pyclass::offset_of!(#cls, #field) + } + } + + const GENERATOR: #pyo3_path::impl_::pyclass::PyClassGetterGenerator::< + #cls, + #ty, + Offset, + { #pyo3_path::impl_::pyclass::IsPyT::<#ty>::VALUE }, + { #pyo3_path::impl_::pyclass::IsToPyObject::<#ty>::VALUE }, + > = unsafe { #pyo3_path::impl_::pyclass::PyClassGetterGenerator::new() }; + #pyo3_path::impl_::pyclass::MaybeRuntimePyMethodDef::Runtime( + || GENERATOR.generate(#python_name, #doc) + ) + } + }; + + Ok(MethodAndMethodDef { + associated_method: quote! {}, + method_def, + }) } // Forward to `IntoPyCallbackOutput`, to handle `#[getter]`s returning results. PropertyType::Function { spec, self_type, .. } => { + let wrapper_ident = format_ident!("__pymethod_get_{}__", spec.name); let call = impl_call_getter(cls, spec, self_type, &mut holders, ctx)?; - quote! { + let body = quote! { #pyo3_path::callback::convert(py, #call) - } - } - }; + }; - let wrapper_ident = match property_type { - PropertyType::Descriptor { - field: syn::Field { - ident: Some(ident), .. - }, - .. - } => { - format_ident!("__pymethod_get_{}__", ident) - } - PropertyType::Descriptor { field_index, .. } => { - format_ident!("__pymethod_get_field_{}__", field_index) - } - PropertyType::Function { spec, .. } => { - format_ident!("__pymethod_get_{}__", spec.name) - } - }; + let init_holders = holders.init_holders(ctx); + let check_gil_refs = holders.check_gil_refs(); + let associated_method = quote! { + #cfg_attrs + unsafe fn #wrapper_ident( + py: #pyo3_path::Python<'_>, + _slf: *mut #pyo3_path::ffi::PyObject + ) -> #pyo3_path::PyResult<*mut #pyo3_path::ffi::PyObject> { + #init_holders + let result = #body; + #check_gil_refs + result + } + }; - let mut cfg_attrs = TokenStream::new(); - if let PropertyType::Descriptor { field, .. } = &property_type { - for attr in field - .attrs - .iter() - .filter(|attr| attr.path().is_ident("cfg")) - { - attr.to_tokens(&mut cfg_attrs); - } - } + let method_def = quote! { + #cfg_attrs + #pyo3_path::impl_::pyclass::MaybeRuntimePyMethodDef::Static( + #pyo3_path::class::PyMethodDefType::Getter( + #pyo3_path::class::PyGetterDef::new( + #python_name, + #cls::#wrapper_ident, + #doc + ) + ) + ) + }; - let init_holders = holders.init_holders(ctx); - let check_gil_refs = holders.check_gil_refs(); - let associated_method = quote! { - #cfg_attrs - unsafe fn #wrapper_ident( - py: #pyo3_path::Python<'_>, - _slf: *mut #pyo3_path::ffi::PyObject - ) -> #pyo3_path::PyResult<*mut #pyo3_path::ffi::PyObject> { - #init_holders - let result = #body; - #check_gil_refs - result + Ok(MethodAndMethodDef { + associated_method, + method_def, + }) } - }; - - let method_def = quote! { - #cfg_attrs - #pyo3_path::class::PyMethodDefType::Getter( - #pyo3_path::class::PyGetterDef::new( - #python_name, - #cls::#wrapper_ident, - #doc - ) - ) - }; - - Ok(MethodAndMethodDef { - associated_method, - method_def, - }) + } } /// Split an argument of pyo3::Python from the front of the arg list, if present diff --git a/src/impl_/pyclass.rs b/src/impl_/pyclass.rs index acfce37e6cf..391e96f5f43 100644 --- a/src/impl_/pyclass.rs +++ b/src/impl_/pyclass.rs @@ -6,9 +6,9 @@ use crate::{ impl_::freelist::FreeList, impl_::pycell::{GetBorrowChecker, PyClassMutability, PyClassObjectLayout}, pyclass_init::PyObjectInit, - types::any::PyAnyMethods, - types::PyBool, - Borrowed, Py, PyAny, PyClass, PyErr, PyMethodDefType, PyResult, PyTypeInfo, Python, + types::{any::PyAnyMethods, PyBool}, + Borrowed, IntoPy, Py, PyAny, PyClass, PyErr, PyMethodDefType, PyResult, PyTypeInfo, Python, + ToPyObject, }; use std::{ borrow::Cow, @@ -131,8 +131,15 @@ impl Clone for PyClassImplCollector { impl Copy for PyClassImplCollector {} +pub enum MaybeRuntimePyMethodDef { + /// Used in cases where const functionality is not sufficient to define the method + /// purely at compile time. + Runtime(fn() -> PyMethodDefType), + Static(PyMethodDefType), +} + pub struct PyClassItems { - pub methods: &'static [PyMethodDefType], + pub methods: &'static [MaybeRuntimePyMethodDef], pub slots: &'static [ffi::PyType_Slot], } @@ -890,7 +897,7 @@ macro_rules! generate_pyclass_richcompare_slot { } pub use generate_pyclass_richcompare_slot; -use super::pycell::PyClassObject; +use super::{pycell::PyClassObject, pymethods::BoundRef}; /// Implements a freelist. /// @@ -1171,3 +1178,279 @@ pub(crate) unsafe extern "C" fn assign_sequence_item_from_mapping( ffi::Py_DECREF(index); result } + +/// Helper trait to locate field within a `#[pyclass]` for a `#[pyo3(get)]`. +/// +/// Below MSRV 1.77 we can't use `std::mem::offset_of!`, and the replacement in +/// `memoffset::offset_of` doesn't work in const contexts for types containing `UnsafeCell`. +/// +/// # Safety +/// +/// The trait is unsafe to implement because producing an incorrect offset will lead to UB. +pub unsafe trait OffsetCalculator { + /// Offset to the field within a `PyClassObject`, in bytes. + fn offset() -> usize; +} + +// Used in generated implementations of OffsetCalculator +pub fn class_offset() -> usize { + offset_of!(PyClassObject, contents) +} + +// Used in generated implementations of OffsetCalculator +pub use memoffset::offset_of; + +/// Type which uses specialization on impl blocks to determine how to read a field from a Rust pyclass +/// as part of a `#[pyo3(get)]` annotation. +pub struct PyClassGetterGenerator< + // structural information about the field: class type, field type, where the field is within the + // class struct + ClassT: PyClass, + FieldT, + Offset: OffsetCalculator, // on Rust 1.77+ this could be a const OFFSET: usize + // additional metadata about the field which is used to switch between different implementations + // at compile time + const IS_PY_T: bool, + const IMPLEMENTS_TOPYOBJECT: bool, +>(PhantomData<(ClassT, FieldT, Offset)>); + +impl< + ClassT: PyClass, + FieldT, + Offset: OffsetCalculator, + const IS_PY_T: bool, + const IMPLEMENTS_TOPYOBJECT: bool, + > PyClassGetterGenerator +{ + /// Safety: constructing this type requires that there exists a value of type FieldT + /// at the calculated offset within the type ClassT. + pub const unsafe fn new() -> Self { + Self(PhantomData) + } +} + +impl< + ClassT: PyClass, + U, + Offset: OffsetCalculator>, + const IMPLEMENTS_TOPYOBJECT: bool, + > PyClassGetterGenerator, Offset, true, IMPLEMENTS_TOPYOBJECT> +{ + /// `Py` fields have a potential optimization to use Python's "struct members" to read + /// the field directly from the struct, rather than using a getter function. + /// + /// This is the most efficient operation the Python interpreter could possibly do to + /// read a field, but it's only possible for us to allow this for frozen classes. + pub fn generate(&self, name: &'static CStr, doc: &'static CStr) -> PyMethodDefType { + use crate::pyclass::boolean_struct::private::Boolean; + if ClassT::Frozen::VALUE { + PyMethodDefType::StructMember(ffi::PyMemberDef { + name: name.as_ptr(), + type_code: ffi::Py_T_OBJECT_EX, + offset: Offset::offset() as ffi::Py_ssize_t, + flags: ffi::Py_READONLY, + doc: doc.as_ptr(), + }) + } else { + PyMethodDefType::Getter(crate::PyGetterDef { + name, + meth: pyo3_get_value_topyobject::, Offset>, + doc, + }) + } + } +} + +/// Field is not `Py`; try to use `ToPyObject` to avoid potentially expensive clones of containers like `Vec` +impl> + PyClassGetterGenerator +{ + pub const fn generate(&self, name: &'static CStr, doc: &'static CStr) -> PyMethodDefType { + PyMethodDefType::Getter(crate::PyGetterDef { + name, + meth: pyo3_get_value_topyobject::, + doc, + }) + } +} + +#[cfg_attr( + diagnostic_namespace, + diagnostic::on_unimplemented( + message = "`{Self}` cannot be converted to a Python object", + label = "required by `#[pyo3(get)]` to create a readable property from a field of type `{Self}`", + note = "implement `ToPyObject` or `IntoPy + Clone` for `{Self}` to define the conversion", + ) +)] +pub trait PyO3GetField: IntoPy> + Clone {} +impl> + Clone> PyO3GetField for T {} + +/// Base case attempts to use IntoPy + Clone, which was the only behaviour before PyO3 0.22. +impl> + PyClassGetterGenerator +{ + pub const fn generate(&self, name: &'static CStr, doc: &'static CStr) -> PyMethodDefType + // The bound goes here rather than on the block so that this impl is always available + // if no specialization is used instead + where + FieldT: PyO3GetField, + { + PyMethodDefType::Getter(crate::PyGetterDef { + name, + meth: pyo3_get_value::, + doc, + }) + } +} + +/// Trait used to combine with zero-sized types to calculate at compile time +/// some property of a type. +/// +/// The trick uses the fact that an associated constant has higher priority +/// than a trait constant, so we can use the trait to define the false case. +/// +/// The true case is defined in the zero-sized type's impl block, which is +/// gated on some property like trait bound or only being implemented +/// for fixed concrete types. +pub trait Probe { + const VALUE: bool = false; +} + +macro_rules! probe { + ($name:ident) => { + pub struct $name(PhantomData); + impl Probe for $name {} + }; +} + +probe!(IsPyT); + +impl IsPyT> { + pub const VALUE: bool = true; +} + +probe!(IsToPyObject); + +impl IsToPyObject { + pub const VALUE: bool = true; +} + +fn pyo3_get_value_topyobject< + ClassT: PyClass, + FieldT: ToPyObject, + Offset: OffsetCalculator, +>( + py: Python<'_>, + obj: *mut ffi::PyObject, +) -> PyResult<*mut ffi::PyObject> { + // Check for mutable aliasing + let _holder = unsafe { + BoundRef::ref_from_ptr(py, &obj) + .downcast_unchecked::() + .try_borrow()? + }; + + let value = unsafe { obj.cast::().add(Offset::offset()).cast::() }; + + // SAFETY: Offset is known to describe the location of the value, and + // _holder is preventing mutable aliasing + Ok((unsafe { &*value }).to_object(py).into_ptr()) +} + +fn pyo3_get_value< + ClassT: PyClass, + FieldT: IntoPy> + Clone, + Offset: OffsetCalculator, +>( + py: Python<'_>, + obj: *mut ffi::PyObject, +) -> PyResult<*mut ffi::PyObject> { + // Check for mutable aliasing + let _holder = unsafe { + BoundRef::ref_from_ptr(py, &obj) + .downcast_unchecked::() + .try_borrow()? + }; + + let value = unsafe { obj.cast::().add(Offset::offset()).cast::() }; + + // SAFETY: Offset is known to describe the location of the value, and + // _holder is preventing mutable aliasing + Ok((unsafe { &*value }).clone().into_py(py).into_ptr()) +} + +#[cfg(test)] +#[cfg(feature = "macros")] +mod tests { + use super::*; + + #[test] + fn get_py_for_frozen_class() { + #[crate::pyclass(crate = "crate", frozen)] + struct FrozenClass { + #[pyo3(get)] + value: Py, + } + + let mut methods = Vec::new(); + let mut slots = Vec::new(); + + for items in FrozenClass::items_iter() { + methods.extend(items.methods.iter().map(|m| match m { + MaybeRuntimePyMethodDef::Static(m) => m.clone(), + MaybeRuntimePyMethodDef::Runtime(r) => r(), + })); + slots.extend_from_slice(items.slots); + } + + assert_eq!(methods.len(), 1); + assert!(slots.is_empty()); + + match methods.first() { + Some(PyMethodDefType::StructMember(member)) => { + assert_eq!(unsafe { CStr::from_ptr(member.name) }, ffi::c_str!("value")); + assert_eq!(member.type_code, ffi::Py_T_OBJECT_EX); + assert_eq!( + member.offset, + (memoffset::offset_of!(PyClassObject, contents) + + memoffset::offset_of!(FrozenClass, value)) + as ffi::Py_ssize_t + ); + assert_eq!(member.flags, ffi::Py_READONLY); + } + _ => panic!("Expected a StructMember"), + } + } + + #[test] + fn get_py_for_non_frozen_class() { + #[crate::pyclass(crate = "crate")] + struct FrozenClass { + #[pyo3(get)] + value: Py, + } + + let mut methods = Vec::new(); + let mut slots = Vec::new(); + + for items in FrozenClass::items_iter() { + methods.extend(items.methods.iter().map(|m| match m { + MaybeRuntimePyMethodDef::Static(m) => m.clone(), + MaybeRuntimePyMethodDef::Runtime(r) => r(), + })); + slots.extend_from_slice(items.slots); + } + + assert_eq!(methods.len(), 1); + assert!(slots.is_empty()); + + match methods.first() { + Some(PyMethodDefType::Getter(getter)) => { + assert_eq!(getter.name, ffi::c_str!("value")); + assert_eq!(getter.doc, ffi::c_str!("")); + // tests for the function pointer are in test_getter_setter.py + } + _ => panic!("Expected a StructMember"), + } + } +} diff --git a/src/impl_/pyclass/lazy_type_object.rs b/src/impl_/pyclass/lazy_type_object.rs index 7afaec8a99b..08a5f17d4bd 100644 --- a/src/impl_/pyclass/lazy_type_object.rs +++ b/src/impl_/pyclass/lazy_type_object.rs @@ -8,6 +8,7 @@ use std::{ use crate::{ exceptions::PyRuntimeError, ffi, + impl_::pyclass::MaybeRuntimePyMethodDef, pyclass::{create_type_object, PyClassTypeObject}, sync::{GILOnceCell, GILProtected}, types::PyType, @@ -149,7 +150,15 @@ impl LazyTypeObjectInner { let mut items = vec![]; for class_items in items_iter { for def in class_items.methods { - if let PyMethodDefType::ClassAttribute(attr) = def { + let built_method; + let method = match def { + MaybeRuntimePyMethodDef::Runtime(builder) => { + built_method = builder(); + &built_method + } + MaybeRuntimePyMethodDef::Static(method) => method, + }; + if let PyMethodDefType::ClassAttribute(attr) = method { match (attr.meth)(py) { Ok(val) => items.push((attr.name, val)), Err(err) => { diff --git a/src/impl_/pymethods.rs b/src/impl_/pymethods.rs index 2b63d1e4ae8..60b655e5647 100644 --- a/src/impl_/pymethods.rs +++ b/src/impl_/pymethods.rs @@ -52,6 +52,7 @@ impl IPowModulo { /// `PyMethodDefType` represents different types of Python callable objects. /// It is used by the `#[pymethods]` attribute. +#[cfg_attr(test, derive(Clone))] pub enum PyMethodDefType { /// Represents class method Class(PyMethodDef), @@ -65,6 +66,8 @@ pub enum PyMethodDefType { Getter(PyGetterDef), /// Represents setter descriptor, used by `#[setter]` Setter(PySetterDef), + /// Represents a struct member + StructMember(ffi::PyMemberDef), } #[derive(Copy, Clone, Debug)] diff --git a/src/pycell/impl_.rs b/src/pycell/impl_.rs index 5404464caba..efc057e74f7 100644 --- a/src/pycell/impl_.rs +++ b/src/pycell/impl_.rs @@ -253,7 +253,7 @@ where #[repr(C)] pub struct PyClassObject { pub(crate) ob_base: ::LayoutAsBase, - contents: PyClassObjectContents, + pub(crate) contents: PyClassObjectContents, } #[repr(C)] diff --git a/src/pyclass.rs b/src/pyclass.rs index 162ae0d3119..29cd1251974 100644 --- a/src/pyclass.rs +++ b/src/pyclass.rs @@ -213,10 +213,16 @@ pub mod boolean_struct { use super::*; /// A way to "seal" the boolean traits. - pub trait Boolean {} + pub trait Boolean { + const VALUE: bool; + } - impl Boolean for True {} - impl Boolean for False {} + impl Boolean for True { + const VALUE: bool = true; + } + impl Boolean for False { + const VALUE: bool = false; + } } pub struct True(()); diff --git a/src/pyclass/create_type_object.rs b/src/pyclass/create_type_object.rs index fd1d2b34998..00dc7ee36f8 100644 --- a/src/pyclass/create_type_object.rs +++ b/src/pyclass/create_type_object.rs @@ -5,7 +5,7 @@ use crate::{ pycell::PyClassObject, pyclass::{ assign_sequence_item_from_mapping, get_sequence_item_from_mapping, tp_dealloc, - tp_dealloc_with_gc, PyClassItemsIter, + tp_dealloc_with_gc, MaybeRuntimePyMethodDef, PyClassItemsIter, }, pymethods::{Getter, Setter}, trampoline::trampoline, @@ -52,6 +52,7 @@ where PyTypeBuilder { slots: Vec::new(), method_defs: Vec::new(), + member_defs: Vec::new(), getset_builders: HashMap::new(), cleanup: Vec::new(), tp_base: base, @@ -102,6 +103,7 @@ type PyTypeBuilderCleanup = Box; struct PyTypeBuilder { slots: Vec, method_defs: Vec, + member_defs: Vec, getset_builders: HashMap<&'static CStr, GetSetDefBuilder>, /// Used to patch the type objects for the things there's no /// PyType_FromSpec API for... there's no reason this should work, @@ -187,6 +189,7 @@ impl PyTypeBuilder { | PyMethodDefType::Static(def) => self.method_defs.push(def.as_method_def()), // These class attributes are added after the type gets created by LazyStaticType PyMethodDefType::ClassAttribute(_) => {} + PyMethodDefType::StructMember(def) => self.member_defs.push(*def), } } @@ -195,6 +198,10 @@ impl PyTypeBuilder { // Safety: Py_tp_methods expects a raw vec of PyMethodDef unsafe { self.push_raw_vec_slot(ffi::Py_tp_methods, method_defs) }; + let member_defs = std::mem::take(&mut self.member_defs); + // Safety: Py_tp_members expects a raw vec of PyMemberDef + unsafe { self.push_raw_vec_slot(ffi::Py_tp_members, member_defs) }; + let mut getset_destructors = Vec::with_capacity(self.getset_builders.len()); #[allow(unused_mut)] @@ -261,7 +268,7 @@ impl PyTypeBuilder { }); } - // Safety: Py_tp_members expects a raw vec of PyGetSetDef + // Safety: Py_tp_getset expects a raw vec of PyGetSetDef unsafe { self.push_raw_vec_slot(ffi::Py_tp_getset, property_defs) }; // If mapping methods implemented, define sequence methods get implemented too. @@ -310,6 +317,14 @@ impl PyTypeBuilder { self.push_slot(slot.slot, slot.pfunc); } for method in items.methods { + let built_method; + let method = match method { + MaybeRuntimePyMethodDef::Runtime(builder) => { + built_method = builder(); + &built_method + } + MaybeRuntimePyMethodDef::Static(method) => method, + }; self.pymethod_def(method); } } @@ -360,23 +375,19 @@ impl PyTypeBuilder { } } - let mut members = Vec::new(); - // __dict__ support if let Some(dict_offset) = dict_offset { - members.push(offset_def(ffi::c_str!("__dictoffset__"), dict_offset)); + self.member_defs + .push(offset_def(ffi::c_str!("__dictoffset__"), dict_offset)); } // weakref support if let Some(weaklist_offset) = weaklist_offset { - members.push(offset_def( + self.member_defs.push(offset_def( ffi::c_str!("__weaklistoffset__"), weaklist_offset, )); } - - // Safety: Py_tp_members expects a raw vec of PyMemberDef - unsafe { self.push_raw_vec_slot(ffi::Py_tp_members, members) }; } // Setting buffer protocols, tp_dictoffset and tp_weaklistoffset via slots doesn't work until diff --git a/tests/test_class_basics.rs b/tests/test_class_basics.rs index 19547cffba9..c1a4b923225 100644 --- a/tests/test_class_basics.rs +++ b/tests/test_class_basics.rs @@ -202,7 +202,6 @@ fn empty_class_in_module() { }); } -#[cfg(feature = "py-clone")] #[pyclass] struct ClassWithObjectField { // It used to be that PyObject was not supported with (get, set) @@ -211,7 +210,6 @@ struct ClassWithObjectField { value: PyObject, } -#[cfg(feature = "py-clone")] #[pymethods] impl ClassWithObjectField { #[new] @@ -220,7 +218,6 @@ impl ClassWithObjectField { } } -#[cfg(feature = "py-clone")] #[test] fn class_with_object_field() { Python::with_gil(|py| { diff --git a/tests/test_class_conversion.rs b/tests/test_class_conversion.rs index a46132b9586..ede8928f865 100644 --- a/tests/test_class_conversion.rs +++ b/tests/test_class_conversion.rs @@ -54,14 +54,12 @@ impl SubClass { } } -#[cfg(feature = "py-clone")] #[pyclass] struct PolymorphicContainer { #[pyo3(get, set)] inner: Py, } -#[cfg(feature = "py-clone")] #[test] fn test_polymorphic_container_stores_base_class() { Python::with_gil(|py| { @@ -78,7 +76,6 @@ fn test_polymorphic_container_stores_base_class() { }); } -#[cfg(feature = "py-clone")] #[test] fn test_polymorphic_container_stores_sub_class() { Python::with_gil(|py| { @@ -106,7 +103,6 @@ fn test_polymorphic_container_stores_sub_class() { }); } -#[cfg(feature = "py-clone")] #[test] fn test_polymorphic_container_does_not_accept_other_types() { Python::with_gil(|py| { diff --git a/tests/test_getter_setter.rs b/tests/test_getter_setter.rs index b0b15d78cd0..e2b8307fd32 100644 --- a/tests/test_getter_setter.rs +++ b/tests/test_getter_setter.rs @@ -258,3 +258,25 @@ fn borrowed_value_with_lifetime_of_self() { py_run!(py, inst, "assert inst.value == 'value'"); }); } + +#[test] +fn frozen_py_field_get() { + #[pyclass(frozen)] + struct FrozenPyField { + #[pyo3(get)] + value: Py, + } + + Python::with_gil(|py| { + let inst = Py::new( + py, + FrozenPyField { + value: "value".into_py(py), + }, + ) + .unwrap() + .to_object(py); + + py_run!(py, inst, "assert inst.value == 'value'"); + }); +} diff --git a/tests/test_methods.rs b/tests/test_methods.rs index 615e2dba0af..ddb3c01b6b8 100644 --- a/tests/test_methods.rs +++ b/tests/test_methods.rs @@ -874,7 +874,6 @@ fn test_from_sequence() { }); } -#[cfg(feature = "py-clone")] #[pyclass] struct r#RawIdents { #[pyo3(get, set)] @@ -883,7 +882,6 @@ struct r#RawIdents { r#subsubtype: PyObject, } -#[cfg(feature = "py-clone")] #[pymethods] impl r#RawIdents { #[new] @@ -901,8 +899,8 @@ impl r#RawIdents { } #[getter(r#subtype)] - pub fn r#get_subtype(&self) -> PyObject { - self.r#subtype.clone() + pub fn r#get_subtype(&self, py: Python<'_>) -> PyObject { + self.r#subtype.clone_ref(py) } #[setter(r#subtype)] @@ -911,8 +909,8 @@ impl r#RawIdents { } #[getter] - pub fn r#get_subsubtype(&self) -> PyObject { - self.r#subsubtype.clone() + pub fn r#get_subsubtype(&self, py: Python<'_>) -> PyObject { + self.r#subsubtype.clone_ref(py) } #[setter] @@ -948,7 +946,6 @@ impl r#RawIdents { } } -#[cfg(feature = "py-clone")] #[test] fn test_raw_idents() { Python::with_gil(|py| { diff --git a/tests/test_no_imports.rs b/tests/test_no_imports.rs index 89d54f4e057..3509a11f4be 100644 --- a/tests/test_no_imports.rs +++ b/tests/test_no_imports.rs @@ -143,14 +143,12 @@ fn test_basic() { }); } -#[cfg(feature = "py-clone")] #[pyo3::pyclass] struct NewClassMethod { #[pyo3(get)] cls: pyo3::PyObject, } -#[cfg(feature = "py-clone")] #[pyo3::pymethods] impl NewClassMethod { #[new] @@ -162,7 +160,6 @@ impl NewClassMethod { } } -#[cfg(feature = "py-clone")] #[test] fn test_new_class_method() { pyo3::Python::with_gil(|py| { diff --git a/tests/test_sequence.rs b/tests/test_sequence.rs index 8c29205cc18..8b1e3114797 100644 --- a/tests/test_sequence.rs +++ b/tests/test_sequence.rs @@ -248,14 +248,12 @@ fn test_inplace_repeat() { // Check that #[pyo3(get, set)] works correctly for Vec -#[cfg(feature = "py-clone")] #[pyclass] struct GenericList { #[pyo3(get, set)] items: Vec, } -#[cfg(feature = "py-clone")] #[test] fn test_generic_list_get() { Python::with_gil(|py| { @@ -268,7 +266,6 @@ fn test_generic_list_get() { }); } -#[cfg(feature = "py-clone")] #[test] fn test_generic_list_set() { Python::with_gil(|py| { diff --git a/tests/ui/invalid_property_args.rs b/tests/ui/invalid_property_args.rs index b5eba27eb60..f35367df7aa 100644 --- a/tests/ui/invalid_property_args.rs +++ b/tests/ui/invalid_property_args.rs @@ -39,4 +39,10 @@ struct MultipleName(#[pyo3(name = "foo", name = "bar")] i32); #[pyclass] struct NameWithoutGetSet(#[pyo3(name = "value")] i32); +#[pyclass] +struct InvalidGetterType { + #[pyo3(get)] + value: ::std::marker::PhantomData, +} + fn main() {} diff --git a/tests/ui/invalid_property_args.stderr b/tests/ui/invalid_property_args.stderr index dea2e3fb2b4..0ee00cc6430 100644 --- a/tests/ui/invalid_property_args.stderr +++ b/tests/ui/invalid_property_args.stderr @@ -45,3 +45,21 @@ error: `name` is useless without `get` or `set` | 40 | struct NameWithoutGetSet(#[pyo3(name = "value")] i32); | ^^^^^^^^^^^^^^ + +error[E0277]: `PhantomData` cannot be converted to a Python object + --> tests/ui/invalid_property_args.rs:45:12 + | +45 | value: ::std::marker::PhantomData, + | ^ required by `#[pyo3(get)]` to create a readable property from a field of type `PhantomData` + | + = help: the trait `IntoPy>` is not implemented for `PhantomData`, which is required by `PhantomData: PyO3GetField` + = note: implement `ToPyObject` or `IntoPy + Clone` for `PhantomData` to define the conversion + = note: required for `PhantomData` to implement `PyO3GetField` +note: required by a bound in `PyClassGetterGenerator::::generate` + --> src/impl_/pyclass.rs + | + | pub const fn generate(&self, name: &'static CStr, doc: &'static CStr) -> PyMethodDefType + | -------- required by a bound in this associated function +... + | FieldT: PyO3GetField, + | ^^^^^^^^^^^^ required by this bound in `PyClassGetterGenerator::::generate` From c4d18e5ee3791b5a4ac9e24ee5370de436e40550 Mon Sep 17 00:00:00 2001 From: Weijie Guo Date: Sat, 22 Jun 2024 22:01:33 +0800 Subject: [PATCH 126/495] Change `search_lib_dir`'s return type to Result (#4043) * Change `search_lib_dir`'s return type to Result * add changelog * add coverage and a hint to `PYO3_CROSS_LIB_DIR` --------- Co-authored-by: David Hewitt --- newsfragments/4043.fixed.md | 1 + pyo3-build-config/src/impl_.rs | 52 +++++++++++++++++++++++++++------- 2 files changed, 42 insertions(+), 11 deletions(-) create mode 100644 newsfragments/4043.fixed.md diff --git a/newsfragments/4043.fixed.md b/newsfragments/4043.fixed.md new file mode 100644 index 00000000000..7653eb295bf --- /dev/null +++ b/newsfragments/4043.fixed.md @@ -0,0 +1 @@ +Don't panic when `PYO3_CROSS_LIB_DIR` is set to a missing path. \ No newline at end of file diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index 1c50c842b91..3dc1e912447 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -1206,7 +1206,7 @@ fn ends_with(entry: &DirEntry, pat: &str) -> bool { /// Returns `None` if the library directory is not available, and a runtime error /// when no or multiple sysconfigdata files are found. fn find_sysconfigdata(cross: &CrossCompileConfig) -> Result> { - let mut sysconfig_paths = find_all_sysconfigdata(cross); + let mut sysconfig_paths = find_all_sysconfigdata(cross)?; if sysconfig_paths.is_empty() { if let Some(lib_dir) = cross.lib_dir.as_ref() { bail!("Could not find _sysconfigdata*.py in {}", lib_dir.display()); @@ -1269,11 +1269,16 @@ fn find_sysconfigdata(cross: &CrossCompileConfig) -> Result> { /// /// Returns an empty vector when the target Python library directory /// is not set via `PYO3_CROSS_LIB_DIR`. -pub fn find_all_sysconfigdata(cross: &CrossCompileConfig) -> Vec { +pub fn find_all_sysconfigdata(cross: &CrossCompileConfig) -> Result> { let sysconfig_paths = if let Some(lib_dir) = cross.lib_dir.as_ref() { - search_lib_dir(lib_dir, cross) + search_lib_dir(lib_dir, cross).with_context(|| { + format!( + "failed to search the lib dir at 'PYO3_CROSS_LIB_DIR={}'", + lib_dir.display() + ) + })? } else { - return Vec::new(); + return Ok(Vec::new()); }; let sysconfig_name = env_var("_PYTHON_SYSCONFIGDATA_NAME"); @@ -1291,7 +1296,7 @@ pub fn find_all_sysconfigdata(cross: &CrossCompileConfig) -> Vec { sysconfig_paths.sort(); sysconfig_paths.dedup(); - sysconfig_paths + Ok(sysconfig_paths) } fn is_pypy_lib_dir(path: &str, v: &Option) -> bool { @@ -1322,9 +1327,14 @@ fn is_cpython_lib_dir(path: &str, v: &Option) -> bool { } /// recursive search for _sysconfigdata, returns all possibilities of sysconfigdata paths -fn search_lib_dir(path: impl AsRef, cross: &CrossCompileConfig) -> Vec { +fn search_lib_dir(path: impl AsRef, cross: &CrossCompileConfig) -> Result> { let mut sysconfig_paths = vec![]; - for f in fs::read_dir(path).expect("Path does not exist") { + for f in fs::read_dir(path.as_ref()).with_context(|| { + format!( + "failed to list the entries in '{}'", + path.as_ref().display() + ) + })? { sysconfig_paths.extend(match &f { // Python 3.7+ sysconfigdata with platform specifics Ok(f) if starts_with(f, "_sysconfigdata_") && ends_with(f, "py") => vec![f.path()], @@ -1332,7 +1342,7 @@ fn search_lib_dir(path: impl AsRef, cross: &CrossCompileConfig) -> Vec, cross: &CrossCompileConfig) -> Vec, cross: &CrossCompileConfig) -> Vec Date: Sat, 22 Jun 2024 16:08:57 -0600 Subject: [PATCH 127/495] Implement PartialEq for PyBytes and [u8] (#4259) * Copy pasta implementation from types/string.rs * changelog * I think I don't need a special implementation for 3.10 or ABI * Copy pasta tests * Fix comment with correct type Co-authored-by: Icxolu <10486322+Icxolu@users.noreply.github.com> * Fix implementation * Use slice in tests * Try renaming changelog file * Fix doc example * Fix doc example Co-authored-by: Icxolu <10486322+Icxolu@users.noreply.github.com> --------- Co-authored-by: Icxolu <10486322+Icxolu@users.noreply.github.com> --- newsfragments/4250.added.md | 1 + .../{4245.added.md => 4259.added.md} | 0 src/types/bytes.rs | 159 ++++++++++++++++++ 3 files changed, 160 insertions(+) create mode 100644 newsfragments/4250.added.md rename newsfragments/{4245.added.md => 4259.added.md} (100%) diff --git a/newsfragments/4250.added.md b/newsfragments/4250.added.md new file mode 100644 index 00000000000..bc179d120a3 --- /dev/null +++ b/newsfragments/4250.added.md @@ -0,0 +1 @@ +Implement `PartialEq` for `Bound<'py, PyBytes>`. diff --git a/newsfragments/4245.added.md b/newsfragments/4259.added.md similarity index 100% rename from newsfragments/4245.added.md rename to newsfragments/4259.added.md diff --git a/src/types/bytes.rs b/src/types/bytes.rs index 661c3022183..0513f4cec8c 100644 --- a/src/types/bytes.rs +++ b/src/types/bytes.rs @@ -11,6 +11,35 @@ use std::str; /// Represents a Python `bytes` object. /// /// This type is immutable. +/// +/// # Equality +/// +/// For convenience, [`Bound<'py, PyBytes>`] implements [`PartialEq<[u8]>`] to allow comparing the +/// data in the Python bytes to a Rust `[u8]`. +/// +/// This is not always the most appropriate way to compare Python bytes, as Python bytes subclasses +/// may have different equality semantics. In situations where subclasses overriding equality might be +/// relevant, use [`PyAnyMethods::eq`], at cost of the additional overhead of a Python method call. +/// +/// ```rust +/// # use pyo3::prelude::*; +/// use pyo3::types::PyBytes; +/// +/// # Python::with_gil(|py| { +/// let py_bytes = PyBytes::new_bound(py, b"foo".as_slice()); +/// // via PartialEq<[u8]> +/// assert_eq!(py_bytes, b"foo".as_slice()); +/// +/// // via Python equality +/// let other = PyBytes::new_bound(py, b"foo".as_slice()); +/// assert!(py_bytes.as_any().eq(other).unwrap()); +/// +/// // Note that `eq` will convert it's argument to Python using `ToPyObject`, +/// // so the following does not compare equal since the slice will convert into a +/// // `list`, not a `bytes` object. +/// assert!(!py_bytes.as_any().eq(b"foo".as_slice()).unwrap()); +/// # }); +/// ``` #[repr(transparent)] pub struct PyBytes(PyAny); @@ -191,6 +220,106 @@ impl> Index for Bound<'_, PyBytes> { } } +/// Compares whether the Python bytes object is equal to the [u8]. +/// +/// In some cases Python equality might be more appropriate; see the note on [`PyBytes`]. +impl PartialEq<[u8]> for Bound<'_, PyBytes> { + #[inline] + fn eq(&self, other: &[u8]) -> bool { + self.as_borrowed() == *other + } +} + +/// Compares whether the Python bytes object is equal to the [u8]. +/// +/// In some cases Python equality might be more appropriate; see the note on [`PyBytes`]. +impl PartialEq<&'_ [u8]> for Bound<'_, PyBytes> { + #[inline] + fn eq(&self, other: &&[u8]) -> bool { + self.as_borrowed() == **other + } +} + +/// Compares whether the Python bytes object is equal to the [u8]. +/// +/// In some cases Python equality might be more appropriate; see the note on [`PyBytes`]. +impl PartialEq> for [u8] { + #[inline] + fn eq(&self, other: &Bound<'_, PyBytes>) -> bool { + *self == other.as_borrowed() + } +} + +/// Compares whether the Python bytes object is equal to the [u8]. +/// +/// In some cases Python equality might be more appropriate; see the note on [`PyBytes`]. +impl PartialEq<&'_ Bound<'_, PyBytes>> for [u8] { + #[inline] + fn eq(&self, other: &&Bound<'_, PyBytes>) -> bool { + *self == other.as_borrowed() + } +} + +/// Compares whether the Python bytes object is equal to the [u8]. +/// +/// In some cases Python equality might be more appropriate; see the note on [`PyBytes`]. +impl PartialEq> for &'_ [u8] { + #[inline] + fn eq(&self, other: &Bound<'_, PyBytes>) -> bool { + **self == other.as_borrowed() + } +} + +/// Compares whether the Python bytes object is equal to the [u8]. +/// +/// In some cases Python equality might be more appropriate; see the note on [`PyBytes`]. +impl PartialEq<[u8]> for &'_ Bound<'_, PyBytes> { + #[inline] + fn eq(&self, other: &[u8]) -> bool { + self.as_borrowed() == other + } +} + +/// Compares whether the Python bytes object is equal to the [u8]. +/// +/// In some cases Python equality might be more appropriate; see the note on [`PyBytes`]. +impl PartialEq<[u8]> for Borrowed<'_, '_, PyBytes> { + #[inline] + fn eq(&self, other: &[u8]) -> bool { + self.as_bytes() == other + } +} + +/// Compares whether the Python bytes object is equal to the [u8]. +/// +/// In some cases Python equality might be more appropriate; see the note on [`PyBytes`]. +impl PartialEq<&[u8]> for Borrowed<'_, '_, PyBytes> { + #[inline] + fn eq(&self, other: &&[u8]) -> bool { + *self == **other + } +} + +/// Compares whether the Python bytes object is equal to the [u8]. +/// +/// In some cases Python equality might be more appropriate; see the note on [`PyBytes`]. +impl PartialEq> for [u8] { + #[inline] + fn eq(&self, other: &Borrowed<'_, '_, PyBytes>) -> bool { + other == self + } +} + +/// Compares whether the Python bytes object is equal to the [u8]. +/// +/// In some cases Python equality might be more appropriate; see the note on [`PyBytes`]. +impl PartialEq> for &'_ [u8] { + #[inline] + fn eq(&self, other: &Borrowed<'_, '_, PyBytes>) -> bool { + other == self + } +} + #[cfg(test)] mod tests { use super::*; @@ -251,4 +380,34 @@ mod tests { .is_instance_of::(py)); }); } + + #[test] + fn test_comparisons() { + Python::with_gil(|py| { + let b = b"hello, world".as_slice(); + let py_bytes = PyBytes::new_bound(py, b); + + assert_eq!(py_bytes, b"hello, world".as_slice()); + + assert_eq!(py_bytes, b); + assert_eq!(&py_bytes, b); + assert_eq!(b, py_bytes); + assert_eq!(b, &py_bytes); + + assert_eq!(py_bytes, *b); + assert_eq!(&py_bytes, *b); + assert_eq!(*b, py_bytes); + assert_eq!(*b, &py_bytes); + + let py_string = py_bytes.as_borrowed(); + + assert_eq!(py_string, b); + assert_eq!(&py_string, b); + assert_eq!(b, py_string); + assert_eq!(b, &py_string); + + assert_eq!(py_string, *b); + assert_eq!(*b, py_string); + }) + } } From a2f9399906274ecec61e70bda185aa12e6c564b6 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Sat, 22 Jun 2024 23:09:59 +0100 Subject: [PATCH 128/495] make datetime from timestamp tests compare against Python result (#4275) * attemp to fix range for st.datetime * remove example * try fixing to utc * make datetime from timestamp tests compare against Python result --------- Co-authored-by: Cheukting --- pytests/tests/test_datetime.py | 46 ++++++++++++++++++++++++---------- 1 file changed, 33 insertions(+), 13 deletions(-) diff --git a/pytests/tests/test_datetime.py b/pytests/tests/test_datetime.py index c81d13a929a..6484b926a96 100644 --- a/pytests/tests/test_datetime.py +++ b/pytests/tests/test_datetime.py @@ -56,11 +56,11 @@ def tzname(self, dt): IS_WINDOWS = sys.platform == "win32" if IS_WINDOWS: - MIN_DATETIME = pdt.datetime(1971, 1, 2, 0, 0) + MIN_DATETIME = pdt.datetime(1970, 1, 1, 0, 0, 0) if IS_32_BIT: - MAX_DATETIME = pdt.datetime(3001, 1, 19, 4, 59, 59) + MAX_DATETIME = pdt.datetime(2038, 1, 18, 23, 59, 59) else: - MAX_DATETIME = pdt.datetime(3001, 1, 19, 7, 59, 59) + MAX_DATETIME = pdt.datetime(3000, 12, 31, 23, 59, 59) else: if IS_32_BIT: # TS ±2147483648 (2**31) @@ -93,11 +93,21 @@ def test_invalid_date_fails(): @given(d=st.dates(MIN_DATETIME.date(), MAX_DATETIME.date())) def test_date_from_timestamp(d): - if PYPY and d < pdt.date(1900, 1, 1): - pytest.xfail("pdt.datetime.timestamp will raise on PyPy with dates before 1900") - - ts = pdt.datetime.timestamp(pdt.datetime.combine(d, pdt.time(0))) - assert rdt.date_from_timestamp(int(ts)) == pdt.date.fromtimestamp(ts) + try: + ts = pdt.datetime.timestamp(d) + except Exception: + # out of range for timestamp + return + + try: + expected = pdt.date.fromtimestamp(ts) + except Exception as pdt_fail: + # date from timestamp failed; expect the same from Rust binding + with pytest.raises(type(pdt_fail)) as exc_info: + rdt.date_from_timestamp(ts) + assert str(exc_info.value) == str(pdt_fail) + else: + assert rdt.date_from_timestamp(int(ts)) == expected @pytest.mark.parametrize( @@ -229,11 +239,21 @@ def test_datetime_typeerror(): @given(dt=st.datetimes(MIN_DATETIME, MAX_DATETIME)) @example(dt=pdt.datetime(1971, 1, 2, 0, 0)) def test_datetime_from_timestamp(dt): - if PYPY and dt < pdt.datetime(1900, 1, 1): - pytest.xfail("pdt.datetime.timestamp will raise on PyPy with dates before 1900") - - ts = pdt.datetime.timestamp(dt) - assert rdt.datetime_from_timestamp(ts) == pdt.datetime.fromtimestamp(ts) + try: + ts = pdt.datetime.timestamp(dt) + except Exception: + # out of range for timestamp + return + + try: + expected = pdt.datetime.fromtimestamp(ts) + except Exception as pdt_fail: + # datetime from timestamp failed; expect the same from Rust binding + with pytest.raises(type(pdt_fail)) as exc_info: + rdt.datetime_from_timestamp(ts) + assert str(exc_info.value) == str(pdt_fail) + else: + assert rdt.datetime_from_timestamp(ts) == expected def test_datetime_from_timestamp_tzinfo(): From c67625d683cbd5aaaa90607b55b588b2176a9f1a Mon Sep 17 00:00:00 2001 From: Aneesh Agrawal Date: Sat, 22 Jun 2024 18:10:27 -0400 Subject: [PATCH 129/495] Revamp PyType name functions to match PEP 737 (#4196) * Revamp PyType name functions to match PEP 737 PyType::name uses `tp_name`, which is not consistent. [PEP 737](https://peps.python.org/pep-0737/) adds a new path forward, so update PyType::name and add PyType::{module,fully_qualified_name} to match the PEP. * refactor conditional code to handle multiple Python versions better * return `Bound<'py, str>` * fixup --------- Co-authored-by: David Hewitt --- guide/src/class/numeric.md | 4 +- guide/src/class/object.md | 6 +- guide/src/migration.md | 58 +++++++++ newsfragments/4196.added.md | 4 + newsfragments/4196.changed.md | 1 + pyo3-ffi/src/object.rs | 8 ++ pytests/src/misc.rs | 12 +- pytests/tests/test_misc.py | 4 +- src/err/mod.rs | 17 ++- src/types/boolobject.rs | 14 ++- src/types/typeobject.rs | 222 +++++++++++++++++++++++++--------- tests/test_methods.rs | 5 +- 12 files changed, 267 insertions(+), 88 deletions(-) create mode 100644 newsfragments/4196.added.md create mode 100644 newsfragments/4196.changed.md diff --git a/guide/src/class/numeric.md b/guide/src/class/numeric.md index 361d2fb6d36..20a1a041450 100644 --- a/guide/src/class/numeric.md +++ b/guide/src/class/numeric.md @@ -210,7 +210,7 @@ use std::hash::{Hash, Hasher}; use pyo3::exceptions::{PyValueError, PyZeroDivisionError}; use pyo3::prelude::*; use pyo3::class::basic::CompareOp; -use pyo3::types::PyComplex; +use pyo3::types::{PyComplex, PyString}; fn wrap(obj: &Bound<'_, PyAny>) -> PyResult { let val = obj.call_method1("__and__", (0xFFFFFFFF_u32,))?; @@ -231,7 +231,7 @@ impl Number { fn __repr__(slf: &Bound<'_, Self>) -> PyResult { // Get the class name dynamically in case `Number` is subclassed - let class_name: String = slf.get_type().qualname()?; + let class_name: Bound<'_, PyString> = slf.get_type().qualname()?; Ok(format!("{}({})", class_name, slf.borrow().0)) } diff --git a/guide/src/class/object.md b/guide/src/class/object.md index c0d25cd0597..e9ea549aab4 100644 --- a/guide/src/class/object.md +++ b/guide/src/class/object.md @@ -80,6 +80,7 @@ the subclass name. This is typically done in Python code by accessing ```rust # use pyo3::prelude::*; +# use pyo3::types::PyString; # # #[pyclass] # struct Number(i32); @@ -88,7 +89,7 @@ the subclass name. This is typically done in Python code by accessing impl Number { fn __repr__(slf: &Bound<'_, Self>) -> PyResult { // This is the equivalent of `self.__class__.__name__` in Python. - let class_name: String = slf.get_type().qualname()?; + let class_name: Bound<'_, PyString> = slf.get_type().qualname()?; // To access fields of the Rust struct, we need to borrow the `PyCell`. Ok(format!("{}({})", class_name, slf.borrow().0)) } @@ -285,6 +286,7 @@ use std::hash::{Hash, Hasher}; use pyo3::prelude::*; use pyo3::class::basic::CompareOp; +use pyo3::types::PyString; #[pyclass] struct Number(i32); @@ -297,7 +299,7 @@ impl Number { } fn __repr__(slf: &Bound<'_, Self>) -> PyResult { - let class_name: String = slf.get_type().qualname()?; + let class_name: Bound<'_, PyString> = slf.get_type().qualname()?; Ok(format!("{}({})", class_name, slf.borrow().0)) } diff --git a/guide/src/migration.md b/guide/src/migration.md index 77f97cf01d2..8ac3ff16c47 100644 --- a/guide/src/migration.md +++ b/guide/src/migration.md @@ -81,6 +81,64 @@ enum SimpleEnum { ```
+### `PyType::name` reworked to better match Python `__name__` +
+Click to expand + +This function previously would try to read directly from Python type objects' C API field (`tp_name`), in which case it +would return a `Cow::Borrowed`. However the contents of `tp_name` don't have well-defined semantics. + +Instead `PyType::name()` now returns the equivalent of Python `__name__` and returns `PyResult>`. + +The closest equivalent to PyO3 0.21's version of `PyType::name()` has been introduced as a new function `PyType::fully_qualified_name()`, +which is equivalent to `__module__` and `__qualname__` joined as `module.qualname`. + +Before: + +```rust,ignore +# #![allow(deprecated, dead_code)] +# use pyo3::prelude::*; +# use pyo3::types::{PyBool}; +# fn main() -> PyResult<()> { +Python::with_gil(|py| { + let bool_type = py.get_type_bound::(); + let name = bool_type.name()?.into_owned(); + println!("Hello, {}", name); + + let mut name_upper = bool_type.name()?; + name_upper.to_mut().make_ascii_uppercase(); + println!("Hello, {}", name_upper); + + Ok(()) +}) +# } +``` + +After: + +```rust +# #![allow(dead_code)] +# use pyo3::prelude::*; +# use pyo3::types::{PyBool}; +# fn main() -> PyResult<()> { +Python::with_gil(|py| { + let bool_type = py.get_type_bound::(); + let name = bool_type.name()?; + println!("Hello, {}", name); + + // (if the full dotted path was desired, switch from `name()` to `fully_qualified_name()`) + let mut name_upper = bool_type.fully_qualified_name()?.to_string(); + name_upper.make_ascii_uppercase(); + println!("Hello, {}", name_upper); + + Ok(()) +}) +# } +``` +
+ + + ## from 0.20.* to 0.21
Click to expand diff --git a/newsfragments/4196.added.md b/newsfragments/4196.added.md new file mode 100644 index 00000000000..d9c295d551c --- /dev/null +++ b/newsfragments/4196.added.md @@ -0,0 +1,4 @@ +Add `PyType::module`, which always matches Python `__module__`. +Add `PyType::fully_qualified_name` which matches the "fully qualified name" +defined in https://peps.python.org/pep-0737 (not exposed in Python), +which is useful for error messages and `repr()` implementations. diff --git a/newsfragments/4196.changed.md b/newsfragments/4196.changed.md new file mode 100644 index 00000000000..4ea69180a2d --- /dev/null +++ b/newsfragments/4196.changed.md @@ -0,0 +1 @@ +Change `PyType::name` to always match Python `__name__`. diff --git a/pyo3-ffi/src/object.rs b/pyo3-ffi/src/object.rs index b33ee558a37..7acd0897217 100644 --- a/pyo3-ffi/src/object.rs +++ b/pyo3-ffi/src/object.rs @@ -261,6 +261,14 @@ extern "C" { #[cfg_attr(PyPy, link_name = "PyPyType_GetQualName")] pub fn PyType_GetQualName(arg1: *mut PyTypeObject) -> *mut PyObject; + #[cfg(Py_3_13)] + #[cfg_attr(PyPy, link_name = "PyPyType_GetFullyQualifiedName")] + pub fn PyType_GetFullyQualifiedName(arg1: *mut PyTypeObject) -> *mut PyObject; + + #[cfg(Py_3_13)] + #[cfg_attr(PyPy, link_name = "PyPyType_GetModuleName")] + pub fn PyType_GetModuleName(arg1: *mut PyTypeObject) -> *mut PyObject; + #[cfg(Py_3_12)] #[cfg_attr(PyPy, link_name = "PyPyType_FromMetaclass")] pub fn PyType_FromMetaclass( diff --git a/pytests/src/misc.rs b/pytests/src/misc.rs index 7704098bd5b..ed9c9333ec2 100644 --- a/pytests/src/misc.rs +++ b/pytests/src/misc.rs @@ -1,5 +1,7 @@ -use pyo3::{prelude::*, types::PyDict}; -use std::borrow::Cow; +use pyo3::{ + prelude::*, + types::{PyDict, PyString}, +}; #[pyfunction] fn issue_219() { @@ -8,8 +10,8 @@ fn issue_219() { } #[pyfunction] -fn get_type_full_name(obj: &Bound<'_, PyAny>) -> PyResult { - obj.get_type().name().map(Cow::into_owned) +fn get_type_fully_qualified_name<'py>(obj: &Bound<'py, PyAny>) -> PyResult> { + obj.get_type().fully_qualified_name() } #[pyfunction] @@ -33,7 +35,7 @@ fn get_item_and_run_callback(dict: Bound<'_, PyDict>, callback: Bound<'_, PyAny> #[pymodule] pub fn misc(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(issue_219, m)?)?; - m.add_function(wrap_pyfunction!(get_type_full_name, m)?)?; + m.add_function(wrap_pyfunction!(get_type_fully_qualified_name, m)?)?; m.add_function(wrap_pyfunction!(accepts_bool, m)?)?; m.add_function(wrap_pyfunction!(get_item_and_run_callback, m)?)?; Ok(()) diff --git a/pytests/tests/test_misc.py b/pytests/tests/test_misc.py index fc8e1095705..7f43fbf11e0 100644 --- a/pytests/tests/test_misc.py +++ b/pytests/tests/test_misc.py @@ -51,11 +51,11 @@ def test_import_in_subinterpreter_forbidden(): subinterpreters.destroy(sub_interpreter) -def test_type_full_name_includes_module(): +def test_type_fully_qualified_name_includes_module(): numpy = pytest.importorskip("numpy") # For numpy 1.x and 2.x - assert pyo3_pytests.misc.get_type_full_name(numpy.bool_(True)) in [ + assert pyo3_pytests.misc.get_type_fully_qualified_name(numpy.bool_(True)) in [ "numpy.bool", "numpy.bool_", ] diff --git a/src/err/mod.rs b/src/err/mod.rs index 6bfe1a6cc99..205145d4e15 100644 --- a/src/err/mod.rs +++ b/src/err/mod.rs @@ -971,16 +971,13 @@ struct PyDowncastErrorArguments { impl PyErrArguments for PyDowncastErrorArguments { fn arguments(self, py: Python<'_>) -> PyObject { - format!( - "'{}' object cannot be converted to '{}'", - self.from - .bind(py) - .qualname() - .as_deref() - .unwrap_or(""), - self.to - ) - .to_object(py) + const FAILED_TO_EXTRACT: Cow<'_, str> = Cow::Borrowed(""); + let from = self.from.bind(py).qualname(); + let from = match &from { + Ok(qn) => qn.to_cow().unwrap_or(FAILED_TO_EXTRACT), + Err(_) => FAILED_TO_EXTRACT, + }; + format!("'{}' object cannot be converted to '{}'", from, self.to).to_object(py) } } diff --git a/src/types/boolobject.rs b/src/types/boolobject.rs index 52465ef305f..04c1fd4c113 100644 --- a/src/types/boolobject.rs +++ b/src/types/boolobject.rs @@ -111,11 +111,15 @@ impl FromPyObject<'_> for bool { Err(err) => err, }; - if obj - .get_type() - .name() - .map_or(false, |name| name == "numpy.bool_" || name == "numpy.bool") - { + let is_numpy_bool = { + let ty = obj.get_type(); + ty.module().map_or(false, |module| module == "numpy") + && ty + .name() + .map_or(false, |name| name == "bool_" || name == "bool") + }; + + if is_numpy_bool { let missing_conversion = |obj: &Bound<'_, PyAny>| { PyTypeError::new_err(format!( "object of type '{}' does not define a '__bool__' conversion", diff --git a/src/types/typeobject.rs b/src/types/typeobject.rs index 9c2d5c5f2c4..9638a2731a3 100644 --- a/src/types/typeobject.rs +++ b/src/types/typeobject.rs @@ -1,13 +1,14 @@ use crate::err::{self, PyResult}; use crate::instance::Borrowed; +#[cfg(not(Py_3_13))] +use crate::pybacked::PyBackedStr; use crate::types::any::PyAnyMethods; use crate::types::PyTuple; #[cfg(feature = "gil-refs")] use crate::PyNativeType; use crate::{ffi, Bound, PyAny, PyTypeInfo, Python}; -use std::borrow::Cow; -#[cfg(not(any(Py_LIMITED_API, PyPy)))] -use std::ffi::CStr; + +use super::PyString; /// Represents a reference to a Python `type object`. #[repr(transparent)] pub struct PyType(PyAny); @@ -71,16 +72,19 @@ impl PyType { Self::from_borrowed_type_ptr(py, p).into_gil_ref() } - /// Gets the [qualified name](https://docs.python.org/3/glossary.html#term-qualified-name) of the `PyType`. - pub fn qualname(&self) -> PyResult { - self.as_borrowed().qualname() + /// Gets the name of the `PyType`. Equivalent to `self.__name__` in Python. + pub fn name(&self) -> PyResult<&PyString> { + self.as_borrowed().name().map(Bound::into_gil_ref) } - /// Gets the full name, which includes the module, of the `PyType`. - pub fn name(&self) -> PyResult> { - self.as_borrowed().name() + /// Gets the [qualified name](https://docs.python.org/3/glossary.html#term-qualified-name) of the `PyType`. + /// Equivalent to `self.__qualname__` in Python. + pub fn qualname(&self) -> PyResult<&PyString> { + self.as_borrowed().qualname().map(Bound::into_gil_ref) } + // `module` and `fully_qualified_name` intentionally omitted + /// Checks whether `self` is a subclass of `other`. /// /// Equivalent to the Python expression `issubclass(self, other)`. @@ -110,11 +114,18 @@ pub trait PyTypeMethods<'py>: crate::sealed::Sealed { /// Retrieves the underlying FFI pointer associated with this Python object. fn as_type_ptr(&self) -> *mut ffi::PyTypeObject; - /// Gets the full name, which includes the module, of the `PyType`. - fn name(&self) -> PyResult>; + /// Gets the name of the `PyType`. Equivalent to `self.__name__` in Python. + fn name(&self) -> PyResult>; /// Gets the [qualified name](https://docs.python.org/3/glossary.html#term-qualified-name) of the `PyType`. - fn qualname(&self) -> PyResult; + /// Equivalent to `self.__qualname__` in Python. + fn qualname(&self) -> PyResult>; + + /// Gets the name of the module defining the `PyType`. + fn module(&self) -> PyResult>; + + /// Gets the [fully qualified name](https://peps.python.org/pep-0737/#add-pytype-getfullyqualifiedname-function) of the `PyType`. + fn fully_qualified_name(&self) -> PyResult>; /// Checks whether `self` is a subclass of `other`. /// @@ -148,25 +159,82 @@ impl<'py> PyTypeMethods<'py> for Bound<'py, PyType> { } /// Gets the name of the `PyType`. - fn name(&self) -> PyResult> { - Borrowed::from(self).name() + fn name(&self) -> PyResult> { + #[cfg(not(Py_3_11))] + let name = self + .getattr(intern!(self.py(), "__name__"))? + .downcast_into()?; + + #[cfg(Py_3_11)] + let name = unsafe { + use crate::ffi_ptr_ext::FfiPtrExt; + ffi::PyType_GetName(self.as_type_ptr()) + .assume_owned_or_err(self.py())? + // SAFETY: setting `__name__` from Python is required to be a `str` + .downcast_into_unchecked() + }; + + Ok(name) } - fn qualname(&self) -> PyResult { - #[cfg(any(Py_LIMITED_API, PyPy, not(Py_3_11)))] - let name = self.getattr(intern!(self.py(), "__qualname__"))?.extract(); + /// Gets the [qualified name](https://docs.python.org/3/glossary.html#term-qualified-name) of the `PyType`. + fn qualname(&self) -> PyResult> { + #[cfg(not(Py_3_11))] + let name = self + .getattr(intern!(self.py(), "__qualname__"))? + .downcast_into()?; + + #[cfg(Py_3_11)] + let name = unsafe { + use crate::ffi_ptr_ext::FfiPtrExt; + ffi::PyType_GetQualName(self.as_type_ptr()) + .assume_owned_or_err(self.py())? + // SAFETY: setting `__qualname__` from Python is required to be a `str` + .downcast_into_unchecked() + }; - #[cfg(not(any(Py_LIMITED_API, PyPy, not(Py_3_11))))] - let name = { + Ok(name) + } + + /// Gets the name of the module defining the `PyType`. + fn module(&self) -> PyResult> { + #[cfg(not(Py_3_13))] + let name = self.getattr(intern!(self.py(), "__module__"))?; + + #[cfg(Py_3_13)] + let name = unsafe { use crate::ffi_ptr_ext::FfiPtrExt; - let obj = unsafe { - ffi::PyType_GetQualName(self.as_type_ptr()).assume_owned_or_err(self.py())? - }; + ffi::PyType_GetModuleName(self.as_type_ptr()).assume_owned_or_err(self.py())? + }; - obj.extract() + // `__module__` is never guaranteed to be a `str` + name.downcast_into().map_err(Into::into) + } + + /// Gets the [fully qualified name](https://docs.python.org/3/glossary.html#term-qualified-name) of the `PyType`. + fn fully_qualified_name(&self) -> PyResult> { + #[cfg(not(Py_3_13))] + let name = { + let module = self.getattr(intern!(self.py(), "__module__"))?; + let qualname = self.getattr(intern!(self.py(), "__qualname__"))?; + + let module_str = module.extract::()?; + if module_str == "builtins" || module_str == "__main__" { + qualname.downcast_into()? + } else { + PyString::new_bound(self.py(), &format!("{}.{}", module, qualname)) + } }; - name + #[cfg(Py_3_13)] + let name = unsafe { + use crate::ffi_ptr_ext::FfiPtrExt; + ffi::PyType_GetFullyQualifiedName(self.as_type_ptr()) + .assume_owned_or_err(self.py())? + .downcast_into_unchecked() + }; + + Ok(name) } /// Checks whether `self` is a subclass of `other`. @@ -232,43 +300,11 @@ impl<'py> PyTypeMethods<'py> for Bound<'py, PyType> { } } -impl<'a> Borrowed<'a, '_, PyType> { - fn name(self) -> PyResult> { - #[cfg(not(any(Py_LIMITED_API, PyPy)))] - { - let ptr = self.as_type_ptr(); - - let name = unsafe { CStr::from_ptr((*ptr).tp_name) }.to_str()?; - - #[cfg(Py_3_10)] - if unsafe { ffi::PyType_HasFeature(ptr, ffi::Py_TPFLAGS_IMMUTABLETYPE) } != 0 { - return Ok(Cow::Borrowed(name)); - } - - Ok(Cow::Owned(name.to_owned())) - } - - #[cfg(any(Py_LIMITED_API, PyPy))] - { - let module = self.getattr(intern!(self.py(), "__module__"))?; - - #[cfg(not(Py_3_11))] - let name = self.getattr(intern!(self.py(), "__name__"))?; - - #[cfg(Py_3_11)] - let name = { - use crate::ffi_ptr_ext::FfiPtrExt; - unsafe { ffi::PyType_GetName(self.as_type_ptr()).assume_owned_or_err(self.py())? } - }; - - Ok(Cow::Owned(format!("{}.{}", module, name))) - } - } -} - #[cfg(test)] mod tests { - use crate::types::{PyAnyMethods, PyBool, PyInt, PyLong, PyTuple, PyTypeMethods}; + use crate::types::{ + PyAnyMethods, PyBool, PyInt, PyLong, PyModule, PyTuple, PyType, PyTypeMethods, + }; use crate::PyAny; use crate::Python; @@ -330,4 +366,72 @@ mod tests { .unwrap()); }); } + + #[test] + fn test_type_names_standard() { + Python::with_gil(|py| { + let module = PyModule::from_code_bound( + py, + r#" +class MyClass: + pass +"#, + file!(), + "test_module", + ) + .expect("module create failed"); + + let my_class = module.getattr("MyClass").unwrap(); + let my_class_type = my_class.downcast_into::().unwrap(); + assert_eq!(my_class_type.name().unwrap(), "MyClass"); + assert_eq!(my_class_type.qualname().unwrap(), "MyClass"); + assert_eq!(my_class_type.module().unwrap(), "test_module"); + assert_eq!( + my_class_type.fully_qualified_name().unwrap(), + "test_module.MyClass" + ); + }); + } + + #[test] + fn test_type_names_builtin() { + Python::with_gil(|py| { + let bool_type = py.get_type_bound::(); + assert_eq!(bool_type.name().unwrap(), "bool"); + assert_eq!(bool_type.qualname().unwrap(), "bool"); + assert_eq!(bool_type.module().unwrap(), "builtins"); + assert_eq!(bool_type.fully_qualified_name().unwrap(), "bool"); + }); + } + + #[test] + fn test_type_names_nested() { + Python::with_gil(|py| { + let module = PyModule::from_code_bound( + py, + r#" +class OuterClass: + class InnerClass: + pass +"#, + file!(), + "test_module", + ) + .expect("module create failed"); + + let outer_class = module.getattr("OuterClass").unwrap(); + let inner_class = outer_class.getattr("InnerClass").unwrap(); + let inner_class_type = inner_class.downcast_into::().unwrap(); + assert_eq!(inner_class_type.name().unwrap(), "InnerClass"); + assert_eq!( + inner_class_type.qualname().unwrap(), + "OuterClass.InnerClass" + ); + assert_eq!(inner_class_type.module().unwrap(), "test_module"); + assert_eq!( + inner_class_type.fully_qualified_name().unwrap(), + "test_module.OuterClass.InnerClass" + ); + }); + } } diff --git a/tests/test_methods.rs b/tests/test_methods.rs index ddb3c01b6b8..40893826f39 100644 --- a/tests/test_methods.rs +++ b/tests/test_methods.rs @@ -78,9 +78,8 @@ impl ClassMethod { } #[classmethod] - fn method_owned(cls: Py) -> PyResult { - let qualname = Python::with_gil(|gil| cls.bind(gil).qualname())?; - Ok(format!("{}.method_owned()!", qualname)) + fn method_owned(cls: Py, py: Python<'_>) -> PyResult { + Ok(format!("{}.method_owned()!", cls.bind(py).qualname()?)) } } From 6a0221ba2c59fe2c498899868a745cc19ec96cdb Mon Sep 17 00:00:00 2001 From: Bruno Kolenbrander <59372212+mejrs@users.noreply.github.com> Date: Sun, 23 Jun 2024 12:36:19 +0200 Subject: [PATCH 130/495] document FnType and refactor FnType::self_arg (#4276) --- pyo3-macros-backend/src/method.rs | 58 +++++++++++++++++-------------- 1 file changed, 32 insertions(+), 26 deletions(-) diff --git a/pyo3-macros-backend/src/method.rs b/pyo3-macros-backend/src/method.rs index 9e4b1ad13d5..cd06a92c0f7 100644 --- a/pyo3-macros-backend/src/method.rs +++ b/pyo3-macros-backend/src/method.rs @@ -192,16 +192,26 @@ fn handle_argument_error(pat: &syn::Pat) -> syn::Error { syn::Error::new(span, msg) } +/// Represents what kind of a function a pyfunction or pymethod is #[derive(Clone, Debug)] pub enum FnType { + /// Represents a pymethod annotated with `#[getter]` Getter(SelfType), + /// Represents a pymethod annotated with `#[setter]` Setter(SelfType), + /// Represents a regular pymethod Fn(SelfType), + /// Represents a pymethod annotated with `#[new]`, i.e. the `__new__` dunder. FnNew, + /// Represents a pymethod annotated with both `#[new]` and `#[classmethod]` (in either order) FnNewClass(Span), + /// Represents a pymethod annotated with `#[classmethod]`, like a `@classmethod` FnClass(Span), + /// Represents a pyfunction or a pymethod annotated with `#[staticmethod]`, like a `@staticmethod` FnStatic, + /// Represents a pyfunction annotated with `#[pyo3(pass_module)] FnModule(Span), + /// Represents a pymethod or associated constant annotated with `#[classattr]` ClassAttribute, } @@ -224,7 +234,7 @@ impl FnType { error_mode: ExtractErrorMode, holders: &mut Holders, ctx: &Ctx, - ) -> TokenStream { + ) -> Option { let Ctx { pyo3_path, .. } = ctx; match self { FnType::Getter(st) | FnType::Setter(st) | FnType::Fn(st) => { @@ -235,35 +245,35 @@ impl FnType { ctx, ); syn::Token![,](Span::call_site()).to_tokens(&mut receiver); - receiver - } - FnType::FnNew | FnType::FnStatic | FnType::ClassAttribute => { - quote!() + Some(receiver) } FnType::FnClass(span) | FnType::FnNewClass(span) => { let py = syn::Ident::new("py", Span::call_site()); let slf: Ident = syn::Ident::new("_slf_ref", Span::call_site()); let pyo3_path = pyo3_path.to_tokens_spanned(*span); - quote_spanned! { *span => + let ret = quote_spanned! { *span => #[allow(clippy::useless_conversion)] ::std::convert::Into::into( #pyo3_path::impl_::pymethods::BoundRef::ref_from_ptr(#py, &*(#slf as *const _ as *const *mut _)) .downcast_unchecked::<#pyo3_path::types::PyType>() ), - } + }; + Some(ret) } FnType::FnModule(span) => { let py = syn::Ident::new("py", Span::call_site()); let slf: Ident = syn::Ident::new("_slf_ref", Span::call_site()); let pyo3_path = pyo3_path.to_tokens_spanned(*span); - quote_spanned! { *span => + let ret = quote_spanned! { *span => #[allow(clippy::useless_conversion)] ::std::convert::Into::into( #pyo3_path::impl_::pymethods::BoundRef::ref_from_ptr(#py, &*(#slf as *const _ as *const *mut _)) .downcast_unchecked::<#pyo3_path::types::PyModule>() ), - } + }; + Some(ret) } + FnType::FnNew | FnType::FnStatic | FnType::ClassAttribute => None, } } } @@ -658,10 +668,7 @@ impl<'a> FnSpec<'a> { }} } _ => { - let self_arg = self_arg(); - if self_arg.is_empty() { - quote! { function(#(#args),*) } - } else { + if let Some(self_arg) = self_arg() { let self_checker = holders.push_gil_refs_checker(self_arg.span()); quote! { function( @@ -670,6 +677,8 @@ impl<'a> FnSpec<'a> { #(#args),* ) } + } else { + quote! { function(#(#args),*) } } } }; @@ -690,20 +699,17 @@ impl<'a> FnSpec<'a> { }}; } call - } else { - let self_arg = self_arg(); - if self_arg.is_empty() { - quote! { function(#(#args),*) } - } else { - let self_checker = holders.push_gil_refs_checker(self_arg.span()); - quote! { - function( - // NB #self_arg includes a comma, so none inserted here - #pyo3_path::impl_::deprecations::inspect_type(#self_arg &#self_checker), - #(#args),* - ) - } + } else if let Some(self_arg) = self_arg() { + let self_checker = holders.push_gil_refs_checker(self_arg.span()); + quote! { + function( + // NB #self_arg includes a comma, so none inserted here + #pyo3_path::impl_::deprecations::inspect_type(#self_arg &#self_checker), + #(#args),* + ) } + } else { + quote! { function(#(#args),*) } }; // We must assign the output_span to the return value of the call, From 91d8683814afbff4aef50d65fc912f73f9273e1a Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Mon, 24 Jun 2024 14:38:33 +0100 Subject: [PATCH 131/495] improve deprecation message on implicit trailing optionals (#4282) --- pyo3-macros-backend/src/deprecations.rs | 10 ++++++---- tests/ui/deprecations.stderr | 16 ++++++++++++---- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/pyo3-macros-backend/src/deprecations.rs b/pyo3-macros-backend/src/deprecations.rs index 68375900c10..802561a126c 100644 --- a/pyo3-macros-backend/src/deprecations.rs +++ b/pyo3-macros-backend/src/deprecations.rs @@ -61,8 +61,9 @@ pub(crate) fn deprecate_trailing_option_default(spec: &FnSpec<'_>) -> TokenStrea { use std::fmt::Write; let mut deprecation_msg = String::from( - "This function has implicit defaults for the trailing `Option` arguments. \ - These implicit defaults are being phased out. Add `#[pyo3(signature = (", + "this function has implicit defaults for the trailing `Option` arguments \n\ + = note: these implicit defaults are being phased out \n\ + = help: add `#[pyo3(signature = (", ); spec.signature.arguments.iter().for_each(|arg| { match arg { @@ -84,8 +85,9 @@ pub(crate) fn deprecate_trailing_option_default(spec: &FnSpec<'_>) -> TokenStrea deprecation_msg.pop(); deprecation_msg.pop(); - deprecation_msg - .push_str(")]` to this function to silence this warning and keep the current behavior"); + deprecation_msg.push_str( + "))]` to this function to silence this warning and keep the current behavior", + ); quote_spanned! { spec.name.span() => #[deprecated(note = #deprecation_msg)] #[allow(dead_code)] diff --git a/tests/ui/deprecations.stderr b/tests/ui/deprecations.stderr index e08139863d1..c09894d86c1 100644 --- a/tests/ui/deprecations.stderr +++ b/tests/ui/deprecations.stderr @@ -10,25 +10,33 @@ note: the lint level is defined here 1 | #![deny(deprecated)] | ^^^^^^^^^^ -error: use of deprecated constant `MyClass::__pymethod_set_set_option__::SIGNATURE`: This function has implicit defaults for the trailing `Option` arguments. These implicit defaults are being phased out. Add `#[pyo3(signature = (_value=None)]` to this function to silence this warning and keep the current behavior +error: use of deprecated constant `MyClass::__pymethod_set_set_option__::SIGNATURE`: this function has implicit defaults for the trailing `Option` arguments + = note: these implicit defaults are being phased out + = help: add `#[pyo3(signature = (_value=None))]` to this function to silence this warning and keep the current behavior --> tests/ui/deprecations.rs:43:8 | 43 | fn set_option(&self, _value: Option) {} | ^^^^^^^^^^ -error: use of deprecated constant `__pyfunction_pyfunction_option_2::SIGNATURE`: This function has implicit defaults for the trailing `Option` arguments. These implicit defaults are being phased out. Add `#[pyo3(signature = (_i, _any=None)]` to this function to silence this warning and keep the current behavior +error: use of deprecated constant `__pyfunction_pyfunction_option_2::SIGNATURE`: this function has implicit defaults for the trailing `Option` arguments + = note: these implicit defaults are being phased out + = help: add `#[pyo3(signature = (_i, _any=None))]` to this function to silence this warning and keep the current behavior --> tests/ui/deprecations.rs:132:4 | 132 | fn pyfunction_option_2(_i: u32, _any: Option) {} | ^^^^^^^^^^^^^^^^^^^ -error: use of deprecated constant `__pyfunction_pyfunction_option_3::SIGNATURE`: This function has implicit defaults for the trailing `Option` arguments. These implicit defaults are being phased out. Add `#[pyo3(signature = (_i, _any=None, _foo=None)]` to this function to silence this warning and keep the current behavior +error: use of deprecated constant `__pyfunction_pyfunction_option_3::SIGNATURE`: this function has implicit defaults for the trailing `Option` arguments + = note: these implicit defaults are being phased out + = help: add `#[pyo3(signature = (_i, _any=None, _foo=None))]` to this function to silence this warning and keep the current behavior --> tests/ui/deprecations.rs:135:4 | 135 | fn pyfunction_option_3(_i: u32, _any: Option, _foo: Option) {} | ^^^^^^^^^^^^^^^^^^^ -error: use of deprecated constant `__pyfunction_pyfunction_option_4::SIGNATURE`: This function has implicit defaults for the trailing `Option` arguments. These implicit defaults are being phased out. Add `#[pyo3(signature = (_i, _any=None, _foo=None)]` to this function to silence this warning and keep the current behavior +error: use of deprecated constant `__pyfunction_pyfunction_option_4::SIGNATURE`: this function has implicit defaults for the trailing `Option` arguments + = note: these implicit defaults are being phased out + = help: add `#[pyo3(signature = (_i, _any=None, _foo=None))]` to this function to silence this warning and keep the current behavior --> tests/ui/deprecations.rs:138:4 | 138 | fn pyfunction_option_4( From 2e2d4404a682ce87e0b0fe254816457872acf5b2 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Mon, 24 Jun 2024 20:06:42 +0100 Subject: [PATCH 132/495] release: 0.22.0 (#4266) --- CHANGELOG.md | 66 ++++++++++++++++++- Cargo.toml | 8 +-- README.md | 4 +- 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/3761.changed.md | 1 - newsfragments/3835.added.md | 1 - newsfragments/3966.packaging.md | 1 - newsfragments/4043.fixed.md | 1 - newsfragments/4061.packaging.md | 1 - newsfragments/4072.added.md | 1 - newsfragments/4078.changed.md | 1 - newsfragments/4079.added.md | 1 - newsfragments/4086.fixed.md | 1 - newsfragments/4095.added.md | 1 - newsfragments/4095.changed.md | 1 - newsfragments/4098.changed.md | 1 - newsfragments/4100.changed.md | 1 - newsfragments/4104.fixed.md | 1 - newsfragments/4116.fixed.md | 1 - newsfragments/4117.fixed.md | 1 - newsfragments/4129.changed.md | 1 - newsfragments/4148.added.md | 1 - newsfragments/4158.added.md | 1 - newsfragments/4178.changed.md | 1 - newsfragments/4184.packaging.md | 1 - newsfragments/4194.added.md | 1 - newsfragments/4196.added.md | 4 -- newsfragments/4196.changed.md | 1 - newsfragments/4197.added.md | 1 - newsfragments/4201.changed.md | 1 - newsfragments/4202.added.md | 1 - newsfragments/4205.added.md | 1 - newsfragments/4206.added.md | 1 - newsfragments/4210.added.md | 2 - newsfragments/4210.changed.md | 1 - newsfragments/4213.added.md | 1 - newsfragments/4214.added.md | 1 - newsfragments/4219.added.md | 3 - newsfragments/4226.fixed.md | 1 - newsfragments/4228.changed.md | 1 - newsfragments/4236.fixed.md | 1 - newsfragments/4237.changed.md | 1 - newsfragments/4249.added.md | 1 - newsfragments/4250.added.md | 1 - newsfragments/4251.fixed.md | 1 - newsfragments/4254.changed.md | 1 - newsfragments/4255.added.md | 1 - newsfragments/4255.changed.md | 1 - newsfragments/4257.changed.md | 1 - newsfragments/4258.added.md | 1 - newsfragments/4259.added.md | 1 - newsfragments/4264.changed.md | 1 - pyo3-build-config/Cargo.toml | 2 +- pyo3-ffi/Cargo.toml | 4 +- pyo3-macros-backend/Cargo.toml | 6 +- pyo3-macros/Cargo.toml | 4 +- pyproject.toml | 2 +- 61 files changed, 85 insertions(+), 75 deletions(-) delete mode 100644 newsfragments/3761.changed.md delete mode 100644 newsfragments/3835.added.md delete mode 100644 newsfragments/3966.packaging.md delete mode 100644 newsfragments/4043.fixed.md delete mode 100644 newsfragments/4061.packaging.md delete mode 100644 newsfragments/4072.added.md delete mode 100644 newsfragments/4078.changed.md delete mode 100644 newsfragments/4079.added.md delete mode 100644 newsfragments/4086.fixed.md delete mode 100644 newsfragments/4095.added.md delete mode 100644 newsfragments/4095.changed.md delete mode 100644 newsfragments/4098.changed.md delete mode 100644 newsfragments/4100.changed.md delete mode 100644 newsfragments/4104.fixed.md delete mode 100644 newsfragments/4116.fixed.md delete mode 100644 newsfragments/4117.fixed.md delete mode 100644 newsfragments/4129.changed.md delete mode 100644 newsfragments/4148.added.md delete mode 100644 newsfragments/4158.added.md delete mode 100644 newsfragments/4178.changed.md delete mode 100644 newsfragments/4184.packaging.md delete mode 100644 newsfragments/4194.added.md delete mode 100644 newsfragments/4196.added.md delete mode 100644 newsfragments/4196.changed.md delete mode 100644 newsfragments/4197.added.md delete mode 100644 newsfragments/4201.changed.md delete mode 100644 newsfragments/4202.added.md delete mode 100644 newsfragments/4205.added.md delete mode 100644 newsfragments/4206.added.md delete mode 100644 newsfragments/4210.added.md delete mode 100644 newsfragments/4210.changed.md delete mode 100644 newsfragments/4213.added.md delete mode 100644 newsfragments/4214.added.md delete mode 100644 newsfragments/4219.added.md delete mode 100644 newsfragments/4226.fixed.md delete mode 100644 newsfragments/4228.changed.md delete mode 100644 newsfragments/4236.fixed.md delete mode 100644 newsfragments/4237.changed.md delete mode 100644 newsfragments/4249.added.md delete mode 100644 newsfragments/4250.added.md delete mode 100644 newsfragments/4251.fixed.md delete mode 100644 newsfragments/4254.changed.md delete mode 100644 newsfragments/4255.added.md delete mode 100644 newsfragments/4255.changed.md delete mode 100644 newsfragments/4257.changed.md delete mode 100644 newsfragments/4258.added.md delete mode 100644 newsfragments/4259.added.md delete mode 100644 newsfragments/4264.changed.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 86055a9a80c..128544202cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,69 @@ To see unreleased changes, please see the [CHANGELOG on the main branch guide](h +## [0.22.0] - 2024-06-24 + +### Packaging + +- Update `heck` dependency to 0.5. [#3966](https://github.com/PyO3/pyo3/pull/3966) +- Extend range of supported versions of `chrono-tz` optional dependency to include version 0.10. [#4061](https://github.com/PyO3/pyo3/pull/4061) +- Update MSRV to 1.63. [#4129](https://github.com/PyO3/pyo3/pull/4129) +- Add optional `num-rational` feature to add conversions with Python's `fractions.Fraction`. [#4148](https://github.com/PyO3/pyo3/pull/4148) +- Support Python 3.13. [#4184](https://github.com/PyO3/pyo3/pull/4184) + +### Added + +- Add `PyWeakref`, `PyWeakrefReference` and `PyWeakrefProxy`. [#3835](https://github.com/PyO3/pyo3/pull/3835) +- Support `#[pyclass]` on enums that have tuple variants. [#4072](https://github.com/PyO3/pyo3/pull/4072) +- Add support for scientific notation in `Decimal` conversion. [#4079](https://github.com/PyO3/pyo3/pull/4079) +- Add `pyo3_disable_reference_pool` conditional compilation flag to avoid the overhead of the global reference pool at the cost of known limitations as explained in the performance section of the guide. [#4095](https://github.com/PyO3/pyo3/pull/4095) +- Add `#[pyo3(constructor = (...))]` to customize the generated constructors for complex enum variants. [#4158](https://github.com/PyO3/pyo3/pull/4158) +- Add `PyType::module`, which always matches Python `__module__`. [#4196](https://github.com/PyO3/pyo3/pull/4196) +- Add `PyType::fully_qualified_name` which matches the "fully qualified name" defined in [PEP 737](https://peps.python.org/pep-0737). [#4196](https://github.com/PyO3/pyo3/pull/4196) +- Add `PyTypeMethods::mro` and `PyTypeMethods::bases`. [#4197](https://github.com/PyO3/pyo3/pull/4197) +- Add `#[pyclass(ord)]` to implement ordering based on `PartialOrd`. [#4202](https://github.com/PyO3/pyo3/pull/4202) +- Implement `ToPyObject` and `IntoPy` for `PyBackedStr` and `PyBackedBytes`. [#4205](https://github.com/PyO3/pyo3/pull/4205) +- Add `#[pyclass(hash)]` option to implement `__hash__` in terms of the `Hash` implementation [#4206](https://github.com/PyO3/pyo3/pull/4206) +- Add `#[pyclass(eq)]` option to generate `__eq__` based on `PartialEq`, and `#[pyclass(eq_int)]` for simple enums to implement equality based on their discriminants. [#4210](https://github.com/PyO3/pyo3/pull/4210) +- Implement `From>` for `PyClassInitializer`. [#4214](https://github.com/PyO3/pyo3/pull/4214) +- Add `as_super` methods to `PyRef` and `PyRefMut` for accesing the base class by reference. [#4219](https://github.com/PyO3/pyo3/pull/4219) +- Implement `PartialEq` for `Bound<'py, PyString>`. [#4245](https://github.com/PyO3/pyo3/pull/4245) +- Implement `PyModuleMethods::filename` on PyPy. [#4249](https://github.com/PyO3/pyo3/pull/4249) +- Implement `PartialEq<[u8]>` for `Bound<'py, PyBytes>`. [#4250](https://github.com/PyO3/pyo3/pull/4250) +- Add `pyo3_ffi::c_str` macro to create `&'static CStr` on Rust versions which don't have 1.77's `c""` literals. [#4255](https://github.com/PyO3/pyo3/pull/4255) +- Support `bool` conversion with `numpy` 2.0's `numpy.bool` type [#4258](https://github.com/PyO3/pyo3/pull/4258) +- Add `PyAnyMethods::{bitnot, matmul, floor_div, rem, divmod}`. [#4264](https://github.com/PyO3/pyo3/pull/4264) + +### Changed + +- Change the type of `PySliceIndices::slicelength` and the `length` parameter of `PySlice::indices()`. [#3761](https://github.com/PyO3/pyo3/pull/3761) +- Deprecate implicit default for trailing optional arguments [#4078](https://github.com/PyO3/pyo3/pull/4078) +- `Clone`ing pointers into the Python heap has been moved behind the `py-clone` feature, as it must panic without the GIL being held as a soundness fix. [#4095](https://github.com/PyO3/pyo3/pull/4095) +- Add `#[track_caller]` to all `Py`, `Bound<'py, T>` and `Borrowed<'a, 'py, T>` methods which can panic. [#4098](https://github.com/PyO3/pyo3/pull/4098) +- Change `PyAnyMethods::dir` to be fallible and return `PyResult>` (and similar for `PyAny::dir`). [#4100](https://github.com/PyO3/pyo3/pull/4100) +- The global reference pool (to track pending reference count decrements) is now initialized lazily to avoid the overhead of taking a mutex upon function entry when the functionality is not actually used. [#4178](https://github.com/PyO3/pyo3/pull/4178) +- Emit error messages when using `weakref` or `dict` when compiling for `abi3` for Python older than 3.9. [#4194](https://github.com/PyO3/pyo3/pull/4194) +- Change `PyType::name` to always match Python `__name__`. [#4196](https://github.com/PyO3/pyo3/pull/4196) +- Remove CPython internal ffi call for complex number including: add, sub, mul, div, neg, abs, pow. Added PyAnyMethods::{abs, pos, neg} [#4201](https://github.com/PyO3/pyo3/pull/4201) +- Deprecate implicit integer comparision for simple enums in favor of `#[pyclass(eq_int)]`. [#4210](https://github.com/PyO3/pyo3/pull/4210) +- Set the `module=` attribute of declarative modules' child `#[pymodule]`s and `#[pyclass]`es. [#4213](https://github.com/PyO3/pyo3/pull/4213) +- Set the `module` option for complex enum variants from the value set on the complex enum `module`. [#4228](https://github.com/PyO3/pyo3/pull/4228) +- Respect the Python "limited API" when building for the `abi3` feature on PyPy or GraalPy. [#4237](https://github.com/PyO3/pyo3/pull/4237) +- Optimize code generated by `#[pyo3(get)]` on `#[pyclass]` fields. [#4254](https://github.com/PyO3/pyo3/pull/4254) +- `PyCFunction::new`, `PyCFunction::new_with_keywords` and `PyCFunction::new_closure` now take `&'static CStr` name and doc arguments (previously was `&'static str`). [#4255](https://github.com/PyO3/pyo3/pull/4255) +- The `experimental-declarative-modules` feature is now stabilized and available by default. [#4257](https://github.com/PyO3/pyo3/pull/4257) + +### Fixed + +- Fix panic when `PYO3_CROSS_LIB_DIR` is set to a missing path. [#4043](https://github.com/PyO3/pyo3/pull/4043) +- Fix a compile error when exporting an exception created with `create_exception!` living in a different Rust module using the `declarative-module` feature. [#4086](https://github.com/PyO3/pyo3/pull/4086) +- Fix FFI definitions of `PY_VECTORCALL_ARGUMENTS_OFFSET` and `PyVectorcall_NARGS` to fix a false-positive assertion. [#4104](https://github.com/PyO3/pyo3/pull/4104) +- Disable `PyUnicode_DATA` on PyPy: not exposed by PyPy. [#4116](https://github.com/PyO3/pyo3/pull/4116) +- Correctly handle `#[pyo3(from_py_with = ...)]` attribute on dunder (`__magic__`) method arguments instead of silently ignoring it. [#4117](https://github.com/PyO3/pyo3/pull/4117) +- Fix a compile error when declaring a standalone function or class method with a Python name that is a Rust keyword. [#4226](https://github.com/PyO3/pyo3/pull/4226) +- Fix declarative modules discarding doc comments on the `mod` node. [#4236](https://github.com/PyO3/pyo3/pull/4236) +- Fix `__dict__` attribute missing for `#[pyclass(dict)]` instances when building for `abi3` on Python 3.9. [#4251](https://github.com/PyO3/pyo3/pull/4251) + ## [0.21.2] - 2024-04-16 ### Changed @@ -1745,7 +1808,8 @@ Yanked - Initial release -[Unreleased]: https://github.com/pyo3/pyo3/compare/v0.21.2...HEAD +[Unreleased]: https://github.com/pyo3/pyo3/compare/v0.22.0...HEAD +[0.22.0]: https://github.com/pyo3/pyo3/compare/v0.21.2...v0.22.0 [0.21.2]: https://github.com/pyo3/pyo3/compare/v0.21.1...v0.21.2 [0.21.1]: https://github.com/pyo3/pyo3/compare/v0.21.0...v0.21.1 [0.21.0]: https://github.com/pyo3/pyo3/compare/v0.20.3...v0.21.0 diff --git a/Cargo.toml b/Cargo.toml index 9ab15c1d052..5d3888dfda1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3" -version = "0.22.0-dev" +version = "0.22.0" description = "Bindings to Python interpreter" authors = ["PyO3 Project and Contributors "] readme = "README.md" @@ -21,10 +21,10 @@ memoffset = "0.9" once_cell = "1.13.0" # ffi bindings to the python interpreter, split into a separate crate so they can be used independently -pyo3-ffi = { path = "pyo3-ffi", version = "=0.22.0-dev" } +pyo3-ffi = { path = "pyo3-ffi", version = "=0.22.0" } # support crates for macros feature -pyo3-macros = { path = "pyo3-macros", version = "=0.22.0-dev", optional = true } +pyo3-macros = { path = "pyo3-macros", version = "=0.22.0", optional = true } indoc = { version = "2.0.1", optional = true } unindent = { version = "0.2.1", optional = true } @@ -63,7 +63,7 @@ rayon = "1.6.1" futures = "0.3.28" [build-dependencies] -pyo3-build-config = { path = "pyo3-build-config", version = "=0.22.0-dev", features = ["resolve-config"] } +pyo3-build-config = { path = "pyo3-build-config", version = "=0.22.0", features = ["resolve-config"] } [features] default = ["macros"] diff --git a/README.md b/README.md index ed77a62b28a..69f7e1740db 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ name = "string_sum" crate-type = ["cdylib"] [dependencies] -pyo3 = { version = "0.21.2", features = ["extension-module"] } +pyo3 = { version = "0.22.0", 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.21.2" +version = "0.22.0" features = ["auto-initialize"] ``` diff --git a/examples/decorator/.template/pre-script.rhai b/examples/decorator/.template/pre-script.rhai index 37372854cd8..aea6c46edc6 100644 --- a/examples/decorator/.template/pre-script.rhai +++ b/examples/decorator/.template/pre-script.rhai @@ -1,4 +1,4 @@ -variable::set("PYO3_VERSION", "0.21.2"); +variable::set("PYO3_VERSION", "0.22.0"); 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 37372854cd8..aea6c46edc6 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.21.2"); +variable::set("PYO3_VERSION", "0.22.0"); 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 7c2f375fbfb..d5f84ed3ff8 100644 --- a/examples/plugin/.template/pre-script.rhai +++ b/examples/plugin/.template/pre-script.rhai @@ -1,4 +1,4 @@ -variable::set("PYO3_VERSION", "0.21.2"); +variable::set("PYO3_VERSION", "0.22.0"); 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 dd2950665eb..5ea757803da 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.21.2"); +variable::set("PYO3_VERSION", "0.22.0"); 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 37372854cd8..aea6c46edc6 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.21.2"); +variable::set("PYO3_VERSION", "0.22.0"); file::rename(".template/Cargo.toml", "Cargo.toml"); file::rename(".template/pyproject.toml", "pyproject.toml"); file::delete(".template"); diff --git a/newsfragments/3761.changed.md b/newsfragments/3761.changed.md deleted file mode 100644 index fd0847211d8..00000000000 --- a/newsfragments/3761.changed.md +++ /dev/null @@ -1 +0,0 @@ -Change the type of `PySliceIndices::slicelength` and the `length` parameter of `PySlice::indices()`. diff --git a/newsfragments/3835.added.md b/newsfragments/3835.added.md deleted file mode 100644 index 2970a4c8db4..00000000000 --- a/newsfragments/3835.added.md +++ /dev/null @@ -1 +0,0 @@ -Add `PyWeakref`, `PyWeakrefReference` and `PyWeakrefProxy`. diff --git a/newsfragments/3966.packaging.md b/newsfragments/3966.packaging.md deleted file mode 100644 index 81220bc4e85..00000000000 --- a/newsfragments/3966.packaging.md +++ /dev/null @@ -1 +0,0 @@ -Update `heck` dependency to 0.5. diff --git a/newsfragments/4043.fixed.md b/newsfragments/4043.fixed.md deleted file mode 100644 index 7653eb295bf..00000000000 --- a/newsfragments/4043.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Don't panic when `PYO3_CROSS_LIB_DIR` is set to a missing path. \ No newline at end of file diff --git a/newsfragments/4061.packaging.md b/newsfragments/4061.packaging.md deleted file mode 100644 index 5e51f50290d..00000000000 --- a/newsfragments/4061.packaging.md +++ /dev/null @@ -1 +0,0 @@ -Extend range of supported versions of `chrono-tz` optional dependency to include version 0.10. diff --git a/newsfragments/4072.added.md b/newsfragments/4072.added.md deleted file mode 100644 index 23207c849d8..00000000000 --- a/newsfragments/4072.added.md +++ /dev/null @@ -1 +0,0 @@ -Support `#[pyclass]` on enums that have tuple variants. \ No newline at end of file diff --git a/newsfragments/4078.changed.md b/newsfragments/4078.changed.md deleted file mode 100644 index 45f160f5556..00000000000 --- a/newsfragments/4078.changed.md +++ /dev/null @@ -1 +0,0 @@ -deprecate implicit default for trailing optional arguments diff --git a/newsfragments/4079.added.md b/newsfragments/4079.added.md deleted file mode 100644 index afe26728f9a..00000000000 --- a/newsfragments/4079.added.md +++ /dev/null @@ -1 +0,0 @@ -Added support for scientific notation in `Decimal` conversion diff --git a/newsfragments/4086.fixed.md b/newsfragments/4086.fixed.md deleted file mode 100644 index e9cae7733f9..00000000000 --- a/newsfragments/4086.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Fixes a compile error when exporting an exception created with `create_exception!` living in a different Rust module using the `declarative-module` feature. \ No newline at end of file diff --git a/newsfragments/4095.added.md b/newsfragments/4095.added.md deleted file mode 100644 index c9940f70f12..00000000000 --- a/newsfragments/4095.added.md +++ /dev/null @@ -1 +0,0 @@ -Add `pyo3_disable_reference_pool` conditional compilation flag to avoid the overhead of the global reference pool at the cost of known limitations as explained in the performance section of the guide. diff --git a/newsfragments/4095.changed.md b/newsfragments/4095.changed.md deleted file mode 100644 index 7f155ae04ef..00000000000 --- a/newsfragments/4095.changed.md +++ /dev/null @@ -1 +0,0 @@ -`Clone`ing pointers into the Python heap has been moved behind the `py-clone` feature, as it must panic without the GIL being held as a soundness fix. diff --git a/newsfragments/4098.changed.md b/newsfragments/4098.changed.md deleted file mode 100644 index 5df526a52e3..00000000000 --- a/newsfragments/4098.changed.md +++ /dev/null @@ -1 +0,0 @@ -Add `#[track_caller]` to all `Py`, `Bound<'py, T>` and `Borrowed<'a, 'py, T>` methods which can panic. diff --git a/newsfragments/4100.changed.md b/newsfragments/4100.changed.md deleted file mode 100644 index 13fd2e02aa8..00000000000 --- a/newsfragments/4100.changed.md +++ /dev/null @@ -1 +0,0 @@ -Change `PyAnyMethods::dir` to be fallible and return `PyResult>` (and similar for `PyAny::dir`). diff --git a/newsfragments/4104.fixed.md b/newsfragments/4104.fixed.md deleted file mode 100644 index 3eff1654b4f..00000000000 --- a/newsfragments/4104.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Changes definitions of `PY_VECTORCALL_ARGUMENTS_OFFSET` and `PyVectorcall_NARGS` to fix a false-positive assertion. diff --git a/newsfragments/4116.fixed.md b/newsfragments/4116.fixed.md deleted file mode 100644 index 63531aceb39..00000000000 --- a/newsfragments/4116.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Disable `PyUnicode_DATA` on PyPy: Not exposed by PyPy. diff --git a/newsfragments/4117.fixed.md b/newsfragments/4117.fixed.md deleted file mode 100644 index c3bb4c144b6..00000000000 --- a/newsfragments/4117.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Correctly handle `#[pyo3(from_py_with = ...)]` attribute on dunder (`__magic__`) method arguments instead of silently ignoring it. diff --git a/newsfragments/4129.changed.md b/newsfragments/4129.changed.md deleted file mode 100644 index 6818181ad0c..00000000000 --- a/newsfragments/4129.changed.md +++ /dev/null @@ -1 +0,0 @@ -Raised the MSRV to 1.63 diff --git a/newsfragments/4148.added.md b/newsfragments/4148.added.md deleted file mode 100644 index 16da3d2db37..00000000000 --- a/newsfragments/4148.added.md +++ /dev/null @@ -1 +0,0 @@ -Conversion between [num-rational](https://github.com/rust-num/num-rational) and Python's fractions.Fraction. diff --git a/newsfragments/4158.added.md b/newsfragments/4158.added.md deleted file mode 100644 index 42e6d3ff4b4..00000000000 --- a/newsfragments/4158.added.md +++ /dev/null @@ -1 +0,0 @@ -Added `#[pyo3(constructor = (...))]` to customize the generated constructors for complex enum variants diff --git a/newsfragments/4178.changed.md b/newsfragments/4178.changed.md deleted file mode 100644 index a97c1ec8f6e..00000000000 --- a/newsfragments/4178.changed.md +++ /dev/null @@ -1 +0,0 @@ -The global reference pool (to track pending reference count decrements) is now initialized lazily to avoid the overhead of taking a mutex upon function entry when the functionality is not actually used. diff --git a/newsfragments/4184.packaging.md b/newsfragments/4184.packaging.md deleted file mode 100644 index c12302a7029..00000000000 --- a/newsfragments/4184.packaging.md +++ /dev/null @@ -1 +0,0 @@ -Support Python 3.13. diff --git a/newsfragments/4194.added.md b/newsfragments/4194.added.md deleted file mode 100644 index 6f032138d25..00000000000 --- a/newsfragments/4194.added.md +++ /dev/null @@ -1 +0,0 @@ -Added error messages when using `weakref` or `dict` when compiling for `abi3` for Python older than 3.9 diff --git a/newsfragments/4196.added.md b/newsfragments/4196.added.md deleted file mode 100644 index d9c295d551c..00000000000 --- a/newsfragments/4196.added.md +++ /dev/null @@ -1,4 +0,0 @@ -Add `PyType::module`, which always matches Python `__module__`. -Add `PyType::fully_qualified_name` which matches the "fully qualified name" -defined in https://peps.python.org/pep-0737 (not exposed in Python), -which is useful for error messages and `repr()` implementations. diff --git a/newsfragments/4196.changed.md b/newsfragments/4196.changed.md deleted file mode 100644 index 4ea69180a2d..00000000000 --- a/newsfragments/4196.changed.md +++ /dev/null @@ -1 +0,0 @@ -Change `PyType::name` to always match Python `__name__`. diff --git a/newsfragments/4197.added.md b/newsfragments/4197.added.md deleted file mode 100644 index 5652028cb76..00000000000 --- a/newsfragments/4197.added.md +++ /dev/null @@ -1 +0,0 @@ -Add `PyTypeMethods::mro` and `PyTypeMethods::bases`. diff --git a/newsfragments/4201.changed.md b/newsfragments/4201.changed.md deleted file mode 100644 index c236dd27210..00000000000 --- a/newsfragments/4201.changed.md +++ /dev/null @@ -1 +0,0 @@ -Remove CPython internal ffi call for complex number including: add, sub, mul, div, neg, abs, pow. Added PyAnyMethods::{abs, pos, neg} diff --git a/newsfragments/4202.added.md b/newsfragments/4202.added.md deleted file mode 100644 index d15b6ce810c..00000000000 --- a/newsfragments/4202.added.md +++ /dev/null @@ -1 +0,0 @@ -Add `#[pyclass(ord)]` to implement ordering based on `PartialOrd`. \ No newline at end of file diff --git a/newsfragments/4205.added.md b/newsfragments/4205.added.md deleted file mode 100644 index 86ce9fc32ae..00000000000 --- a/newsfragments/4205.added.md +++ /dev/null @@ -1 +0,0 @@ -Added `ToPyObject` and `IntoPy` impls for `PyBackedStr` and `PyBackedBytes`. diff --git a/newsfragments/4206.added.md b/newsfragments/4206.added.md deleted file mode 100644 index 90a74af329c..00000000000 --- a/newsfragments/4206.added.md +++ /dev/null @@ -1 +0,0 @@ -Added `#[pyclass(hash)]` option to implement `__hash__` in terms of the `Hash` implementation diff --git a/newsfragments/4210.added.md b/newsfragments/4210.added.md deleted file mode 100644 index dae8cd8dabb..00000000000 --- a/newsfragments/4210.added.md +++ /dev/null @@ -1,2 +0,0 @@ -Added `#[pyclass(eq)]` option to generate `__eq__` based on `PartialEq`. -Added `#[pyclass(eq_int)]` for simple enums to implement equality based on their discriminants. \ No newline at end of file diff --git a/newsfragments/4210.changed.md b/newsfragments/4210.changed.md deleted file mode 100644 index a69e3c3a37e..00000000000 --- a/newsfragments/4210.changed.md +++ /dev/null @@ -1 +0,0 @@ -Deprecate implicit integer comparision for simple enums in favor of `#[pyclass(eq_int)]`. \ No newline at end of file diff --git a/newsfragments/4213.added.md b/newsfragments/4213.added.md deleted file mode 100644 index 6f553dc93ab..00000000000 --- a/newsfragments/4213.added.md +++ /dev/null @@ -1 +0,0 @@ -Properly fills the `module=` attribute of declarative modules child `#[pymodule]` and `#[pyclass]`. \ No newline at end of file diff --git a/newsfragments/4214.added.md b/newsfragments/4214.added.md deleted file mode 100644 index 943e1e99e60..00000000000 --- a/newsfragments/4214.added.md +++ /dev/null @@ -1 +0,0 @@ -Added `From>` impl for `PyClassInitializer`. \ No newline at end of file diff --git a/newsfragments/4219.added.md b/newsfragments/4219.added.md deleted file mode 100644 index cea8fa1c314..00000000000 --- a/newsfragments/4219.added.md +++ /dev/null @@ -1,3 +0,0 @@ -- Added `as_super` methods to `PyRef` and `PyRefMut` for accesing the base class by reference -- Updated user guide to recommend `as_super` for referencing the base class instead of `as_ref` -- Added `pyo3::internal_tricks::ptr_from_mut` function for casting `&mut T` to `*mut T` \ No newline at end of file diff --git a/newsfragments/4226.fixed.md b/newsfragments/4226.fixed.md deleted file mode 100644 index b2b7d7d12a2..00000000000 --- a/newsfragments/4226.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Fixes a compile error when declaring a standalone function or class method with a Python name that is a Rust keyword. diff --git a/newsfragments/4228.changed.md b/newsfragments/4228.changed.md deleted file mode 100644 index 84323876b42..00000000000 --- a/newsfragments/4228.changed.md +++ /dev/null @@ -1 +0,0 @@ -Changed the `module` option for complex enum variants to inherit from the value set on the complex enum `module`. diff --git a/newsfragments/4236.fixed.md b/newsfragments/4236.fixed.md deleted file mode 100644 index f87d16941c7..00000000000 --- a/newsfragments/4236.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Declarative modules: do not discard doc comments on the `mod` node. \ No newline at end of file diff --git a/newsfragments/4237.changed.md b/newsfragments/4237.changed.md deleted file mode 100644 index 25dd922d668..00000000000 --- a/newsfragments/4237.changed.md +++ /dev/null @@ -1 +0,0 @@ -Respect the Python "limited API" when building for the `abi3` feature on PyPy or GraalPy. diff --git a/newsfragments/4249.added.md b/newsfragments/4249.added.md deleted file mode 100644 index 8037f562699..00000000000 --- a/newsfragments/4249.added.md +++ /dev/null @@ -1 +0,0 @@ -Implement `PyModuleMethods::filename` on PyPy. diff --git a/newsfragments/4250.added.md b/newsfragments/4250.added.md deleted file mode 100644 index bc179d120a3..00000000000 --- a/newsfragments/4250.added.md +++ /dev/null @@ -1 +0,0 @@ -Implement `PartialEq` for `Bound<'py, PyBytes>`. diff --git a/newsfragments/4251.fixed.md b/newsfragments/4251.fixed.md deleted file mode 100644 index 5cc23c7a126..00000000000 --- a/newsfragments/4251.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Fix `__dict__` attribute missing for `#[pyclass(dict)]` instances when building for `abi3` on Python 3.9. diff --git a/newsfragments/4254.changed.md b/newsfragments/4254.changed.md deleted file mode 100644 index e58e0345696..00000000000 --- a/newsfragments/4254.changed.md +++ /dev/null @@ -1 +0,0 @@ -Optimize code generated by `#[pyo3(get)]` on `#[pyclass]` fields. diff --git a/newsfragments/4255.added.md b/newsfragments/4255.added.md deleted file mode 100644 index c70c5da279d..00000000000 --- a/newsfragments/4255.added.md +++ /dev/null @@ -1 +0,0 @@ -Add `pyo3_ffi::c_str` macro to create `&'static CStr` on Rust versions which don't have 1.77's `c""` literals. diff --git a/newsfragments/4255.changed.md b/newsfragments/4255.changed.md deleted file mode 100644 index 185bd5cc39d..00000000000 --- a/newsfragments/4255.changed.md +++ /dev/null @@ -1 +0,0 @@ -`PyCFunction::new`, `PyCFunction::new_with_keywords` and `PyCFunction::new_closure` now take `&'static CStr` name and doc arguments (previously was `&'static str`). diff --git a/newsfragments/4257.changed.md b/newsfragments/4257.changed.md deleted file mode 100644 index dee4a7ae13d..00000000000 --- a/newsfragments/4257.changed.md +++ /dev/null @@ -1 +0,0 @@ -The `experimental-declarative-modules` feature is now stabilized and available by default diff --git a/newsfragments/4258.added.md b/newsfragments/4258.added.md deleted file mode 100644 index 0ceea11f91e..00000000000 --- a/newsfragments/4258.added.md +++ /dev/null @@ -1 +0,0 @@ -Added support for `bool` conversion with `numpy` 2.0's `numpy.bool` type diff --git a/newsfragments/4259.added.md b/newsfragments/4259.added.md deleted file mode 100644 index 692fb277422..00000000000 --- a/newsfragments/4259.added.md +++ /dev/null @@ -1 +0,0 @@ -Implement `PartialEq` for `Bound<'py, PyString>`. diff --git a/newsfragments/4264.changed.md b/newsfragments/4264.changed.md deleted file mode 100644 index a3e89c6f98c..00000000000 --- a/newsfragments/4264.changed.md +++ /dev/null @@ -1 +0,0 @@ -Added `PyAnyMethods::{bitnot, matmul, floor_div, rem, divmod}` for completeness. diff --git a/pyo3-build-config/Cargo.toml b/pyo3-build-config/Cargo.toml index 600237f8646..6ab56e2ee14 100644 --- a/pyo3-build-config/Cargo.toml +++ b/pyo3-build-config/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3-build-config" -version = "0.22.0-dev" +version = "0.22.0" 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 865da93926a..c3d9c608b0c 100644 --- a/pyo3-ffi/Cargo.toml +++ b/pyo3-ffi/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3-ffi" -version = "0.22.0-dev" +version = "0.22.0" 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.22.0-dev", features = ["resolve-config"] } +pyo3-build-config = { path = "../pyo3-build-config", version = "=0.22.0", features = ["resolve-config"] } [lints] workspace = true diff --git a/pyo3-macros-backend/Cargo.toml b/pyo3-macros-backend/Cargo.toml index 54220ab462b..264134d5249 100644 --- a/pyo3-macros-backend/Cargo.toml +++ b/pyo3-macros-backend/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3-macros-backend" -version = "0.22.0-dev" +version = "0.22.0" description = "Code generation for PyO3 package" authors = ["PyO3 Project and Contributors "] keywords = ["pyo3", "python", "cpython", "ffi"] @@ -16,7 +16,7 @@ edition = "2021" [dependencies] heck = "0.5" proc-macro2 = { version = "1.0.60", default-features = false } -pyo3-build-config = { path = "../pyo3-build-config", version = "=0.22.0-dev", features = ["resolve-config"] } +pyo3-build-config = { path = "../pyo3-build-config", version = "=0.22.0", features = ["resolve-config"] } quote = { version = "1", default-features = false } [dependencies.syn] @@ -25,7 +25,7 @@ default-features = false features = ["derive", "parsing", "printing", "clone-impls", "full", "extra-traits"] [build-dependencies] -pyo3-build-config = { path = "../pyo3-build-config", version = "=0.22.0-dev" } +pyo3-build-config = { path = "../pyo3-build-config", version = "=0.22.0" } [lints] workspace = true diff --git a/pyo3-macros/Cargo.toml b/pyo3-macros/Cargo.toml index 04c0ae6fb28..0dbbdd7cac9 100644 --- a/pyo3-macros/Cargo.toml +++ b/pyo3-macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3-macros" -version = "0.22.0-dev" +version = "0.22.0" description = "Proc macros for PyO3 package" authors = ["PyO3 Project and Contributors "] keywords = ["pyo3", "python", "cpython", "ffi"] @@ -22,7 +22,7 @@ gil-refs = ["pyo3-macros-backend/gil-refs"] proc-macro2 = { version = "1.0.60", default-features = false } quote = "1" syn = { version = "2", features = ["full", "extra-traits"] } -pyo3-macros-backend = { path = "../pyo3-macros-backend", version = "=0.22.0-dev" } +pyo3-macros-backend = { path = "../pyo3-macros-backend", version = "=0.22.0" } [lints] workspace = true diff --git a/pyproject.toml b/pyproject.toml index a007ee6dc7a..ab846250956 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ [tool.towncrier] filename = "CHANGELOG.md" -version = "0.22.0-dev" +version = "0.22.0" start_string = "\n" template = ".towncrier.template.md" title_format = "## [{version}] - {project_date}" From 7c2f5e80de3a08bacd125164178da5f739b1b379 Mon Sep 17 00:00:00 2001 From: jatoben Date: Tue, 25 Jun 2024 22:41:42 -0700 Subject: [PATCH 133/495] Don't raise `TypeError` from generated equality method (#4287) * Don't raise TypeError in derived equality method * Add newsfragment --- newsfragments/4287.changed.md | 1 + pyo3-macros-backend/src/pyclass.rs | 10 ++++++--- pytests/src/comparisons.rs | 13 ++++++++++++ pytests/tests/test_comparisons.py | 33 ++++++++++++++++++++++++------ 4 files changed, 48 insertions(+), 9 deletions(-) create mode 100644 newsfragments/4287.changed.md diff --git a/newsfragments/4287.changed.md b/newsfragments/4287.changed.md new file mode 100644 index 00000000000..440e123cebf --- /dev/null +++ b/newsfragments/4287.changed.md @@ -0,0 +1 @@ +Return `NotImplemented` from generated equality method when comparing different types. diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index 07d9e32e528..fd85cfa3bb6 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -1844,9 +1844,13 @@ fn pyclass_richcmp( op: #pyo3_path::pyclass::CompareOp ) -> #pyo3_path::PyResult<#pyo3_path::PyObject> { let self_val = self; - let other = &*#pyo3_path::types::PyAnyMethods::downcast::(other)?.borrow(); - match op { - #arms + if let Ok(other) = #pyo3_path::types::PyAnyMethods::downcast::(other) { + let other = &*other.borrow(); + match op { + #arms + } + } else { + ::std::result::Result::Ok(py.NotImplemented()) } } }; diff --git a/pytests/src/comparisons.rs b/pytests/src/comparisons.rs index fa35acf8e1a..5c7f659c9b3 100644 --- a/pytests/src/comparisons.rs +++ b/pytests/src/comparisons.rs @@ -34,6 +34,18 @@ impl EqDefaultNe { } } +#[pyclass(eq)] +#[derive(PartialEq, Eq)] +struct EqDerived(i64); + +#[pymethods] +impl EqDerived { + #[new] + fn new(value: i64) -> Self { + Self(value) + } +} + #[pyclass] struct Ordered(i64); @@ -104,6 +116,7 @@ impl OrderedDefaultNe { pub fn comparisons(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_class::()?; + m.add_class::()?; m.add_class::()?; m.add_class::()?; Ok(()) diff --git a/pytests/tests/test_comparisons.py b/pytests/tests/test_comparisons.py index 508cdeb2465..50bba81cb1a 100644 --- a/pytests/tests/test_comparisons.py +++ b/pytests/tests/test_comparisons.py @@ -1,7 +1,13 @@ from typing import Type, Union import pytest -from pyo3_pytests.comparisons import Eq, EqDefaultNe, Ordered, OrderedDefaultNe +from pyo3_pytests.comparisons import ( + Eq, + EqDefaultNe, + EqDerived, + Ordered, + OrderedDefaultNe, +) from typing_extensions import Self @@ -9,15 +15,23 @@ class PyEq: def __init__(self, x: int) -> None: self.x = x - def __eq__(self, other: Self) -> bool: - return self.x == other.x + def __eq__(self, other: object) -> bool: + if isinstance(other, self.__class__): + return self.x == other.x + else: + return NotImplemented def __ne__(self, other: Self) -> bool: - return self.x != other.x + if isinstance(other, self.__class__): + return self.x != other.x + else: + return NotImplemented -@pytest.mark.parametrize("ty", (Eq, PyEq), ids=("rust", "python")) -def test_eq(ty: Type[Union[Eq, PyEq]]): +@pytest.mark.parametrize( + "ty", (Eq, EqDerived, PyEq), ids=("rust", "rust-derived", "python") +) +def test_eq(ty: Type[Union[Eq, EqDerived, PyEq]]): a = ty(0) b = ty(0) c = ty(1) @@ -32,6 +46,13 @@ def test_eq(ty: Type[Union[Eq, PyEq]]): assert b != c assert not (b == c) + assert not a == 0 + assert a != 0 + assert not b == 0 + assert b != 1 + assert not c == 1 + assert c != 1 + with pytest.raises(TypeError): assert a <= b From 8f7450e33d9edcab790d5b2ad303cbb81a86536e Mon Sep 17 00:00:00 2001 From: Ben Beasley Date: Wed, 26 Jun 2024 15:21:31 -0400 Subject: [PATCH 134/495] Fix 128-bit int regression on big-endian with Python <3.13 (#4291) Fixes #4290. --- newsfragments/4291.fixed.md | 1 + src/conversions/std/num.rs | 23 +++++++++++++---------- 2 files changed, 14 insertions(+), 10 deletions(-) create mode 100644 newsfragments/4291.fixed.md diff --git a/newsfragments/4291.fixed.md b/newsfragments/4291.fixed.md new file mode 100644 index 00000000000..38242ff7de9 --- /dev/null +++ b/newsfragments/4291.fixed.md @@ -0,0 +1 @@ +Fix 128-bit int regression on big-endian platforms with Python <3.13 diff --git a/src/conversions/std/num.rs b/src/conversions/std/num.rs index effe7c7c062..1304e73e4b6 100644 --- a/src/conversions/std/num.rs +++ b/src/conversions/std/num.rs @@ -238,15 +238,18 @@ mod fast_128bit_int_conversion { unsafe { ffi::PyNumber_Index(ob.as_ptr()).assume_owned_or_err(ob.py())? }; let mut buffer = [0u8; std::mem::size_of::<$rust_type>()]; #[cfg(not(Py_3_13))] - crate::err::error_on_minusone(ob.py(), unsafe { - ffi::_PyLong_AsByteArray( - num.as_ptr() as *mut ffi::PyLongObject, - buffer.as_mut_ptr(), - buffer.len(), - 1, - $is_signed.into(), - ) - })?; + { + crate::err::error_on_minusone(ob.py(), unsafe { + ffi::_PyLong_AsByteArray( + num.as_ptr() as *mut ffi::PyLongObject, + buffer.as_mut_ptr(), + buffer.len(), + 1, + $is_signed.into(), + ) + })?; + Ok(<$rust_type>::from_le_bytes(buffer)) + } #[cfg(Py_3_13)] { let mut flags = ffi::Py_ASNATIVEBYTES_NATIVE_ENDIAN; @@ -272,8 +275,8 @@ mod fast_128bit_int_conversion { "Python int larger than 128 bits", )); } + Ok(<$rust_type>::from_ne_bytes(buffer)) } - Ok(<$rust_type>::from_ne_bytes(buffer)) } #[cfg(feature = "experimental-inspect")] From 872bd7e6f7390ff2035b1d164582aace33d69e76 Mon Sep 17 00:00:00 2001 From: Kyle Barron Date: Mon, 1 Jul 2024 12:26:17 -0400 Subject: [PATCH 135/495] Add pyo3-arrow to README (#4302) --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 69f7e1740db..9f08dc0ea28 100644 --- a/README.md +++ b/README.md @@ -176,6 +176,7 @@ about this topic. - [pythonize](https://github.com/davidhewitt/pythonize) _Serde serializer for converting Rust objects to JSON-compatible Python objects_ - [pyo3-asyncio](https://github.com/awestlake87/pyo3-asyncio) _Utilities for working with Python's Asyncio library and async functions_ - [rustimport](https://github.com/mityax/rustimport) _Directly import Rust files or crates from Python, without manual compilation step. Provides pyo3 integration by default and generates pyo3 binding code automatically._ +- [pyo3-arrow](https://crates.io/crates/pyo3-arrow) _Lightweight [Apache Arrow](https://arrow.apache.org/) integration for pyo3._ ## Examples From f3603a0a483448c71f1811786257d070733540bf Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Mon, 1 Jul 2024 17:54:50 -0400 Subject: [PATCH 136/495] Avoid generating functions that are only ever const evaluated with declarative modules (#4297) Refs #4286 --- newsfragments/4297.fixed.md | 1 + pyo3-macros-backend/src/module.rs | 34 ++++++++++++++----------------- 2 files changed, 16 insertions(+), 19 deletions(-) create mode 100644 newsfragments/4297.fixed.md diff --git a/newsfragments/4297.fixed.md b/newsfragments/4297.fixed.md new file mode 100644 index 00000000000..18cfd93756f --- /dev/null +++ b/newsfragments/4297.fixed.md @@ -0,0 +1 @@ +stop generating code that will never be covered with declarative modules diff --git a/pyo3-macros-backend/src/module.rs b/pyo3-macros-backend/src/module.rs index 39240aba7e8..4ce3023cbb4 100644 --- a/pyo3-macros-backend/src/module.rs +++ b/pyo3-macros-backend/src/module.rs @@ -286,7 +286,18 @@ pub fn pymodule_module_impl(mut module: syn::ItemMod) -> Result { } } - let initialization = module_initialization(&name, ctx); + let module_def = quote! {{ + use #pyo3_path::impl_::pymodule as impl_; + const INITIALIZER: impl_::ModuleInitializer = impl_::ModuleInitializer(__pyo3_pymodule); + unsafe { + impl_::ModuleDef::new( + __PYO3_NAME, + #doc, + INITIALIZER + ) + } + }}; + let initialization = module_initialization(&name, ctx, module_def); Ok(quote!( #(#attrs)* #vis mod #ident { @@ -294,21 +305,6 @@ pub fn pymodule_module_impl(mut module: syn::ItemMod) -> Result { #initialization - #[allow(unknown_lints, non_local_definitions)] - impl MakeDef { - const fn make_def() -> #pyo3_path::impl_::pymodule::ModuleDef { - use #pyo3_path::impl_::pymodule as impl_; - const INITIALIZER: impl_::ModuleInitializer = impl_::ModuleInitializer(__pyo3_pymodule); - unsafe { - impl_::ModuleDef::new( - __PYO3_NAME, - #doc, - INITIALIZER - ) - } - } - } - fn __pyo3_pymodule(module: &#pyo3_path::Bound<'_, #pyo3_path::types::PyModule>) -> #pyo3_path::PyResult<()> { use #pyo3_path::impl_::pymodule::PyAddToModule; #( @@ -335,7 +331,7 @@ pub fn pymodule_function_impl(mut function: syn::ItemFn) -> Result let vis = &function.vis; let doc = get_doc(&function.attrs, None, ctx); - let initialization = module_initialization(&name, ctx); + let initialization = module_initialization(&name, ctx, quote! { MakeDef::make_def() }); // Module function called with optional Python<'_> marker as first arg, followed by the module. let mut module_args = Vec::new(); @@ -400,7 +396,7 @@ pub fn pymodule_function_impl(mut function: syn::ItemFn) -> Result }) } -fn module_initialization(name: &syn::Ident, ctx: &Ctx) -> TokenStream { +fn module_initialization(name: &syn::Ident, ctx: &Ctx, module_def: TokenStream) -> TokenStream { let Ctx { pyo3_path, .. } = ctx; let pyinit_symbol = format!("PyInit_{}", name); let name = name.to_string(); @@ -412,7 +408,7 @@ fn module_initialization(name: &syn::Ident, ctx: &Ctx) -> TokenStream { pub(super) struct MakeDef; #[doc(hidden)] - pub static _PYO3_DEF: #pyo3_path::impl_::pymodule::ModuleDef = MakeDef::make_def(); + pub static _PYO3_DEF: #pyo3_path::impl_::pymodule::ModuleDef = #module_def; /// This autogenerated function is called by the python interpreter when importing /// the module. From ccd04475a30409f226a1e044239c6b7e35260e98 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Tue, 2 Jul 2024 07:24:47 -0400 Subject: [PATCH 137/495] refs #4286 -- allow setting submodule on declarative pymodules (#4301) --- guide/src/module.md | 4 +- newsfragments/4301.added.md | 1 + pyo3-macros-backend/src/attributes.rs | 2 + pyo3-macros-backend/src/module.rs | 55 ++++++++++++++++++++------- pyo3-macros/src/lib.rs | 24 ++++++++++-- tests/test_declarative_module.rs | 10 ++++- 6 files changed, 77 insertions(+), 19 deletions(-) create mode 100644 newsfragments/4301.added.md diff --git a/guide/src/module.md b/guide/src/module.md index 8c6049270cb..2c4039a6e76 100644 --- a/guide/src/module.md +++ b/guide/src/module.md @@ -154,6 +154,8 @@ The `#[pymodule]` macro automatically sets the `module` attribute of the `#[pycl For nested modules, the name of the parent module is automatically added. In the following example, the `Unit` class will have for `module` `my_extension.submodule` because it is properly nested but the `Ext` class will have for `module` the default `builtins` because it not nested. + +You can provide the `submodule` argument to `pymodule()` for modules that are not top-level modules. ```rust # mod declarative_module_module_attr_test { use pyo3::prelude::*; @@ -168,7 +170,7 @@ mod my_extension { #[pymodule_export] use super::Ext; - #[pymodule] + #[pymodule(submodule)] mod submodule { use super::*; // This is a submodule diff --git a/newsfragments/4301.added.md b/newsfragments/4301.added.md new file mode 100644 index 00000000000..2ee759c28b5 --- /dev/null +++ b/newsfragments/4301.added.md @@ -0,0 +1 @@ +allow setting `submodule` on declarative `#[pymodule]`s diff --git a/pyo3-macros-backend/src/attributes.rs b/pyo3-macros-backend/src/attributes.rs index 02af17b618b..6a45ee875e3 100644 --- a/pyo3-macros-backend/src/attributes.rs +++ b/pyo3-macros-backend/src/attributes.rs @@ -37,6 +37,7 @@ pub mod kw { syn::custom_keyword!(set_all); syn::custom_keyword!(signature); syn::custom_keyword!(subclass); + syn::custom_keyword!(submodule); syn::custom_keyword!(text_signature); syn::custom_keyword!(transparent); syn::custom_keyword!(unsendable); @@ -178,6 +179,7 @@ pub type ModuleAttribute = KeywordAttribute; pub type NameAttribute = KeywordAttribute; pub type RenameAllAttribute = KeywordAttribute; pub type TextSignatureAttribute = KeywordAttribute; +pub type SubmoduleAttribute = kw::submodule; impl Parse for KeywordAttribute { fn parse(input: ParseStream<'_>) -> Result { diff --git a/pyo3-macros-backend/src/module.rs b/pyo3-macros-backend/src/module.rs index 4ce3023cbb4..faa7032de80 100644 --- a/pyo3-macros-backend/src/module.rs +++ b/pyo3-macros-backend/src/module.rs @@ -3,6 +3,7 @@ use crate::{ attributes::{ self, take_attributes, take_pyo3_options, CrateAttribute, ModuleAttribute, NameAttribute, + SubmoduleAttribute, }, get_doc, pyclass::PyClassPyO3Option, @@ -27,6 +28,7 @@ pub struct PyModuleOptions { krate: Option, name: Option, module: Option, + is_submodule: bool, } impl PyModuleOptions { @@ -38,6 +40,7 @@ impl PyModuleOptions { PyModulePyO3Option::Name(name) => options.set_name(name.value.0)?, PyModulePyO3Option::Crate(path) => options.set_crate(path)?, PyModulePyO3Option::Module(module) => options.set_module(module)?, + PyModulePyO3Option::Submodule(submod) => options.set_submodule(submod)?, } } @@ -73,9 +76,22 @@ impl PyModuleOptions { self.module = Some(name); Ok(()) } + + fn set_submodule(&mut self, submod: SubmoduleAttribute) -> Result<()> { + ensure_spanned!( + !self.is_submodule, + submod.span() => "`submodule` may only be specified once" + ); + + self.is_submodule = true; + Ok(()) + } } -pub fn pymodule_module_impl(mut module: syn::ItemMod) -> Result { +pub fn pymodule_module_impl( + mut module: syn::ItemMod, + mut is_submodule: bool, +) -> Result { let syn::ItemMod { attrs, vis, @@ -100,6 +116,7 @@ pub fn pymodule_module_impl(mut module: syn::ItemMod) -> Result { } else { name.to_string() }; + is_submodule = is_submodule || options.is_submodule; let mut module_items = Vec::new(); let mut module_items_cfg_attrs = Vec::new(); @@ -297,7 +314,7 @@ pub fn pymodule_module_impl(mut module: syn::ItemMod) -> Result { ) } }}; - let initialization = module_initialization(&name, ctx, module_def); + let initialization = module_initialization(&name, ctx, module_def, is_submodule); Ok(quote!( #(#attrs)* #vis mod #ident { @@ -331,7 +348,7 @@ pub fn pymodule_function_impl(mut function: syn::ItemFn) -> Result let vis = &function.vis; let doc = get_doc(&function.attrs, None, ctx); - let initialization = module_initialization(&name, ctx, quote! { MakeDef::make_def() }); + let initialization = module_initialization(&name, ctx, quote! { MakeDef::make_def() }, false); // Module function called with optional Python<'_> marker as first arg, followed by the module. let mut module_args = Vec::new(); @@ -396,28 +413,37 @@ pub fn pymodule_function_impl(mut function: syn::ItemFn) -> Result }) } -fn module_initialization(name: &syn::Ident, ctx: &Ctx, module_def: TokenStream) -> TokenStream { +fn module_initialization( + name: &syn::Ident, + ctx: &Ctx, + module_def: TokenStream, + is_submodule: bool, +) -> TokenStream { let Ctx { pyo3_path, .. } = ctx; let pyinit_symbol = format!("PyInit_{}", name); let name = name.to_string(); let pyo3_name = LitCStr::new(CString::new(name).unwrap(), Span::call_site(), ctx); - quote! { + let mut result = quote! { #[doc(hidden)] pub const __PYO3_NAME: &'static ::std::ffi::CStr = #pyo3_name; pub(super) struct MakeDef; #[doc(hidden)] pub static _PYO3_DEF: #pyo3_path::impl_::pymodule::ModuleDef = #module_def; - - /// This autogenerated function is called by the python interpreter when importing - /// the module. - #[doc(hidden)] - #[export_name = #pyinit_symbol] - pub unsafe extern "C" fn __pyo3_init() -> *mut #pyo3_path::ffi::PyObject { - #pyo3_path::impl_::trampoline::module_init(|py| _PYO3_DEF.make_module(py)) - } + }; + if !is_submodule { + result.extend(quote! { + /// This autogenerated function is called by the python interpreter when importing + /// the module. + #[doc(hidden)] + #[export_name = #pyinit_symbol] + pub unsafe extern "C" fn __pyo3_init() -> *mut #pyo3_path::ffi::PyObject { + #pyo3_path::impl_::trampoline::module_init(|py| _PYO3_DEF.make_module(py)) + } + }); } + result } /// Finds and takes care of the #[pyfn(...)] in `#[pymodule]` @@ -557,6 +583,7 @@ fn has_pyo3_module_declared( } enum PyModulePyO3Option { + Submodule(SubmoduleAttribute), Crate(CrateAttribute), Name(NameAttribute), Module(ModuleAttribute), @@ -571,6 +598,8 @@ impl Parse for PyModulePyO3Option { input.parse().map(PyModulePyO3Option::Crate) } else if lookahead.peek(attributes::kw::module) { input.parse().map(PyModulePyO3Option::Module) + } else if lookahead.peek(attributes::kw::submodule) { + input.parse().map(PyModulePyO3Option::Submodule) } else { Err(lookahead.error()) } diff --git a/pyo3-macros/src/lib.rs b/pyo3-macros/src/lib.rs index 8dbf2782d5b..95e983079f1 100644 --- a/pyo3-macros/src/lib.rs +++ b/pyo3-macros/src/lib.rs @@ -3,7 +3,7 @@ #![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] use proc_macro::TokenStream; -use proc_macro2::TokenStream as TokenStream2; +use proc_macro2::{Span, TokenStream as TokenStream2}; use pyo3_macros_backend::{ build_derive_from_pyobject, build_py_class, build_py_enum, build_py_function, build_py_methods, pymodule_function_impl, pymodule_module_impl, PyClassArgs, PyClassMethodsType, @@ -35,10 +35,26 @@ use syn::{parse::Nothing, parse_macro_input, Item}; /// [1]: https://pyo3.rs/latest/module.html #[proc_macro_attribute] pub fn pymodule(args: TokenStream, input: TokenStream) -> TokenStream { - parse_macro_input!(args as Nothing); match parse_macro_input!(input as Item) { - Item::Mod(module) => pymodule_module_impl(module), - Item::Fn(function) => pymodule_function_impl(function), + Item::Mod(module) => { + let is_submodule = match parse_macro_input!(args as Option) { + Some(i) if i == "submodule" => true, + Some(_) => { + return syn::Error::new( + Span::call_site(), + "#[pymodule] only accepts submodule as an argument", + ) + .into_compile_error() + .into(); + } + None => false, + }; + pymodule_module_impl(module, is_submodule) + } + Item::Fn(function) => { + parse_macro_input!(args as Nothing); + pymodule_function_impl(function) + } unsupported => Err(syn::Error::new_spanned( unsupported, "#[pymodule] only supports modules and functions.", diff --git a/tests/test_declarative_module.rs b/tests/test_declarative_module.rs index 061d0337285..0bf426a52cc 100644 --- a/tests/test_declarative_module.rs +++ b/tests/test_declarative_module.rs @@ -49,6 +49,10 @@ create_exception!( "Some description." ); +#[pymodule] +#[pyo3(submodule)] +mod external_submodule {} + /// A module written using declarative syntax. #[pymodule] mod declarative_module { @@ -70,6 +74,9 @@ mod declarative_module { #[pymodule_export] use super::some_module::SomeException; + #[pymodule_export] + use super::external_submodule; + #[pymodule] mod inner { use super::*; @@ -108,7 +115,7 @@ mod declarative_module { } } - #[pymodule] + #[pymodule(submodule)] #[pyo3(module = "custom_root")] mod inner_custom_root { use super::*; @@ -174,6 +181,7 @@ fn test_declarative_module() { py_assert!(py, m, "hasattr(m, 'LocatedClass')"); py_assert!(py, m, "isinstance(m.inner.Struct(), m.inner.Struct)"); py_assert!(py, m, "isinstance(m.inner.Enum.A, m.inner.Enum)"); + py_assert!(py, m, "hasattr(m, 'external_submodule')") }) } From ee9123a2d278dee8b461f9c3ee3235dee9c755c0 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Tue, 2 Jul 2024 13:28:26 -0600 Subject: [PATCH 138/495] Fix link in the contribution guide (#4306) --- Contributing.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Contributing.md b/Contributing.md index 1503f803e80..d3c61cf2195 100644 --- a/Contributing.md +++ b/Contributing.md @@ -117,7 +117,7 @@ You can run these tests yourself with #### UI Tests -PyO3 uses [`trybuild`][trybuild] to develop UI tests to capture error messages from the Rust compiler for some of the macro functionality. +PyO3 uses [`trybuild`](https://github.com/dtolnay/trybuild) to develop UI tests to capture error messages from the Rust compiler for some of the macro functionality. Because there are several feature combinations for these UI tests, when updating them all (e.g. for a new Rust compiler version) it may be helpful to use the `update-ui-tests` nox session: From 0af02278342bada5c76bded787d4a7736cbdb96a Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Thu, 4 Jul 2024 11:08:22 +0100 Subject: [PATCH 139/495] fix deprecation warning for trailing optional on `#[setter]` functions (#4304) * fix deprecation warning for trailing optional on `#[setter]` functions * add comment --- newsfragments/4304.fixed.md | 1 + pyo3-macros-backend/src/deprecations.rs | 1 + pyo3-macros-backend/src/method.rs | 19 ++++++- tests/test_getter_setter.rs | 36 +++++++++++++ tests/ui/deprecations.rs | 3 -- tests/ui/deprecations.stderr | 72 +++++++++++-------------- 6 files changed, 88 insertions(+), 44 deletions(-) create mode 100644 newsfragments/4304.fixed.md diff --git a/newsfragments/4304.fixed.md b/newsfragments/4304.fixed.md new file mode 100644 index 00000000000..5f5b5fd8ed0 --- /dev/null +++ b/newsfragments/4304.fixed.md @@ -0,0 +1 @@ +Fix invalid deprecation warning for trailing optional on `#[setter]` function. diff --git a/pyo3-macros-backend/src/deprecations.rs b/pyo3-macros-backend/src/deprecations.rs index 802561a126c..426ca2c0c7d 100644 --- a/pyo3-macros-backend/src/deprecations.rs +++ b/pyo3-macros-backend/src/deprecations.rs @@ -51,6 +51,7 @@ impl<'ctx> ToTokens for Deprecations<'ctx> { pub(crate) fn deprecate_trailing_option_default(spec: &FnSpec<'_>) -> TokenStream { if spec.signature.attribute.is_none() + && spec.tp.signature_attribute_allowed() && spec.signature.arguments.iter().any(|arg| { if let FnArg::Regular(arg) = arg { arg.option_wrapped_type.is_some() diff --git a/pyo3-macros-backend/src/method.rs b/pyo3-macros-backend/src/method.rs index cd06a92c0f7..0d00f952f6c 100644 --- a/pyo3-macros-backend/src/method.rs +++ b/pyo3-macros-backend/src/method.rs @@ -228,6 +228,20 @@ impl FnType { } } + pub fn signature_attribute_allowed(&self) -> bool { + match self { + FnType::Fn(_) + | FnType::FnNew + | FnType::FnStatic + | FnType::FnClass(_) + | FnType::FnNewClass(_) + | FnType::FnModule(_) => true, + // Setter, Getter and ClassAttribute all have fixed signatures (either take 0 or 1 + // arguments) so cannot have a `signature = (...)` attribute. + FnType::Getter(_) | FnType::Setter(_) | FnType::ClassAttribute => false, + } + } + pub fn self_arg( &self, cls: Option<&syn::Type>, @@ -1096,15 +1110,18 @@ fn ensure_signatures_on_valid_method( if let Some(signature) = signature { match fn_type { FnType::Getter(_) => { + debug_assert!(!fn_type.signature_attribute_allowed()); bail_spanned!(signature.kw.span() => "`signature` not allowed with `getter`") } FnType::Setter(_) => { + debug_assert!(!fn_type.signature_attribute_allowed()); bail_spanned!(signature.kw.span() => "`signature` not allowed with `setter`") } FnType::ClassAttribute => { + debug_assert!(!fn_type.signature_attribute_allowed()); bail_spanned!(signature.kw.span() => "`signature` not allowed with `classattr`") } - _ => {} + _ => debug_assert!(fn_type.signature_attribute_allowed()), } } if let Some(text_signature) = text_signature { diff --git a/tests/test_getter_setter.rs b/tests/test_getter_setter.rs index e2b8307fd32..e3852fcd29a 100644 --- a/tests/test_getter_setter.rs +++ b/tests/test_getter_setter.rs @@ -280,3 +280,39 @@ fn frozen_py_field_get() { py_run!(py, inst, "assert inst.value == 'value'"); }); } + +#[test] +fn test_optional_setter() { + #[pyclass] + struct SimpleClass { + field: Option, + } + + #[pymethods] + impl SimpleClass { + #[getter] + fn get_field(&self) -> Option { + self.field + } + + #[setter] + fn set_field(&mut self, field: Option) { + self.field = field; + } + } + + Python::with_gil(|py| { + let instance = Py::new(py, SimpleClass { field: None }).unwrap(); + py_run!(py, instance, "assert instance.field is None"); + py_run!( + py, + instance, + "instance.field = 42; assert instance.field == 42" + ); + py_run!( + py, + instance, + "instance.field = None; assert instance.field is None" + ); + }) +} diff --git a/tests/ui/deprecations.rs b/tests/ui/deprecations.rs index dbd0f8aa462..ff34966e00d 100644 --- a/tests/ui/deprecations.rs +++ b/tests/ui/deprecations.rs @@ -39,9 +39,6 @@ impl MyClass { #[setter] fn set_bar_bound(&self, _value: &Bound<'_, PyAny>) {} - #[setter] - fn set_option(&self, _value: Option) {} - fn __eq__(&self, #[pyo3(from_py_with = "extract_gil_ref")] _other: i32) -> bool { true } diff --git a/tests/ui/deprecations.stderr b/tests/ui/deprecations.stderr index c09894d86c1..4f1797da838 100644 --- a/tests/ui/deprecations.stderr +++ b/tests/ui/deprecations.stderr @@ -10,42 +10,34 @@ note: the lint level is defined here 1 | #![deny(deprecated)] | ^^^^^^^^^^ -error: use of deprecated constant `MyClass::__pymethod_set_set_option__::SIGNATURE`: this function has implicit defaults for the trailing `Option` arguments - = note: these implicit defaults are being phased out - = help: add `#[pyo3(signature = (_value=None))]` to this function to silence this warning and keep the current behavior - --> tests/ui/deprecations.rs:43:8 - | -43 | fn set_option(&self, _value: Option) {} - | ^^^^^^^^^^ - error: use of deprecated constant `__pyfunction_pyfunction_option_2::SIGNATURE`: this function has implicit defaults for the trailing `Option` arguments = note: these implicit defaults are being phased out = help: add `#[pyo3(signature = (_i, _any=None))]` to this function to silence this warning and keep the current behavior - --> tests/ui/deprecations.rs:132:4 + --> tests/ui/deprecations.rs:129:4 | -132 | fn pyfunction_option_2(_i: u32, _any: Option) {} +129 | fn pyfunction_option_2(_i: u32, _any: Option) {} | ^^^^^^^^^^^^^^^^^^^ error: use of deprecated constant `__pyfunction_pyfunction_option_3::SIGNATURE`: this function has implicit defaults for the trailing `Option` arguments = note: these implicit defaults are being phased out = help: add `#[pyo3(signature = (_i, _any=None, _foo=None))]` to this function to silence this warning and keep the current behavior - --> tests/ui/deprecations.rs:135:4 + --> tests/ui/deprecations.rs:132:4 | -135 | fn pyfunction_option_3(_i: u32, _any: Option, _foo: Option) {} +132 | fn pyfunction_option_3(_i: u32, _any: Option, _foo: Option) {} | ^^^^^^^^^^^^^^^^^^^ error: use of deprecated constant `__pyfunction_pyfunction_option_4::SIGNATURE`: this function has implicit defaults for the trailing `Option` arguments = note: these implicit defaults are being phased out = help: add `#[pyo3(signature = (_i, _any=None, _foo=None))]` to this function to silence this warning and keep the current behavior - --> tests/ui/deprecations.rs:138:4 + --> tests/ui/deprecations.rs:135:4 | -138 | fn pyfunction_option_4( +135 | fn pyfunction_option_4( | ^^^^^^^^^^^^^^^^^^^ error: use of deprecated constant `SimpleEnumWithoutEq::__pyo3__generated____richcmp__::DEPRECATION`: Implicit equality for simple enums is deprecated. Use `#[pyclass(eq, eq_int)` to keep the current behavior. - --> tests/ui/deprecations.rs:200:1 + --> tests/ui/deprecations.rs:197:1 | -200 | #[pyclass] +197 | #[pyclass] | ^^^^^^^^^^ | = note: this error originates in the attribute macro `pyclass` (in Nightly builds, run with -Z macro-backtrace for more info) @@ -57,9 +49,9 @@ error: use of deprecated struct `pyo3::PyCell`: `PyCell` was merged into `Bound` | ^^^^^^ error: use of deprecated method `pyo3::deprecations::GilRefs::::from_py_with_arg`: use `&Bound<'_, PyAny>` as the argument for this `from_py_with` extractor - --> tests/ui/deprecations.rs:45:44 + --> tests/ui/deprecations.rs:42:44 | -45 | fn __eq__(&self, #[pyo3(from_py_with = "extract_gil_ref")] _other: i32) -> bool { +42 | fn __eq__(&self, #[pyo3(from_py_with = "extract_gil_ref")] _other: i32) -> bool { | ^^^^^^^^^^^^^^^^^ error: use of deprecated method `pyo3::deprecations::GilRefs::::function_arg`: use `&Bound<'_, T>` instead for this function argument @@ -93,69 +85,69 @@ error: use of deprecated method `pyo3::deprecations::GilRefs::::function_arg` | ^ error: use of deprecated method `pyo3::deprecations::GilRefs::::function_arg`: use `&Bound<'_, T>` instead for this function argument - --> tests/ui/deprecations.rs:64:44 + --> tests/ui/deprecations.rs:61:44 | -64 | fn pyfunction_with_module_gil_ref(_module: &PyModule) -> PyResult<&str> { +61 | fn pyfunction_with_module_gil_ref(_module: &PyModule) -> PyResult<&str> { | ^ error: use of deprecated method `pyo3::deprecations::GilRefs::::function_arg`: use `&Bound<'_, T>` instead for this function argument - --> tests/ui/deprecations.rs:74:19 + --> tests/ui/deprecations.rs:71:19 | -74 | fn module_gil_ref(_m: &PyModule) -> PyResult<()> { +71 | fn module_gil_ref(_m: &PyModule) -> PyResult<()> { | ^^ error: use of deprecated method `pyo3::deprecations::GilRefs::::function_arg`: use `&Bound<'_, T>` instead for this function argument - --> tests/ui/deprecations.rs:79:57 + --> tests/ui/deprecations.rs:76:57 | -79 | fn module_gil_ref_with_explicit_py_arg(_py: Python<'_>, _m: &PyModule) -> PyResult<()> { +76 | fn module_gil_ref_with_explicit_py_arg(_py: Python<'_>, _m: &PyModule) -> PyResult<()> { | ^^ error: use of deprecated method `pyo3::deprecations::GilRefs::::from_py_with_arg`: use `&Bound<'_, PyAny>` as the argument for this `from_py_with` extractor - --> tests/ui/deprecations.rs:115:27 + --> tests/ui/deprecations.rs:112:27 | -115 | #[pyo3(from_py_with = "extract_gil_ref")] _gil_ref: i32, +112 | #[pyo3(from_py_with = "extract_gil_ref")] _gil_ref: i32, | ^^^^^^^^^^^^^^^^^ error: use of deprecated method `pyo3::deprecations::GilRefs::::function_arg`: use `&Bound<'_, T>` instead for this function argument - --> tests/ui/deprecations.rs:121:29 + --> tests/ui/deprecations.rs:118:29 | -121 | fn pyfunction_gil_ref(_any: &PyAny) {} +118 | fn pyfunction_gil_ref(_any: &PyAny) {} | ^ error: use of deprecated method `pyo3::deprecations::OptionGilRefs::>::function_arg`: use `Option<&Bound<'_, T>>` instead for this function argument - --> tests/ui/deprecations.rs:125:36 + --> tests/ui/deprecations.rs:122:36 | -125 | fn pyfunction_option_gil_ref(_any: Option<&PyAny>) {} +122 | fn pyfunction_option_gil_ref(_any: Option<&PyAny>) {} | ^^^^^^ error: use of deprecated method `pyo3::deprecations::GilRefs::::from_py_with_arg`: use `&Bound<'_, PyAny>` as the argument for this `from_py_with` extractor - --> tests/ui/deprecations.rs:150:27 + --> tests/ui/deprecations.rs:147:27 | -150 | #[pyo3(from_py_with = "PyAny::len", item("my_object"))] +147 | #[pyo3(from_py_with = "PyAny::len", item("my_object"))] | ^^^^^^^^^^^^ error: use of deprecated method `pyo3::deprecations::GilRefs::::from_py_with_arg`: use `&Bound<'_, PyAny>` as the argument for this `from_py_with` extractor - --> tests/ui/deprecations.rs:160:27 + --> tests/ui/deprecations.rs:157:27 | -160 | #[pyo3(from_py_with = "PyAny::len")] usize, +157 | #[pyo3(from_py_with = "PyAny::len")] usize, | ^^^^^^^^^^^^ error: use of deprecated method `pyo3::deprecations::GilRefs::::from_py_with_arg`: use `&Bound<'_, PyAny>` as the argument for this `from_py_with` extractor - --> tests/ui/deprecations.rs:166:31 + --> tests/ui/deprecations.rs:163:31 | -166 | Zip(#[pyo3(from_py_with = "extract_gil_ref")] i32), +163 | Zip(#[pyo3(from_py_with = "extract_gil_ref")] i32), | ^^^^^^^^^^^^^^^^^ error: use of deprecated method `pyo3::deprecations::GilRefs::::from_py_with_arg`: use `&Bound<'_, PyAny>` as the argument for this `from_py_with` extractor - --> tests/ui/deprecations.rs:173:27 + --> tests/ui/deprecations.rs:170:27 | -173 | #[pyo3(from_py_with = "extract_gil_ref")] +170 | #[pyo3(from_py_with = "extract_gil_ref")] | ^^^^^^^^^^^^^^^^^ error: use of deprecated method `pyo3::deprecations::GilRefs::>::is_python`: use `wrap_pyfunction_bound!` instead - --> tests/ui/deprecations.rs:186:13 + --> tests/ui/deprecations.rs:183:13 | -186 | let _ = wrap_pyfunction!(double, py); +183 | let _ = wrap_pyfunction!(double, py); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | = note: this error originates in the macro `wrap_pyfunction` (in Nightly builds, run with -Z macro-backtrace for more info) From 5860c4f7e9615afcf33b3905c95211d54242fad7 Mon Sep 17 00:00:00 2001 From: Owen Leung Date: Fri, 5 Jul 2024 17:10:38 +0800 Subject: [PATCH 140/495] implement PartialEq for Pybool & bool (#4305) * implement PartialEq for Pybool implement PartialEq for Pybool * fix failing cargodoc and add changelog file * Use PR number for change log file * Add false checking into test --- newsfragments/4305.added.md | 1 + src/types/boolobject.rs | 139 ++++++++++++++++++++++++++++++++++++ 2 files changed, 140 insertions(+) create mode 100644 newsfragments/4305.added.md diff --git a/newsfragments/4305.added.md b/newsfragments/4305.added.md new file mode 100644 index 00000000000..9a00b8fcfd1 --- /dev/null +++ b/newsfragments/4305.added.md @@ -0,0 +1 @@ +Implement `PartialEq` for `Bound<'py, PyBool>`. \ No newline at end of file diff --git a/src/types/boolobject.rs b/src/types/boolobject.rs index 04c1fd4c113..ee19797d66e 100644 --- a/src/types/boolobject.rs +++ b/src/types/boolobject.rs @@ -72,6 +72,86 @@ impl<'py> PyBoolMethods<'py> for Bound<'py, PyBool> { } } +/// Compare `Bound` with `bool`. +impl PartialEq for Bound<'_, PyBool> { + #[inline] + fn eq(&self, other: &bool) -> bool { + self.as_borrowed() == *other + } +} + +/// Compare `&Bound` with `bool`. +impl PartialEq for &'_ Bound<'_, PyBool> { + #[inline] + fn eq(&self, other: &bool) -> bool { + self.as_borrowed() == *other + } +} + +/// Compare `Bound` with `&bool`. +impl PartialEq<&'_ bool> for Bound<'_, PyBool> { + #[inline] + fn eq(&self, other: &&bool) -> bool { + self.as_borrowed() == **other + } +} + +/// Compare `bool` with `Bound` +impl PartialEq> for bool { + #[inline] + fn eq(&self, other: &Bound<'_, PyBool>) -> bool { + *self == other.as_borrowed() + } +} + +/// Compare `bool` with `&Bound` +impl PartialEq<&'_ Bound<'_, PyBool>> for bool { + #[inline] + fn eq(&self, other: &&'_ Bound<'_, PyBool>) -> bool { + *self == other.as_borrowed() + } +} + +/// Compare `&bool` with `Bound` +impl PartialEq> for &'_ bool { + #[inline] + fn eq(&self, other: &Bound<'_, PyBool>) -> bool { + **self == other.as_borrowed() + } +} + +/// Compare `Borrowed` with `bool` +impl PartialEq for Borrowed<'_, '_, PyBool> { + #[inline] + fn eq(&self, other: &bool) -> bool { + self.is_true() == *other + } +} + +/// Compare `Borrowed` with `&bool` +impl PartialEq<&bool> for Borrowed<'_, '_, PyBool> { + #[inline] + fn eq(&self, other: &&bool) -> bool { + self.is_true() == **other + } +} + +/// Compare `bool` with `Borrowed` +impl PartialEq> for bool { + #[inline] + fn eq(&self, other: &Borrowed<'_, '_, PyBool>) -> bool { + *self == other.is_true() + } +} + +/// Compare `&bool` with `Borrowed` +impl PartialEq> for &'_ bool { + #[inline] + fn eq(&self, other: &Borrowed<'_, '_, PyBool>) -> bool { + **self == other.is_true() + } +} + /// Converts a Rust `bool` to a Python `bool`. impl ToPyObject for bool { #[inline] @@ -191,4 +271,63 @@ mod tests { assert!(false.to_object(py).is(&*PyBool::new_bound(py, false))); }); } + + #[test] + fn test_pybool_comparisons() { + Python::with_gil(|py| { + let py_bool = PyBool::new_bound(py, true); + let py_bool_false = PyBool::new_bound(py, false); + let rust_bool = true; + + // Bound<'_, PyBool> == bool + assert_eq!(*py_bool, rust_bool); + assert_ne!(*py_bool_false, rust_bool); + + // Bound<'_, PyBool> == &bool + assert_eq!(*py_bool, &rust_bool); + assert_ne!(*py_bool_false, &rust_bool); + + // &Bound<'_, PyBool> == bool + assert_eq!(&*py_bool, rust_bool); + assert_ne!(&*py_bool_false, rust_bool); + + // &Bound<'_, PyBool> == &bool + assert_eq!(&*py_bool, &rust_bool); + assert_ne!(&*py_bool_false, &rust_bool); + + // bool == Bound<'_, PyBool> + assert_eq!(rust_bool, *py_bool); + assert_ne!(rust_bool, *py_bool_false); + + // bool == &Bound<'_, PyBool> + assert_eq!(rust_bool, &*py_bool); + assert_ne!(rust_bool, &*py_bool_false); + + // &bool == Bound<'_, PyBool> + assert_eq!(&rust_bool, *py_bool); + assert_ne!(&rust_bool, *py_bool_false); + + // &bool == &Bound<'_, PyBool> + assert_eq!(&rust_bool, &*py_bool); + assert_ne!(&rust_bool, &*py_bool_false); + + // Borrowed<'_, '_, PyBool> == bool + assert_eq!(py_bool, rust_bool); + assert_ne!(py_bool_false, rust_bool); + + // Borrowed<'_, '_, PyBool> == &bool + assert_eq!(py_bool, &rust_bool); + assert_ne!(py_bool_false, &rust_bool); + + // bool == Borrowed<'_, '_, PyBool> + assert_eq!(rust_bool, py_bool); + assert_ne!(rust_bool, py_bool_false); + + // &bool == Borrowed<'_, '_, PyBool> + assert_eq!(&rust_bool, py_bool); + assert_ne!(&rust_bool, py_bool_false); + assert_eq!(py_bool, rust_bool); + assert_ne!(py_bool_false, rust_bool); + }) + } } From 9afc38ae416bb750efd227e4f8b4302392c0d303 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Fri, 5 Jul 2024 05:16:06 -0400 Subject: [PATCH 141/495] fixes #4285 -- allow full-path to pymodule with nested declarative modules (#4288) --- newsfragments/4288.fixed.md | 1 + pyo3-macros-backend/src/module.rs | 88 ++++++++++++++++++++++++++++--- tests/test_declarative_module.rs | 11 ++++ 3 files changed, 94 insertions(+), 6 deletions(-) create mode 100644 newsfragments/4288.fixed.md diff --git a/newsfragments/4288.fixed.md b/newsfragments/4288.fixed.md new file mode 100644 index 00000000000..105bb042276 --- /dev/null +++ b/newsfragments/4288.fixed.md @@ -0,0 +1 @@ +allow `#[pyo3::prelude::pymodule]` with nested declarative modules diff --git a/pyo3-macros-backend/src/module.rs b/pyo3-macros-backend/src/module.rs index faa7032de80..2ca084a6a4b 100644 --- a/pyo3-macros-backend/src/module.rs +++ b/pyo3-macros-backend/src/module.rs @@ -8,7 +8,7 @@ use crate::{ get_doc, pyclass::PyClassPyO3Option, pyfunction::{impl_wrap_pyfunction, PyFunctionOptions}, - utils::{Ctx, LitCStr}, + utils::{Ctx, LitCStr, PyO3CratePath}, }; use proc_macro2::{Span, TokenStream}; use quote::quote; @@ -183,7 +183,18 @@ pub fn pymodule_module_impl( ); ensure_spanned!(pymodule_init.is_none(), item_fn.span() => "only one `#[pymodule_init]` may be specified"); pymodule_init = Some(quote! { #ident(module)?; }); - } else if has_attribute(&item_fn.attrs, "pyfunction") { + } else if has_attribute(&item_fn.attrs, "pyfunction") + || has_attribute_with_namespace( + &item_fn.attrs, + Some(pyo3_path), + &["pyfunction"], + ) + || has_attribute_with_namespace( + &item_fn.attrs, + Some(pyo3_path), + &["prelude", "pyfunction"], + ) + { module_items.push(ident.clone()); module_items_cfg_attrs.push(get_cfg_attributes(&item_fn.attrs)); } @@ -193,7 +204,18 @@ pub fn pymodule_module_impl( !has_attribute(&item_struct.attrs, "pymodule_export"), item.span() => "`#[pymodule_export]` may only be used on `use` statements" ); - if has_attribute(&item_struct.attrs, "pyclass") { + if has_attribute(&item_struct.attrs, "pyclass") + || has_attribute_with_namespace( + &item_struct.attrs, + Some(pyo3_path), + &["pyclass"], + ) + || has_attribute_with_namespace( + &item_struct.attrs, + Some(pyo3_path), + &["prelude", "pyclass"], + ) + { module_items.push(item_struct.ident.clone()); module_items_cfg_attrs.push(get_cfg_attributes(&item_struct.attrs)); if !has_pyo3_module_declared::( @@ -210,7 +232,14 @@ pub fn pymodule_module_impl( !has_attribute(&item_enum.attrs, "pymodule_export"), item.span() => "`#[pymodule_export]` may only be used on `use` statements" ); - if has_attribute(&item_enum.attrs, "pyclass") { + if has_attribute(&item_enum.attrs, "pyclass") + || has_attribute_with_namespace(&item_enum.attrs, Some(pyo3_path), &["pyclass"]) + || has_attribute_with_namespace( + &item_enum.attrs, + Some(pyo3_path), + &["prelude", "pyclass"], + ) + { module_items.push(item_enum.ident.clone()); module_items_cfg_attrs.push(get_cfg_attributes(&item_enum.attrs)); if !has_pyo3_module_declared::( @@ -227,7 +256,14 @@ pub fn pymodule_module_impl( !has_attribute(&item_mod.attrs, "pymodule_export"), item.span() => "`#[pymodule_export]` may only be used on `use` statements" ); - if has_attribute(&item_mod.attrs, "pymodule") { + if has_attribute(&item_mod.attrs, "pymodule") + || has_attribute_with_namespace(&item_mod.attrs, Some(pyo3_path), &["pymodule"]) + || has_attribute_with_namespace( + &item_mod.attrs, + Some(pyo3_path), + &["prelude", "pymodule"], + ) + { module_items.push(item_mod.ident.clone()); module_items_cfg_attrs.push(get_cfg_attributes(&item_mod.attrs)); if !has_pyo3_module_declared::( @@ -555,8 +591,48 @@ fn find_and_remove_attribute(attrs: &mut Vec, ident: &str) -> bo found } +enum IdentOrStr<'a> { + Str(&'a str), + Ident(syn::Ident), +} + +impl<'a> PartialEq for IdentOrStr<'a> { + fn eq(&self, other: &syn::Ident) -> bool { + match self { + IdentOrStr::Str(s) => other == s, + IdentOrStr::Ident(i) => other == i, + } + } +} fn has_attribute(attrs: &[syn::Attribute], ident: &str) -> bool { - attrs.iter().any(|attr| attr.path().is_ident(ident)) + has_attribute_with_namespace(attrs, None, &[ident]) +} + +fn has_attribute_with_namespace( + attrs: &[syn::Attribute], + crate_path: Option<&PyO3CratePath>, + idents: &[&str], +) -> bool { + let mut segments = vec![]; + if let Some(c) = crate_path { + match c { + PyO3CratePath::Given(paths) => { + for p in &paths.segments { + segments.push(IdentOrStr::Ident(p.ident.clone())); + } + } + PyO3CratePath::Default => segments.push(IdentOrStr::Str("pyo3")), + } + }; + for i in idents { + segments.push(IdentOrStr::Str(i)); + } + + attrs.iter().any(|attr| { + segments + .iter() + .eq(attr.path().segments.iter().map(|v| &v.ident)) + }) } fn set_module_attribute(attrs: &mut Vec, module_name: &str) { diff --git a/tests/test_declarative_module.rs b/tests/test_declarative_module.rs index 0bf426a52cc..f62d51822ee 100644 --- a/tests/test_declarative_module.rs +++ b/tests/test_declarative_module.rs @@ -124,6 +124,9 @@ mod declarative_module { struct Struct; } + #[pyo3::prelude::pymodule] + mod full_path_inner {} + #[pymodule_init] fn init(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add("double2", m.getattr("double")?) @@ -247,3 +250,11 @@ fn test_module_names() { ); }) } + +#[test] +fn test_inner_module_full_path() { + Python::with_gil(|py| { + let m = declarative_module(py); + py_assert!(py, m, "m.full_path_inner"); + }) +} From 59c4fa3f249aa19eb9cb80fe418298c8267c00b2 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Sat, 6 Jul 2024 23:00:28 +0100 Subject: [PATCH 142/495] release: 0.22.1 (#4314) --- CHANGELOG.md | 19 ++++++++++++++++++- Cargo.toml | 8 ++++---- README.md | 4 ++-- 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/4287.changed.md | 1 - newsfragments/4288.fixed.md | 1 - newsfragments/4291.fixed.md | 1 - newsfragments/4297.fixed.md | 1 - newsfragments/4301.added.md | 1 - newsfragments/4304.fixed.md | 1 - newsfragments/4305.added.md | 1 - pyo3-build-config/Cargo.toml | 2 +- pyo3-ffi/Cargo.toml | 4 ++-- pyo3-macros-backend/Cargo.toml | 6 +++--- pyo3-macros/Cargo.toml | 4 ++-- pyproject.toml | 2 +- 20 files changed, 38 insertions(+), 28 deletions(-) delete mode 100644 newsfragments/4287.changed.md delete mode 100644 newsfragments/4288.fixed.md delete mode 100644 newsfragments/4291.fixed.md delete mode 100644 newsfragments/4297.fixed.md delete mode 100644 newsfragments/4301.added.md delete mode 100644 newsfragments/4304.fixed.md delete mode 100644 newsfragments/4305.added.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 128544202cf..beabedc28de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,22 @@ To see unreleased changes, please see the [CHANGELOG on the main branch guide](h +## [0.22.1] - 2024-07-06 + +### Added + +- Add `#[pyo3(submodule)]` option for declarative `#[pymodule]`s. [#4301](https://github.com/PyO3/pyo3/pull/4301) +- Implement `PartialEq` for `Bound<'py, PyBool>`. [#4305](https://github.com/PyO3/pyo3/pull/4305) + +### Fixed + +- Return `NotImplemented` instead of raising `TypeError` from generated equality method when comparing different types. [#4287](https://github.com/PyO3/pyo3/pull/4287) +- Handle full-path `#[pyo3::prelude::pymodule]` and similar for `#[pyclass]` and `#[pyfunction]` in declarative modules.[#4288](https://github.com/PyO3/pyo3/pull/4288) +- Fix 128-bit int regression on big-endian platforms with Python <3.13. [#4291](https://github.com/PyO3/pyo3/pull/4291) +- Stop generating code that will never be covered with declarative modules. [#4297](https://github.com/PyO3/pyo3/pull/4297) +- Fix invalid deprecation warning for trailing optional on `#[setter]` function. [#4304](https://github.com/PyO3/pyo3/pull/4304) + + ## [0.22.0] - 2024-06-24 ### Packaging @@ -1808,7 +1824,8 @@ Yanked - Initial release -[Unreleased]: https://github.com/pyo3/pyo3/compare/v0.22.0...HEAD +[Unreleased]: https://github.com/pyo3/pyo3/compare/v0.22.1...HEAD +[0.22.1]: https://github.com/pyo3/pyo3/compare/v0.22.0...v0.22.1 [0.22.0]: https://github.com/pyo3/pyo3/compare/v0.21.2...v0.22.0 [0.21.2]: https://github.com/pyo3/pyo3/compare/v0.21.1...v0.21.2 [0.21.1]: https://github.com/pyo3/pyo3/compare/v0.21.0...v0.21.1 diff --git a/Cargo.toml b/Cargo.toml index 5d3888dfda1..364fbac81c1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3" -version = "0.22.0" +version = "0.22.1" description = "Bindings to Python interpreter" authors = ["PyO3 Project and Contributors "] readme = "README.md" @@ -21,10 +21,10 @@ memoffset = "0.9" once_cell = "1.13.0" # ffi bindings to the python interpreter, split into a separate crate so they can be used independently -pyo3-ffi = { path = "pyo3-ffi", version = "=0.22.0" } +pyo3-ffi = { path = "pyo3-ffi", version = "=0.22.1" } # support crates for macros feature -pyo3-macros = { path = "pyo3-macros", version = "=0.22.0", optional = true } +pyo3-macros = { path = "pyo3-macros", version = "=0.22.1", optional = true } indoc = { version = "2.0.1", optional = true } unindent = { version = "0.2.1", optional = true } @@ -63,7 +63,7 @@ rayon = "1.6.1" futures = "0.3.28" [build-dependencies] -pyo3-build-config = { path = "pyo3-build-config", version = "=0.22.0", features = ["resolve-config"] } +pyo3-build-config = { path = "pyo3-build-config", version = "=0.22.1", features = ["resolve-config"] } [features] default = ["macros"] diff --git a/README.md b/README.md index 9f08dc0ea28..d5be31967a7 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ name = "string_sum" crate-type = ["cdylib"] [dependencies] -pyo3 = { version = "0.22.0", features = ["extension-module"] } +pyo3 = { version = "0.22.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.22.0" +version = "0.22.1" features = ["auto-initialize"] ``` diff --git a/examples/decorator/.template/pre-script.rhai b/examples/decorator/.template/pre-script.rhai index aea6c46edc6..f21daafeb2a 100644 --- a/examples/decorator/.template/pre-script.rhai +++ b/examples/decorator/.template/pre-script.rhai @@ -1,4 +1,4 @@ -variable::set("PYO3_VERSION", "0.22.0"); +variable::set("PYO3_VERSION", "0.22.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 aea6c46edc6..f21daafeb2a 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.22.0"); +variable::set("PYO3_VERSION", "0.22.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 d5f84ed3ff8..086868dfc42 100644 --- a/examples/plugin/.template/pre-script.rhai +++ b/examples/plugin/.template/pre-script.rhai @@ -1,4 +1,4 @@ -variable::set("PYO3_VERSION", "0.22.0"); +variable::set("PYO3_VERSION", "0.22.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 5ea757803da..0679e89ab3f 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.22.0"); +variable::set("PYO3_VERSION", "0.22.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 aea6c46edc6..f21daafeb2a 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.22.0"); +variable::set("PYO3_VERSION", "0.22.1"); file::rename(".template/Cargo.toml", "Cargo.toml"); file::rename(".template/pyproject.toml", "pyproject.toml"); file::delete(".template"); diff --git a/newsfragments/4287.changed.md b/newsfragments/4287.changed.md deleted file mode 100644 index 440e123cebf..00000000000 --- a/newsfragments/4287.changed.md +++ /dev/null @@ -1 +0,0 @@ -Return `NotImplemented` from generated equality method when comparing different types. diff --git a/newsfragments/4288.fixed.md b/newsfragments/4288.fixed.md deleted file mode 100644 index 105bb042276..00000000000 --- a/newsfragments/4288.fixed.md +++ /dev/null @@ -1 +0,0 @@ -allow `#[pyo3::prelude::pymodule]` with nested declarative modules diff --git a/newsfragments/4291.fixed.md b/newsfragments/4291.fixed.md deleted file mode 100644 index 38242ff7de9..00000000000 --- a/newsfragments/4291.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Fix 128-bit int regression on big-endian platforms with Python <3.13 diff --git a/newsfragments/4297.fixed.md b/newsfragments/4297.fixed.md deleted file mode 100644 index 18cfd93756f..00000000000 --- a/newsfragments/4297.fixed.md +++ /dev/null @@ -1 +0,0 @@ -stop generating code that will never be covered with declarative modules diff --git a/newsfragments/4301.added.md b/newsfragments/4301.added.md deleted file mode 100644 index 2ee759c28b5..00000000000 --- a/newsfragments/4301.added.md +++ /dev/null @@ -1 +0,0 @@ -allow setting `submodule` on declarative `#[pymodule]`s diff --git a/newsfragments/4304.fixed.md b/newsfragments/4304.fixed.md deleted file mode 100644 index 5f5b5fd8ed0..00000000000 --- a/newsfragments/4304.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Fix invalid deprecation warning for trailing optional on `#[setter]` function. diff --git a/newsfragments/4305.added.md b/newsfragments/4305.added.md deleted file mode 100644 index 9a00b8fcfd1..00000000000 --- a/newsfragments/4305.added.md +++ /dev/null @@ -1 +0,0 @@ -Implement `PartialEq` for `Bound<'py, PyBool>`. \ No newline at end of file diff --git a/pyo3-build-config/Cargo.toml b/pyo3-build-config/Cargo.toml index 6ab56e2ee14..d8c84685c7c 100644 --- a/pyo3-build-config/Cargo.toml +++ b/pyo3-build-config/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3-build-config" -version = "0.22.0" +version = "0.22.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 c3d9c608b0c..85de25c80f0 100644 --- a/pyo3-ffi/Cargo.toml +++ b/pyo3-ffi/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3-ffi" -version = "0.22.0" +version = "0.22.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.22.0", features = ["resolve-config"] } +pyo3-build-config = { path = "../pyo3-build-config", version = "=0.22.1", features = ["resolve-config"] } [lints] workspace = true diff --git a/pyo3-macros-backend/Cargo.toml b/pyo3-macros-backend/Cargo.toml index 264134d5249..280e12e37a6 100644 --- a/pyo3-macros-backend/Cargo.toml +++ b/pyo3-macros-backend/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3-macros-backend" -version = "0.22.0" +version = "0.22.1" description = "Code generation for PyO3 package" authors = ["PyO3 Project and Contributors "] keywords = ["pyo3", "python", "cpython", "ffi"] @@ -16,7 +16,7 @@ edition = "2021" [dependencies] heck = "0.5" proc-macro2 = { version = "1.0.60", default-features = false } -pyo3-build-config = { path = "../pyo3-build-config", version = "=0.22.0", features = ["resolve-config"] } +pyo3-build-config = { path = "../pyo3-build-config", version = "=0.22.1", features = ["resolve-config"] } quote = { version = "1", default-features = false } [dependencies.syn] @@ -25,7 +25,7 @@ default-features = false features = ["derive", "parsing", "printing", "clone-impls", "full", "extra-traits"] [build-dependencies] -pyo3-build-config = { path = "../pyo3-build-config", version = "=0.22.0" } +pyo3-build-config = { path = "../pyo3-build-config", version = "=0.22.1" } [lints] workspace = true diff --git a/pyo3-macros/Cargo.toml b/pyo3-macros/Cargo.toml index 0dbbdd7cac9..23da8ccd697 100644 --- a/pyo3-macros/Cargo.toml +++ b/pyo3-macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3-macros" -version = "0.22.0" +version = "0.22.1" description = "Proc macros for PyO3 package" authors = ["PyO3 Project and Contributors "] keywords = ["pyo3", "python", "cpython", "ffi"] @@ -22,7 +22,7 @@ gil-refs = ["pyo3-macros-backend/gil-refs"] proc-macro2 = { version = "1.0.60", default-features = false } quote = "1" syn = { version = "2", features = ["full", "extra-traits"] } -pyo3-macros-backend = { path = "../pyo3-macros-backend", version = "=0.22.0" } +pyo3-macros-backend = { path = "../pyo3-macros-backend", version = "=0.22.1" } [lints] workspace = true diff --git a/pyproject.toml b/pyproject.toml index ab846250956..26cbfee0064 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ [tool.towncrier] filename = "CHANGELOG.md" -version = "0.22.0" +version = "0.22.1" start_string = "\n" template = ".towncrier.template.md" title_format = "## [{version}] - {project_date}" From d5c886f4c055d4a1241d555a9b03811c44b13399 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Sun, 7 Jul 2024 07:53:43 +0100 Subject: [PATCH 143/495] simplify implementation of `Py::clone_ref` (#4313) --- src/instance.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/instance.rs b/src/instance.rs index 4703dd12a94..cc1ae684e44 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -1329,8 +1329,11 @@ impl Py { /// # } /// ``` #[inline] - pub fn clone_ref(&self, py: Python<'_>) -> Py { - unsafe { Py::from_borrowed_ptr(py, self.0.as_ptr()) } + pub fn clone_ref(&self, _py: Python<'_>) -> Py { + unsafe { + ffi::Py_INCREF(self.as_ptr()); + Self::from_non_null(self.0) + } } /// Drops `self` and immediately decreases its reference count. From 3c155d9fefea89d448c647b5906fe6b40bda3443 Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Mon, 8 Jul 2024 15:40:27 +0200 Subject: [PATCH 144/495] remove the gil-ref deprecations infrastructure (#4320) --- pyo3-macros-backend/src/frompyobject.rs | 170 ++++++------------------ pyo3-macros-backend/src/method.rs | 14 +- pyo3-macros-backend/src/module.rs | 27 +--- pyo3-macros-backend/src/params.rs | 64 +-------- pyo3-macros-backend/src/pymethod.rs | 98 ++++++-------- src/impl_/deprecations.rs | 72 ---------- src/macros.rs | 6 +- tests/test_compile_error.rs | 1 - tests/ui/deprecations.rs | 156 ---------------------- tests/ui/deprecations.stderr | 146 +++----------------- 10 files changed, 104 insertions(+), 650 deletions(-) diff --git a/pyo3-macros-backend/src/frompyobject.rs b/pyo3-macros-backend/src/frompyobject.rs index d7767174e62..a20eeec9ffd 100644 --- a/pyo3-macros-backend/src/frompyobject.rs +++ b/pyo3-macros-backend/src/frompyobject.rs @@ -1,7 +1,7 @@ use crate::attributes::{self, get_pyo3_options, CrateAttribute, FromPyWithAttribute}; use crate::utils::Ctx; use proc_macro2::TokenStream; -use quote::{format_ident, quote, quote_spanned}; +use quote::{format_ident, quote}; use syn::{ parenthesized, parse::{Parse, ParseStream}, @@ -44,16 +44,14 @@ impl<'a> Enum<'a> { } /// Build derivation body for enums. - fn build(&self, ctx: &Ctx) -> (TokenStream, TokenStream) { + fn build(&self, ctx: &Ctx) -> TokenStream { let Ctx { pyo3_path, .. } = ctx; let mut var_extracts = Vec::new(); let mut variant_names = Vec::new(); let mut error_names = Vec::new(); - let mut deprecations = TokenStream::new(); for var in &self.variants { - let (struct_derive, dep) = var.build(ctx); - deprecations.extend(dep); + let struct_derive = var.build(ctx); let ext = quote!({ let maybe_ret = || -> #pyo3_path::PyResult { #struct_derive @@ -70,22 +68,19 @@ impl<'a> Enum<'a> { error_names.push(&var.err_name); } let ty_name = self.enum_ident.to_string(); - ( - quote!( - let errors = [ - #(#var_extracts),* - ]; - ::std::result::Result::Err( - #pyo3_path::impl_::frompyobject::failed_to_extract_enum( - obj.py(), - #ty_name, - &[#(#variant_names),*], - &[#(#error_names),*], - &errors - ) + quote!( + let errors = [ + #(#var_extracts),* + ]; + ::std::result::Result::Err( + #pyo3_path::impl_::frompyobject::failed_to_extract_enum( + obj.py(), + #ty_name, + &[#(#variant_names),*], + &[#(#error_names),*], + &errors ) - ), - deprecations, + ) ) } } @@ -244,7 +239,7 @@ impl<'a> Container<'a> { } /// Build derivation body for a struct. - fn build(&self, ctx: &Ctx) -> (TokenStream, TokenStream) { + fn build(&self, ctx: &Ctx) -> TokenStream { match &self.ty { ContainerType::StructNewtype(ident, from_py_with) => { self.build_newtype_struct(Some(ident), from_py_with, ctx) @@ -262,73 +257,42 @@ impl<'a> Container<'a> { field_ident: Option<&Ident>, from_py_with: &Option, ctx: &Ctx, - ) -> (TokenStream, TokenStream) { + ) -> TokenStream { let Ctx { pyo3_path, .. } = ctx; let self_ty = &self.path; let struct_name = self.name(); if let Some(ident) = field_ident { let field_name = ident.to_string(); match from_py_with { - None => ( - quote! { - Ok(#self_ty { - #ident: #pyo3_path::impl_::frompyobject::extract_struct_field(obj, #struct_name, #field_name)? - }) - }, - TokenStream::new(), - ), + None => quote! { + Ok(#self_ty { + #ident: #pyo3_path::impl_::frompyobject::extract_struct_field(obj, #struct_name, #field_name)? + }) + }, Some(FromPyWithAttribute { value: expr_path, .. - }) => ( - quote! { - Ok(#self_ty { - #ident: #pyo3_path::impl_::frompyobject::extract_struct_field_with(#expr_path as fn(_) -> _, obj, #struct_name, #field_name)? - }) - }, - quote_spanned! { expr_path.span() => - const _: () = { - fn check_from_py_with() { - let e = #pyo3_path::impl_::deprecations::GilRefs::new(); - #pyo3_path::impl_::deprecations::inspect_fn(#expr_path, &e); - e.from_py_with_arg(); - } - }; - }, - ), + }) => quote! { + Ok(#self_ty { + #ident: #pyo3_path::impl_::frompyobject::extract_struct_field_with(#expr_path as fn(_) -> _, obj, #struct_name, #field_name)? + }) + }, } } else { match from_py_with { - None => ( - quote!( - #pyo3_path::impl_::frompyobject::extract_tuple_struct_field(obj, #struct_name, 0).map(#self_ty) - ), - TokenStream::new(), - ), + None => quote! { + #pyo3_path::impl_::frompyobject::extract_tuple_struct_field(obj, #struct_name, 0).map(#self_ty) + }, + Some(FromPyWithAttribute { value: expr_path, .. - }) => ( - quote! ( - #pyo3_path::impl_::frompyobject::extract_tuple_struct_field_with(#expr_path as fn(_) -> _, obj, #struct_name, 0).map(#self_ty) - ), - quote_spanned! { expr_path.span() => - const _: () = { - fn check_from_py_with() { - let e = #pyo3_path::impl_::deprecations::GilRefs::new(); - #pyo3_path::impl_::deprecations::inspect_fn(#expr_path, &e); - e.from_py_with_arg(); - } - }; - }, - ), + }) => quote! { + #pyo3_path::impl_::frompyobject::extract_tuple_struct_field_with(#expr_path as fn(_) -> _, obj, #struct_name, 0).map(#self_ty) + }, } } } - fn build_tuple_struct( - &self, - struct_fields: &[TupleStructField], - ctx: &Ctx, - ) -> (TokenStream, TokenStream) { + fn build_tuple_struct(&self, struct_fields: &[TupleStructField], ctx: &Ctx) -> TokenStream { let Ctx { pyo3_path, .. } = ctx; let self_ty = &self.path; let struct_name = &self.name(); @@ -348,40 +312,15 @@ impl<'a> Container<'a> { } }); - let deprecations = struct_fields - .iter() - .filter_map(|field| { - let FromPyWithAttribute { - value: expr_path, .. - } = field.from_py_with.as_ref()?; - Some(quote_spanned! { expr_path.span() => - const _: () = { - fn check_from_py_with() { - let e = #pyo3_path::impl_::deprecations::GilRefs::new(); - #pyo3_path::impl_::deprecations::inspect_fn(#expr_path, &e); - e.from_py_with_arg(); - } - }; - }) - }) - .collect::(); - - ( - quote!( - match #pyo3_path::types::PyAnyMethods::extract(obj) { - ::std::result::Result::Ok((#(#field_idents),*)) => ::std::result::Result::Ok(#self_ty(#(#fields),*)), - ::std::result::Result::Err(err) => ::std::result::Result::Err(err), - } - ), - deprecations, + quote!( + match #pyo3_path::types::PyAnyMethods::extract(obj) { + ::std::result::Result::Ok((#(#field_idents),*)) => ::std::result::Result::Ok(#self_ty(#(#fields),*)), + ::std::result::Result::Err(err) => ::std::result::Result::Err(err), + } ) } - fn build_struct( - &self, - struct_fields: &[NamedStructField<'_>], - ctx: &Ctx, - ) -> (TokenStream, TokenStream) { + fn build_struct(&self, struct_fields: &[NamedStructField<'_>], ctx: &Ctx) -> TokenStream { let Ctx { pyo3_path, .. } = ctx; let self_ty = &self.path; let struct_name = &self.name(); @@ -420,28 +359,7 @@ impl<'a> Container<'a> { fields.push(quote!(#ident: #extractor)); } - let deprecations = struct_fields - .iter() - .filter_map(|field| { - let FromPyWithAttribute { - value: expr_path, .. - } = field.from_py_with.as_ref()?; - Some(quote_spanned! { expr_path.span() => - const _: () = { - fn check_from_py_with() { - let e = #pyo3_path::impl_::deprecations::GilRefs::new(); - #pyo3_path::impl_::deprecations::inspect_fn(#expr_path, &e); - e.from_py_with_arg(); - } - }; - }) - }) - .collect::(); - - ( - quote!(::std::result::Result::Ok(#self_ty{#fields})), - deprecations, - ) + quote!(::std::result::Result::Ok(#self_ty{#fields})) } } @@ -673,7 +591,7 @@ pub fn build_derive_from_pyobject(tokens: &DeriveInput) -> Result { let ctx = &Ctx::new(&options.krate, None); let Ctx { pyo3_path, .. } = &ctx; - let (derives, from_py_with_deprecations) = match &tokens.data { + let derives = match &tokens.data { syn::Data::Enum(en) => { if options.transparent || options.annotation.is_some() { bail_spanned!(tokens.span() => "`transparent` or `annotation` is not supported \ @@ -703,7 +621,5 @@ pub fn build_derive_from_pyobject(tokens: &DeriveInput) -> Result { #derives } } - - #from_py_with_deprecations )) } diff --git a/pyo3-macros-backend/src/method.rs b/pyo3-macros-backend/src/method.rs index 0d00f952f6c..3efcc3e9967 100644 --- a/pyo3-macros-backend/src/method.rs +++ b/pyo3-macros-backend/src/method.rs @@ -683,11 +683,10 @@ impl<'a> FnSpec<'a> { } _ => { if let Some(self_arg) = self_arg() { - let self_checker = holders.push_gil_refs_checker(self_arg.span()); quote! { function( // NB #self_arg includes a comma, so none inserted here - #pyo3_path::impl_::deprecations::inspect_type(#self_arg &#self_checker), + #self_arg #(#args),* ) } @@ -714,11 +713,10 @@ impl<'a> FnSpec<'a> { } call } else if let Some(self_arg) = self_arg() { - let self_checker = holders.push_gil_refs_checker(self_arg.span()); quote! { function( // NB #self_arg includes a comma, so none inserted here - #pyo3_path::impl_::deprecations::inspect_type(#self_arg &#self_checker), + #self_arg #(#args),* ) } @@ -762,7 +760,6 @@ impl<'a> FnSpec<'a> { }) .collect(); let call = rust_call(args, &mut holders); - let check_gil_refs = holders.check_gil_refs(); let init_holders = holders.init_holders(ctx); quote! { unsafe fn #ident<'py>( @@ -774,7 +771,6 @@ impl<'a> FnSpec<'a> { let function = #rust_name; // Shadow the function name to avoid #3017 #init_holders let result = #call; - #check_gil_refs result } } @@ -784,7 +780,6 @@ impl<'a> FnSpec<'a> { let (arg_convert, args) = impl_arg_params(self, cls, true, &mut holders, ctx); let call = rust_call(args, &mut holders); let init_holders = holders.init_holders(ctx); - let check_gil_refs = holders.check_gil_refs(); quote! { unsafe fn #ident<'py>( @@ -800,7 +795,6 @@ impl<'a> FnSpec<'a> { #arg_convert #init_holders let result = #call; - #check_gil_refs result } } @@ -810,7 +804,6 @@ impl<'a> FnSpec<'a> { let (arg_convert, args) = impl_arg_params(self, cls, false, &mut holders, ctx); let call = rust_call(args, &mut holders); let init_holders = holders.init_holders(ctx); - let check_gil_refs = holders.check_gil_refs(); quote! { unsafe fn #ident<'py>( @@ -825,7 +818,6 @@ impl<'a> FnSpec<'a> { #arg_convert #init_holders let result = #call; - #check_gil_refs result } } @@ -838,7 +830,6 @@ impl<'a> FnSpec<'a> { .self_arg(cls, ExtractErrorMode::Raise, &mut holders, ctx); let call = quote_spanned! {*output_span=> #rust_name(#self_arg #(#args),*) }; let init_holders = holders.init_holders(ctx); - let check_gil_refs = holders.check_gil_refs(); quote! { unsafe fn #ident( py: #pyo3_path::Python<'_>, @@ -854,7 +845,6 @@ impl<'a> FnSpec<'a> { #init_holders let result = #call; let initializer: #pyo3_path::PyClassInitializer::<#cls> = result.convert(py)?; - #check_gil_refs #pyo3_path::impl_::pymethods::tp_new_impl(py, initializer, _slf) } } diff --git a/pyo3-macros-backend/src/module.rs b/pyo3-macros-backend/src/module.rs index 2ca084a6a4b..26e3816c8c4 100644 --- a/pyo3-macros-backend/src/module.rs +++ b/pyo3-macros-backend/src/module.rs @@ -16,7 +16,7 @@ use std::ffi::CString; use syn::{ ext::IdentExt, parse::{Parse, ParseStream}, - parse_quote, parse_quote_spanned, + parse_quote, punctuated::Punctuated, spanned::Spanned, token::Comma, @@ -377,7 +377,6 @@ pub fn pymodule_function_impl(mut function: syn::ItemFn) -> Result let options = PyModuleOptions::from_attrs(&mut function.attrs)?; process_functions_in_module(&options, &mut function)?; let ctx = &Ctx::new(&options.krate, None); - let stmts = std::mem::take(&mut function.block.stmts); let Ctx { pyo3_path, .. } = ctx; let ident = &function.sig.ident; let name = options.name.unwrap_or_else(|| ident.unraw()); @@ -394,30 +393,6 @@ pub fn pymodule_function_impl(mut function: syn::ItemFn) -> Result module_args .push(quote!(::std::convert::Into::into(#pyo3_path::impl_::pymethods::BoundRef(module)))); - let extractors = function - .sig - .inputs - .iter() - .filter_map(|param| { - if let syn::FnArg::Typed(pat_type) = param { - if let syn::Pat::Ident(pat_ident) = &*pat_type.pat { - let ident: &syn::Ident = &pat_ident.ident; - return Some([ - parse_quote!{ let check_gil_refs = #pyo3_path::impl_::deprecations::GilRefs::new(); }, - parse_quote! { let #ident = #pyo3_path::impl_::deprecations::inspect_type(#ident, &check_gil_refs); }, - parse_quote_spanned! { pat_type.span() => check_gil_refs.function_arg(); }, - ]); - } - } - None - }) - .flatten(); - - function.block.stmts = extractors.chain(stmts).collect(); - function - .attrs - .push(parse_quote!(#[allow(clippy::used_underscore_binding)])); - Ok(quote! { #function #[doc(hidden)] diff --git a/pyo3-macros-backend/src/params.rs b/pyo3-macros-backend/src/params.rs index f7d71b923a6..ccf725d3760 100644 --- a/pyo3-macros-backend/src/params.rs +++ b/pyo3-macros-backend/src/params.rs @@ -10,14 +10,12 @@ use syn::spanned::Spanned; pub struct Holders { holders: Vec, - gil_refs_checkers: Vec, } impl Holders { pub fn new() -> Self { Holders { holders: Vec::new(), - gil_refs_checkers: Vec::new(), } } @@ -27,58 +25,14 @@ impl Holders { holder } - pub fn push_gil_refs_checker(&mut self, span: Span) -> syn::Ident { - let gil_refs_checker = syn::Ident::new( - &format!("gil_refs_checker_{}", self.gil_refs_checkers.len()), - span, - ); - self.gil_refs_checkers - .push(GilRefChecker::FunctionArg(gil_refs_checker.clone())); - gil_refs_checker - } - - pub fn push_from_py_with_checker(&mut self, span: Span) -> syn::Ident { - let gil_refs_checker = syn::Ident::new( - &format!("gil_refs_checker_{}", self.gil_refs_checkers.len()), - span, - ); - self.gil_refs_checkers - .push(GilRefChecker::FromPyWith(gil_refs_checker.clone())); - gil_refs_checker - } - pub fn init_holders(&self, ctx: &Ctx) -> TokenStream { let Ctx { pyo3_path, .. } = ctx; let holders = &self.holders; - let gil_refs_checkers = self.gil_refs_checkers.iter().map(|checker| match checker { - GilRefChecker::FunctionArg(ident) => ident, - GilRefChecker::FromPyWith(ident) => ident, - }); quote! { #[allow(clippy::let_unit_value)] #(let mut #holders = #pyo3_path::impl_::extract_argument::FunctionArgumentHolder::INIT;)* - #(let #gil_refs_checkers = #pyo3_path::impl_::deprecations::GilRefs::new();)* } } - - pub fn check_gil_refs(&self) -> TokenStream { - self.gil_refs_checkers - .iter() - .map(|checker| match checker { - GilRefChecker::FunctionArg(ident) => { - quote_spanned! { ident.span() => #ident.function_arg(); } - } - GilRefChecker::FromPyWith(ident) => { - quote_spanned! { ident.span() => #ident.from_py_with_arg(); } - } - }) - .collect() - } -} - -enum GilRefChecker { - FunctionArg(syn::Ident), - FromPyWith(syn::Ident), } /// Return true if the argument list is simply (*args, **kwds). @@ -89,17 +43,6 @@ pub fn is_forwarded_args(signature: &FunctionSignature<'_>) -> bool { ) } -pub(crate) fn check_arg_for_gil_refs( - tokens: TokenStream, - gil_refs_checker: syn::Ident, - ctx: &Ctx, -) -> TokenStream { - let Ctx { pyo3_path, .. } = ctx; - quote! { - #pyo3_path::impl_::deprecations::inspect_type(#tokens, &#gil_refs_checker) - } -} - pub fn impl_arg_params( spec: &FnSpec<'_>, self_: Option<&syn::Type>, @@ -119,9 +62,7 @@ pub fn impl_arg_params( let from_py_with = &arg.from_py_with()?.value; let from_py_with_holder = format_ident!("from_py_with_{}", i); Some(quote_spanned! { from_py_with.span() => - let e = #pyo3_path::impl_::deprecations::GilRefs::new(); - let #from_py_with_holder = #pyo3_path::impl_::deprecations::inspect_fn(#from_py_with, &e); - e.from_py_with_arg(); + let #from_py_with_holder = #from_py_with; }) }) .collect::(); @@ -250,8 +191,7 @@ fn impl_arg_param( let from_py_with = format_ident!("from_py_with_{}", pos); let arg_value = quote!(#args_array[#option_pos].as_deref()); *option_pos += 1; - let tokens = impl_regular_arg_param(arg, from_py_with, arg_value, holders, ctx); - check_arg_for_gil_refs(tokens, holders.push_gil_refs_checker(arg.ty.span()), ctx) + impl_regular_arg_param(arg, from_py_with, arg_value, holders, ctx) } FnArg::VarArgs(arg) => { let holder = holders.push_holder(arg.name.span()); diff --git a/pyo3-macros-backend/src/pymethod.rs b/pyo3-macros-backend/src/pymethod.rs index 7a9afa54755..c02599903e1 100644 --- a/pyo3-macros-backend/src/pymethod.rs +++ b/pyo3-macros-backend/src/pymethod.rs @@ -4,7 +4,7 @@ use std::ffi::CString; use crate::attributes::{NameAttribute, RenamingRule}; use crate::deprecations::deprecate_trailing_option_default; use crate::method::{CallingConvention, ExtractErrorMode, PyArg}; -use crate::params::{check_arg_for_gil_refs, impl_regular_arg_param, Holders}; +use crate::params::{impl_regular_arg_param, Holders}; use crate::utils::PythonDoc; use crate::utils::{Ctx, LitCStr}; use crate::{ @@ -612,21 +612,18 @@ pub fn impl_py_setter_def( PropertyType::Function { spec, .. } => { let (_, args) = split_off_python_arg(&spec.signature.arguments); let value_arg = &args[0]; - let (from_py_with, ident) = if let Some(from_py_with) = - &value_arg.from_py_with().as_ref().map(|f| &f.value) - { - let ident = syn::Ident::new("from_py_with", from_py_with.span()); - ( - quote_spanned! { from_py_with.span() => - let e = #pyo3_path::impl_::deprecations::GilRefs::new(); - let #ident = #pyo3_path::impl_::deprecations::inspect_fn(#from_py_with, &e); - e.from_py_with_arg(); - }, - ident, - ) - } else { - (quote!(), syn::Ident::new("dummy", Span::call_site())) - }; + let (from_py_with, ident) = + if let Some(from_py_with) = &value_arg.from_py_with().as_ref().map(|f| &f.value) { + let ident = syn::Ident::new("from_py_with", from_py_with.span()); + ( + quote_spanned! { from_py_with.span() => + let #ident = #from_py_with; + }, + ident, + ) + } else { + (quote!(), syn::Ident::new("dummy", Span::call_site())) + }; let arg = if let FnArg::Regular(arg) = &value_arg { arg @@ -634,15 +631,13 @@ pub fn impl_py_setter_def( bail_spanned!(value_arg.name().span() => "The #[setter] value argument can't be *args, **kwargs or `cancel_handle`."); }; - let tokens = impl_regular_arg_param( + let extract = impl_regular_arg_param( arg, ident, quote!(::std::option::Option::Some(_value.into())), &mut holders, ctx, ); - let extract = - check_arg_for_gil_refs(tokens, holders.push_gil_refs_checker(arg.ty.span()), ctx); let deprecation = deprecate_trailing_option_default(spec); quote! { @@ -660,12 +655,8 @@ pub fn impl_py_setter_def( .unwrap_or_default(); let holder = holders.push_holder(span); - let gil_refs_checker = holders.push_gil_refs_checker(span); quote! { - let _val = #pyo3_path::impl_::deprecations::inspect_type( - #pyo3_path::impl_::extract_argument::extract_argument(_value.into(), &mut #holder, #name)?, - &#gil_refs_checker - ); + let _val = #pyo3_path::impl_::extract_argument::extract_argument(_value.into(), &mut #holder, #name)?; } } }; @@ -682,7 +673,6 @@ pub fn impl_py_setter_def( } let init_holders = holders.init_holders(ctx); - let check_gil_refs = holders.check_gil_refs(); let associated_method = quote! { #cfg_attrs unsafe fn #wrapper_ident( @@ -698,7 +688,6 @@ pub fn impl_py_setter_def( #init_holders #extract let result = #setter_impl; - #check_gil_refs #pyo3_path::callback::convert(py, result) } }; @@ -824,7 +813,6 @@ pub fn impl_py_getter_def( }; let init_holders = holders.init_holders(ctx); - let check_gil_refs = holders.check_gil_refs(); let associated_method = quote! { #cfg_attrs unsafe fn #wrapper_ident( @@ -833,7 +821,6 @@ pub fn impl_py_getter_def( ) -> #pyo3_path::PyResult<*mut #pyo3_path::ffi::PyObject> { #init_holders let result = #body; - #check_gil_refs result } }; @@ -1139,35 +1126,30 @@ fn extract_object( ctx: &Ctx, ) -> TokenStream { let Ctx { pyo3_path, .. } = ctx; - let gil_refs_checker = holders.push_gil_refs_checker(arg.ty().span()); let name = arg.name().unraw().to_string(); - let extract = if let Some(from_py_with) = - arg.from_py_with().map(|from_py_with| &from_py_with.value) - { - let from_py_with_checker = holders.push_from_py_with_checker(from_py_with.span()); - quote! { - #pyo3_path::impl_::extract_argument::from_py_with( - #pyo3_path::impl_::pymethods::BoundRef::ref_from_ptr(py, &#source_ptr).0, - #name, - #pyo3_path::impl_::deprecations::inspect_fn(#from_py_with, &#from_py_with_checker) as fn(_) -> _, - ) - } - } else { - let holder = holders.push_holder(Span::call_site()); - quote! { - #pyo3_path::impl_::extract_argument::extract_argument( - #pyo3_path::impl_::pymethods::BoundRef::ref_from_ptr(py, &#source_ptr).0, - &mut #holder, - #name - ) - } - }; + let extract = + if let Some(from_py_with) = arg.from_py_with().map(|from_py_with| &from_py_with.value) { + quote! { + #pyo3_path::impl_::extract_argument::from_py_with( + #pyo3_path::impl_::pymethods::BoundRef::ref_from_ptr(py, &#source_ptr).0, + #name, + #from_py_with as fn(_) -> _, + ) + } + } else { + let holder = holders.push_holder(Span::call_site()); + quote! { + #pyo3_path::impl_::extract_argument::extract_argument( + #pyo3_path::impl_::pymethods::BoundRef::ref_from_ptr(py, &#source_ptr).0, + &mut #holder, + #name + ) + } + }; let extracted = extract_error_mode.handle_error(extract, ctx); - quote! { - #pyo3_path::impl_::deprecations::inspect_type(#extracted, &#gil_refs_checker) - } + quote!(#extracted) } enum ReturnMode { @@ -1177,15 +1159,13 @@ enum ReturnMode { } impl ReturnMode { - fn return_call_output(&self, call: TokenStream, ctx: &Ctx, holders: &Holders) -> TokenStream { + fn return_call_output(&self, call: TokenStream, ctx: &Ctx) -> TokenStream { let Ctx { pyo3_path, .. } = ctx; - let check_gil_refs = holders.check_gil_refs(); match self { ReturnMode::Conversion(conversion) => { let conversion = TokenGeneratorCtx(*conversion, ctx); quote! { let _result: #pyo3_path::PyResult<#conversion> = #pyo3_path::callback::convert(py, #call); - #check_gil_refs #pyo3_path::callback::convert(py, _result) } } @@ -1195,14 +1175,12 @@ impl ReturnMode { quote! { let _result = #call; use #pyo3_path::impl_::pymethods::{#traits}; - #check_gil_refs (&_result).#tag().convert(py, _result) } } ReturnMode::ReturnSelf => quote! { let _result: #pyo3_path::PyResult<()> = #pyo3_path::callback::convert(py, #call); _result?; - #check_gil_refs #pyo3_path::ffi::Py_XINCREF(_raw_slf); ::std::result::Result::Ok(_raw_slf) }, @@ -1369,12 +1347,10 @@ fn generate_method_body( let args = extract_proto_arguments(spec, arguments, extract_error_mode, holders, ctx)?; let call = quote! { #cls::#rust_name(#self_arg #(#args),*) }; Ok(if let Some(return_mode) = return_mode { - return_mode.return_call_output(call, ctx, holders) + return_mode.return_call_output(call, ctx) } else { - let check_gil_refs = holders.check_gil_refs(); quote! { let result = #call; - #check_gil_refs; #pyo3_path::callback::convert(py, result) } }) diff --git a/src/impl_/deprecations.rs b/src/impl_/deprecations.rs index eb5caa8dffb..6b9930ac69b 100644 --- a/src/impl_/deprecations.rs +++ b/src/impl_/deprecations.rs @@ -1,76 +1,4 @@ //! Symbols used to denote deprecated usages of PyO3's proc macros. -use crate::{PyResult, Python}; - #[deprecated(since = "0.20.0", note = "use `#[new]` instead of `#[__new__]`")] pub const PYMETHODS_NEW_DEPRECATED_FORM: () = (); - -pub fn inspect_type(t: T, _: &GilRefs) -> T { - t -} - -pub fn inspect_fn(f: fn(A) -> PyResult, _: &GilRefs) -> fn(A) -> PyResult { - f -} - -pub struct GilRefs(OptionGilRefs); -pub struct OptionGilRefs(NotAGilRef); -pub struct NotAGilRef(std::marker::PhantomData); - -pub trait IsGilRef {} - -#[cfg(feature = "gil-refs")] -impl IsGilRef for &'_ T {} - -impl GilRefs { - #[allow(clippy::new_without_default)] - pub fn new() -> Self { - GilRefs(OptionGilRefs(NotAGilRef(std::marker::PhantomData))) - } -} - -impl GilRefs> { - #[deprecated(since = "0.21.0", note = "use `wrap_pyfunction_bound!` instead")] - pub fn is_python(&self) {} -} - -impl GilRefs { - #[deprecated( - since = "0.21.0", - note = "use `&Bound<'_, T>` instead for this function argument" - )] - pub fn function_arg(&self) {} - #[deprecated( - since = "0.21.0", - note = "use `&Bound<'_, PyAny>` as the argument for this `from_py_with` extractor" - )] - pub fn from_py_with_arg(&self) {} -} - -impl OptionGilRefs> { - #[deprecated( - since = "0.21.0", - note = "use `Option<&Bound<'_, T>>` instead for this function argument" - )] - pub fn function_arg(&self) {} -} - -impl NotAGilRef { - pub fn function_arg(&self) {} - pub fn from_py_with_arg(&self) {} - pub fn is_python(&self) {} -} - -impl std::ops::Deref for GilRefs { - type Target = OptionGilRefs; - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl std::ops::Deref for OptionGilRefs { - type Target = NotAGilRef; - fn deref(&self) -> &Self::Target { - &self.0 - } -} diff --git a/src/macros.rs b/src/macros.rs index ab91d577ac5..43fbef12b89 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -145,12 +145,8 @@ macro_rules! wrap_pyfunction { }; ($function:path, $py_or_module:expr) => {{ use $function as wrapped_pyfunction; - let check_gil_refs = $crate::impl_::deprecations::GilRefs::new(); - let py_or_module = - $crate::impl_::deprecations::inspect_type($py_or_module, &check_gil_refs); - check_gil_refs.is_python(); $crate::impl_::pyfunction::WrapPyFunctionArg::wrap_pyfunction( - py_or_module, + $py_or_module, &wrapped_pyfunction::_PYO3_DEF, ) }}; diff --git a/tests/test_compile_error.rs b/tests/test_compile_error.rs index 9e8b3b1a593..bdfc4893ed1 100644 --- a/tests/test_compile_error.rs +++ b/tests/test_compile_error.rs @@ -20,7 +20,6 @@ fn test_compile_errors() { t.compile_fail("tests/ui/invalid_pymethod_names.rs"); t.compile_fail("tests/ui/invalid_pymodule_args.rs"); t.compile_fail("tests/ui/reject_generics.rs"); - #[cfg(feature = "gil-refs")] t.compile_fail("tests/ui/deprecations.rs"); t.compile_fail("tests/ui/invalid_closure.rs"); t.compile_fail("tests/ui/pyclass_send.rs"); diff --git a/tests/ui/deprecations.rs b/tests/ui/deprecations.rs index ff34966e00d..97ad6629f2f 100644 --- a/tests/ui/deprecations.rs +++ b/tests/ui/deprecations.rs @@ -2,7 +2,6 @@ #![allow(dead_code)] use pyo3::prelude::*; -use pyo3::types::{PyString, PyType}; #[pyclass] struct MyClass; @@ -13,114 +12,14 @@ impl MyClass { fn new() -> Self { Self } - - #[classmethod] - fn cls_method_gil_ref(_cls: &PyType) {} - - #[classmethod] - fn cls_method_bound(_cls: &Bound<'_, PyType>) {} - - fn method_gil_ref(_slf: &PyCell) {} - - fn method_bound(_slf: &Bound<'_, Self>) {} - - #[staticmethod] - fn static_method_gil_ref(_any: &PyAny) {} - - #[setter] - fn set_foo_gil_ref(&self, #[pyo3(from_py_with = "extract_gil_ref")] _value: i32) {} - - #[setter] - fn set_foo_bound(&self, #[pyo3(from_py_with = "extract_bound")] _value: i32) {} - - #[setter] - fn set_bar_gil_ref(&self, _value: &PyAny) {} - - #[setter] - fn set_bar_bound(&self, _value: &Bound<'_, PyAny>) {} - - fn __eq__(&self, #[pyo3(from_py_with = "extract_gil_ref")] _other: i32) -> bool { - true - } - - fn __contains__(&self, #[pyo3(from_py_with = "extract_bound")] _value: i32) -> bool { - true - } } fn main() {} -#[pyfunction] -#[pyo3(pass_module)] -fn pyfunction_with_module<'py>(module: &Bound<'py, PyModule>) -> PyResult> { - module.name() -} - -#[pyfunction] -#[pyo3(pass_module)] -fn pyfunction_with_module_gil_ref(_module: &PyModule) -> PyResult<&str> { - todo!() -} - -#[pyfunction] -fn double(x: usize) -> usize { - x * 2 -} - -#[pymodule] -fn module_gil_ref(_m: &PyModule) -> PyResult<()> { - Ok(()) -} - -#[pymodule] -fn module_gil_ref_with_explicit_py_arg(_py: Python<'_>, _m: &PyModule) -> PyResult<()> { - Ok(()) -} - -#[pymodule] -fn module_bound(m: &Bound<'_, PyModule>) -> PyResult<()> { - m.add_function(wrap_pyfunction!(double, m)?)?; - Ok(()) -} - -#[pymodule] -fn module_bound_with_explicit_py_arg(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { - m.add_function(wrap_pyfunction!(double, m)?)?; - Ok(()) -} - -#[pymodule] -fn module_bound_by_value(m: Bound<'_, PyModule>) -> PyResult<()> { - m.add_function(wrap_pyfunction!(double, &m)?)?; - Ok(()) -} - -fn extract_gil_ref(obj: &PyAny) -> PyResult { - obj.extract() -} - -fn extract_bound(obj: &Bound<'_, PyAny>) -> PyResult { - obj.extract() -} - fn extract_options(obj: &Bound<'_, PyAny>) -> PyResult> { obj.extract() } -#[pyfunction] -fn pyfunction_from_py_with( - #[pyo3(from_py_with = "extract_gil_ref")] _gil_ref: i32, - #[pyo3(from_py_with = "extract_bound")] _bound: i32, -) { -} - -#[pyfunction] -fn pyfunction_gil_ref(_any: &PyAny) {} - -#[pyfunction] -#[pyo3(signature = (_any))] -fn pyfunction_option_gil_ref(_any: Option<&PyAny>) {} - #[pyfunction] #[pyo3(signature = (_i, _any=None))] fn pyfunction_option_1(_i: u32, _any: Option) {} @@ -139,61 +38,6 @@ fn pyfunction_option_4( ) { } -#[derive(Debug, FromPyObject)] -pub struct Zap { - #[pyo3(item)] - name: String, - - #[pyo3(from_py_with = "PyAny::len", item("my_object"))] - some_object_length: usize, - - #[pyo3(from_py_with = "extract_bound")] - some_number: i32, -} - -#[derive(Debug, FromPyObject)] -pub struct ZapTuple( - String, - #[pyo3(from_py_with = "PyAny::len")] usize, - #[pyo3(from_py_with = "extract_bound")] i32, -); - -#[derive(Debug, FromPyObject, PartialEq, Eq)] -pub enum ZapEnum { - Zip(#[pyo3(from_py_with = "extract_gil_ref")] i32), - Zap(String, #[pyo3(from_py_with = "extract_bound")] i32), -} - -#[derive(Debug, FromPyObject, PartialEq, Eq)] -#[pyo3(transparent)] -pub struct TransparentFromPyWithGilRef { - #[pyo3(from_py_with = "extract_gil_ref")] - len: i32, -} - -#[derive(Debug, FromPyObject, PartialEq, Eq)] -#[pyo3(transparent)] -pub struct TransparentFromPyWithBound { - #[pyo3(from_py_with = "extract_bound")] - len: i32, -} - -fn test_wrap_pyfunction(py: Python<'_>, m: &Bound<'_, PyModule>) { - // should lint - let _ = wrap_pyfunction!(double, py); - - // should lint but currently does not - let _ = wrap_pyfunction!(double)(py); - - // should not lint - let _ = wrap_pyfunction!(double, m); - let _ = wrap_pyfunction!(double)(m); - let _ = wrap_pyfunction!(double, m.as_gil_ref()); - let _ = wrap_pyfunction!(double)(m.as_gil_ref()); - let _ = wrap_pyfunction_bound!(double, py); - let _ = wrap_pyfunction_bound!(double)(py); -} - #[pyclass] pub enum SimpleEnumWithoutEq { VariamtA, diff --git a/tests/ui/deprecations.stderr b/tests/ui/deprecations.stderr index 4f1797da838..f9e9652e377 100644 --- a/tests/ui/deprecations.stderr +++ b/tests/ui/deprecations.stderr @@ -1,7 +1,7 @@ error: use of deprecated constant `pyo3::deprecations::PYMETHODS_NEW_DEPRECATED_FORM`: use `#[new]` instead of `#[__new__]` - --> tests/ui/deprecations.rs:12:7 + --> tests/ui/deprecations.rs:11:7 | -12 | #[__new__] +11 | #[__new__] | ^^^^^^^ | note: the lint level is defined here @@ -13,141 +13,31 @@ note: the lint level is defined here error: use of deprecated constant `__pyfunction_pyfunction_option_2::SIGNATURE`: this function has implicit defaults for the trailing `Option` arguments = note: these implicit defaults are being phased out = help: add `#[pyo3(signature = (_i, _any=None))]` to this function to silence this warning and keep the current behavior - --> tests/ui/deprecations.rs:129:4 - | -129 | fn pyfunction_option_2(_i: u32, _any: Option) {} - | ^^^^^^^^^^^^^^^^^^^ + --> tests/ui/deprecations.rs:28:4 + | +28 | fn pyfunction_option_2(_i: u32, _any: Option) {} + | ^^^^^^^^^^^^^^^^^^^ error: use of deprecated constant `__pyfunction_pyfunction_option_3::SIGNATURE`: this function has implicit defaults for the trailing `Option` arguments = note: these implicit defaults are being phased out = help: add `#[pyo3(signature = (_i, _any=None, _foo=None))]` to this function to silence this warning and keep the current behavior - --> tests/ui/deprecations.rs:132:4 - | -132 | fn pyfunction_option_3(_i: u32, _any: Option, _foo: Option) {} - | ^^^^^^^^^^^^^^^^^^^ + --> tests/ui/deprecations.rs:31:4 + | +31 | fn pyfunction_option_3(_i: u32, _any: Option, _foo: Option) {} + | ^^^^^^^^^^^^^^^^^^^ error: use of deprecated constant `__pyfunction_pyfunction_option_4::SIGNATURE`: this function has implicit defaults for the trailing `Option` arguments = note: these implicit defaults are being phased out = help: add `#[pyo3(signature = (_i, _any=None, _foo=None))]` to this function to silence this warning and keep the current behavior - --> tests/ui/deprecations.rs:135:4 - | -135 | fn pyfunction_option_4( - | ^^^^^^^^^^^^^^^^^^^ - -error: use of deprecated constant `SimpleEnumWithoutEq::__pyo3__generated____richcmp__::DEPRECATION`: Implicit equality for simple enums is deprecated. Use `#[pyclass(eq, eq_int)` to keep the current behavior. - --> tests/ui/deprecations.rs:197:1 - | -197 | #[pyclass] - | ^^^^^^^^^^ - | - = note: this error originates in the attribute macro `pyclass` (in Nightly builds, run with -Z macro-backtrace for more info) - -error: use of deprecated struct `pyo3::PyCell`: `PyCell` was merged into `Bound`, use that instead; see the migration guide for more info - --> tests/ui/deprecations.rs:23:30 - | -23 | fn method_gil_ref(_slf: &PyCell) {} - | ^^^^^^ - -error: use of deprecated method `pyo3::deprecations::GilRefs::::from_py_with_arg`: use `&Bound<'_, PyAny>` as the argument for this `from_py_with` extractor - --> tests/ui/deprecations.rs:42:44 - | -42 | fn __eq__(&self, #[pyo3(from_py_with = "extract_gil_ref")] _other: i32) -> bool { - | ^^^^^^^^^^^^^^^^^ - -error: use of deprecated method `pyo3::deprecations::GilRefs::::function_arg`: use `&Bound<'_, T>` instead for this function argument - --> tests/ui/deprecations.rs:18:33 - | -18 | fn cls_method_gil_ref(_cls: &PyType) {} - | ^ - -error: use of deprecated method `pyo3::deprecations::GilRefs::::function_arg`: use `&Bound<'_, T>` instead for this function argument - --> tests/ui/deprecations.rs:23:29 - | -23 | fn method_gil_ref(_slf: &PyCell) {} - | ^ - -error: use of deprecated method `pyo3::deprecations::GilRefs::::function_arg`: use `&Bound<'_, T>` instead for this function argument - --> tests/ui/deprecations.rs:28:36 - | -28 | fn static_method_gil_ref(_any: &PyAny) {} - | ^ - -error: use of deprecated method `pyo3::deprecations::GilRefs::::from_py_with_arg`: use `&Bound<'_, PyAny>` as the argument for this `from_py_with` extractor - --> tests/ui/deprecations.rs:31:53 + --> tests/ui/deprecations.rs:34:4 | -31 | fn set_foo_gil_ref(&self, #[pyo3(from_py_with = "extract_gil_ref")] _value: i32) {} - | ^^^^^^^^^^^^^^^^^ +34 | fn pyfunction_option_4( + | ^^^^^^^^^^^^^^^^^^^ -error: use of deprecated method `pyo3::deprecations::GilRefs::::function_arg`: use `&Bound<'_, T>` instead for this function argument - --> tests/ui/deprecations.rs:37:39 - | -37 | fn set_bar_gil_ref(&self, _value: &PyAny) {} - | ^ - -error: use of deprecated method `pyo3::deprecations::GilRefs::::function_arg`: use `&Bound<'_, T>` instead for this function argument - --> tests/ui/deprecations.rs:61:44 - | -61 | fn pyfunction_with_module_gil_ref(_module: &PyModule) -> PyResult<&str> { - | ^ - -error: use of deprecated method `pyo3::deprecations::GilRefs::::function_arg`: use `&Bound<'_, T>` instead for this function argument - --> tests/ui/deprecations.rs:71:19 +error: use of deprecated constant `SimpleEnumWithoutEq::__pyo3__generated____richcmp__::DEPRECATION`: Implicit equality for simple enums is deprecated. Use `#[pyclass(eq, eq_int)` to keep the current behavior. + --> tests/ui/deprecations.rs:41:1 | -71 | fn module_gil_ref(_m: &PyModule) -> PyResult<()> { - | ^^ - -error: use of deprecated method `pyo3::deprecations::GilRefs::::function_arg`: use `&Bound<'_, T>` instead for this function argument - --> tests/ui/deprecations.rs:76:57 +41 | #[pyclass] + | ^^^^^^^^^^ | -76 | fn module_gil_ref_with_explicit_py_arg(_py: Python<'_>, _m: &PyModule) -> PyResult<()> { - | ^^ - -error: use of deprecated method `pyo3::deprecations::GilRefs::::from_py_with_arg`: use `&Bound<'_, PyAny>` as the argument for this `from_py_with` extractor - --> tests/ui/deprecations.rs:112:27 - | -112 | #[pyo3(from_py_with = "extract_gil_ref")] _gil_ref: i32, - | ^^^^^^^^^^^^^^^^^ - -error: use of deprecated method `pyo3::deprecations::GilRefs::::function_arg`: use `&Bound<'_, T>` instead for this function argument - --> tests/ui/deprecations.rs:118:29 - | -118 | fn pyfunction_gil_ref(_any: &PyAny) {} - | ^ - -error: use of deprecated method `pyo3::deprecations::OptionGilRefs::>::function_arg`: use `Option<&Bound<'_, T>>` instead for this function argument - --> tests/ui/deprecations.rs:122:36 - | -122 | fn pyfunction_option_gil_ref(_any: Option<&PyAny>) {} - | ^^^^^^ - -error: use of deprecated method `pyo3::deprecations::GilRefs::::from_py_with_arg`: use `&Bound<'_, PyAny>` as the argument for this `from_py_with` extractor - --> tests/ui/deprecations.rs:147:27 - | -147 | #[pyo3(from_py_with = "PyAny::len", item("my_object"))] - | ^^^^^^^^^^^^ - -error: use of deprecated method `pyo3::deprecations::GilRefs::::from_py_with_arg`: use `&Bound<'_, PyAny>` as the argument for this `from_py_with` extractor - --> tests/ui/deprecations.rs:157:27 - | -157 | #[pyo3(from_py_with = "PyAny::len")] usize, - | ^^^^^^^^^^^^ - -error: use of deprecated method `pyo3::deprecations::GilRefs::::from_py_with_arg`: use `&Bound<'_, PyAny>` as the argument for this `from_py_with` extractor - --> tests/ui/deprecations.rs:163:31 - | -163 | Zip(#[pyo3(from_py_with = "extract_gil_ref")] i32), - | ^^^^^^^^^^^^^^^^^ - -error: use of deprecated method `pyo3::deprecations::GilRefs::::from_py_with_arg`: use `&Bound<'_, PyAny>` as the argument for this `from_py_with` extractor - --> tests/ui/deprecations.rs:170:27 - | -170 | #[pyo3(from_py_with = "extract_gil_ref")] - | ^^^^^^^^^^^^^^^^^ - -error: use of deprecated method `pyo3::deprecations::GilRefs::>::is_python`: use `wrap_pyfunction_bound!` instead - --> tests/ui/deprecations.rs:183:13 - | -183 | let _ = wrap_pyfunction!(double, py); - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | - = note: this error originates in the macro `wrap_pyfunction` (in Nightly builds, run with -Z macro-backtrace for more info) + = note: this error originates in the attribute macro `pyclass` (in Nightly builds, run with -Z macro-backtrace for more info) From 1861d6d379d8f04bf94e529ddc329ab831a60d2d Mon Sep 17 00:00:00 2001 From: Sede Soukossi <4968379+styvane@users.noreply.github.com> Date: Mon, 8 Jul 2024 22:30:44 +0200 Subject: [PATCH 145/495] docs: Fix mismatch return value, remove redundant error propagation, and additional fixes (#4318) * Fix mismatch return value, remove redundant error propagation, and fix typo * Update guide/src/function/signature.md Co-authored-by: Lily Foote --------- Co-authored-by: Lily Foote --- guide/src/ecosystem/async-await.md | 9 +++------ guide/src/function.md | 12 ++++-------- guide/src/function/signature.md | 3 +-- guide/src/getting-started.md | 3 +-- guide/src/module.md | 12 ++++-------- 5 files changed, 13 insertions(+), 26 deletions(-) diff --git a/guide/src/ecosystem/async-await.md b/guide/src/ecosystem/async-await.md index 9c0ad19bdef..0128efbc8a3 100644 --- a/guide/src/ecosystem/async-await.md +++ b/guide/src/ecosystem/async-await.md @@ -129,9 +129,7 @@ fn rust_sleep(py: Python<'_>) -> PyResult<&Bound<'_, PyAny>> { #[pymodule] fn my_async_module(m: &Bound<'_, PyModule>) -> PyResult<()> { - m.add_function(wrap_pyfunction!(rust_sleep, m)?)?; - - Ok(()) + m.add_function(wrap_pyfunction!(rust_sleep, m)?) } ``` @@ -152,8 +150,7 @@ fn rust_sleep(py: Python<'_>) -> PyResult<&Bound<'_, PyAny>>> { #[pymodule] fn my_async_module(m: &Bound<'_, PyModule>) -> PyResult<()> { - m.add_function(wrap_pyfunction!(rust_sleep, m)?)?; - Ok(()) + m.add_function(wrap_pyfunction!(rust_sleep, m)?) } ``` @@ -257,7 +254,7 @@ async def py_sleep(): await_coro(py_sleep()) ``` -If for you wanted to pass a callable function to the `#[pyfunction]` instead, (i.e. the last line becomes `await_coro(py_sleep))`, then the above example needs to be tweaked to first call the callable to get the coroutine: +If you wanted to pass a callable function to the `#[pyfunction]` instead, (i.e. the last line becomes `await_coro(py_sleep))`, then the above example needs to be tweaked to first call the callable to get the coroutine: ```rust #[pyfunction] diff --git a/guide/src/function.md b/guide/src/function.md index 86ac4c89b46..5e6cfaba773 100644 --- a/guide/src/function.md +++ b/guide/src/function.md @@ -14,8 +14,7 @@ fn double(x: usize) -> usize { #[pymodule] fn my_extension(m: &Bound<'_, PyModule>) -> PyResult<()> { - m.add_function(wrap_pyfunction!(double, m)?)?; - Ok(()) + m.add_function(wrap_pyfunction!(double, m)?) } ``` @@ -56,8 +55,7 @@ The `#[pyo3]` attribute can be used to modify properties of the generated Python #[pymodule] fn module_with_functions(m: &Bound<'_, PyModule>) -> PyResult<()> { - m.add_function(wrap_pyfunction!(no_args_py, m)?)?; - Ok(()) + m.add_function(wrap_pyfunction!(no_args_py, m)?) } # Python::with_gil(|py| { @@ -113,8 +111,7 @@ The `#[pyo3]` attribute can be used on individual arguments to modify properties use pyo3::prelude::*; fn get_length(obj: &Bound<'_, PyAny>) -> PyResult { - let length = obj.len()?; - Ok(length) + obj.len() } #[pyfunction] @@ -204,8 +201,7 @@ fn my_extension(m: &Bound<'_, PyModule>) -> PyResult<()> { x * 2 } - m.add_function(wrap_pyfunction!(double, m)?)?; - Ok(()) + m.add_function(wrap_pyfunction!(double, m)?) } ``` diff --git a/guide/src/function/signature.md b/guide/src/function/signature.md index 69949220be6..a9be5983422 100644 --- a/guide/src/function/signature.md +++ b/guide/src/function/signature.md @@ -22,8 +22,7 @@ fn num_kwds(kwds: Option<&Bound<'_, PyDict>>) -> usize { #[pymodule] fn module_with_functions(m: &Bound<'_, PyModule>) -> PyResult<()> { - m.add_function(wrap_pyfunction!(num_kwds, m)?).unwrap(); - Ok(()) + m.add_function(wrap_pyfunction!(num_kwds, m)?) } ``` diff --git a/guide/src/getting-started.md b/guide/src/getting-started.md index ede48d50c33..e2cc040bbd7 100644 --- a/guide/src/getting-started.md +++ b/guide/src/getting-started.md @@ -159,8 +159,7 @@ fn sum_as_string(a: usize, b: usize) -> PyResult { /// import the module. #[pymodule] fn pyo3_example(m: &Bound<'_, PyModule>) -> PyResult<()> { - m.add_function(wrap_pyfunction!(sum_as_string, m)?)?; - Ok(()) + m.add_function(wrap_pyfunction!(sum_as_string, m)?) } ``` diff --git a/guide/src/module.md b/guide/src/module.md index 2c4039a6e76..a2cb8b37a05 100644 --- a/guide/src/module.md +++ b/guide/src/module.md @@ -13,8 +13,7 @@ fn double(x: usize) -> usize { /// This module is implemented in Rust. #[pymodule] fn my_extension(m: &Bound<'_, PyModule>) -> PyResult<()> { - m.add_function(wrap_pyfunction!(double, m)?)?; - Ok(()) + m.add_function(wrap_pyfunction!(double, m)?) } ``` @@ -35,8 +34,7 @@ fn double(x: usize) -> usize { #[pymodule] #[pyo3(name = "custom_name")] fn my_extension(m: &Bound<'_, PyModule>) -> PyResult<()> { - m.add_function(wrap_pyfunction!(double, m)?)?; - Ok(()) + m.add_function(wrap_pyfunction!(double, m)?) } ``` @@ -80,8 +78,7 @@ fn parent_module(m: &Bound<'_, PyModule>) -> PyResult<()> { fn register_child_module(parent_module: &Bound<'_, PyModule>) -> PyResult<()> { let child_module = PyModule::new_bound(parent_module.py(), "child_module")?; child_module.add_function(wrap_pyfunction!(func, &child_module)?)?; - parent_module.add_submodule(&child_module)?; - Ok(()) + parent_module.add_submodule(&child_module) } #[pyfunction] @@ -143,8 +140,7 @@ mod my_extension { #[pymodule_init] fn init(m: &Bound<'_, PyModule>) -> PyResult<()> { // Arbitrary code to run at the module initialization - m.add("double2", m.getattr("double")?)?; - Ok(()) + m.add("double2", m.getattr("double")?) } } # } From 186c7d33152f060980e2ac8ef4d640eebe921fe1 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Tue, 9 Jul 2024 12:53:11 +0100 Subject: [PATCH 146/495] docs: improve signposting to bound and traits (#4325) * docs: improve signposting to bound and traits * update UI tests --- src/instance.rs | 14 +++++++++++++- src/lib.rs | 5 ++++- src/types/any.rs | 25 +++++++------------------ src/types/boolobject.rs | 6 ++++++ src/types/bytearray.rs | 6 ++++++ src/types/bytes.rs | 10 ++++++++-- src/types/capsule.rs | 5 +++++ src/types/code.rs | 3 +++ src/types/complex.rs | 6 ++++++ src/types/datetime.rs | 23 +++++++++++++++++++---- src/types/dict.rs | 6 ++++++ src/types/ellipsis.rs | 3 +++ src/types/float.rs | 10 ++++++++-- src/types/frame.rs | 3 +++ src/types/frozenset.rs | 8 +++++++- src/types/function.rs | 6 ++++++ src/types/iterator.rs | 3 +++ src/types/list.rs | 6 ++++++ src/types/mapping.rs | 6 ++++++ src/types/memoryview.rs | 3 +++ src/types/module.rs | 6 ++++++ src/types/none.rs | 3 +++ src/types/notimplemented.rs | 3 +++ src/types/num.rs | 3 +++ src/types/pysuper.rs | 3 ++- src/types/sequence.rs | 6 ++++++ src/types/set.rs | 8 +++++++- src/types/slice.rs | 6 ++++++ src/types/string.rs | 7 ++++--- src/types/traceback.rs | 6 ++++++ src/types/tuple.rs | 6 +++++- src/types/typeobject.rs | 9 ++++++++- tests/ui/invalid_pyfunctions.stderr | 6 +++--- 33 files changed, 190 insertions(+), 39 deletions(-) diff --git a/src/instance.rs b/src/instance.rs index cc1ae684e44..499f751027c 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -65,7 +65,19 @@ pub unsafe trait PyNativeType: Sized { } } -/// A GIL-attached equivalent to `Py`. +/// A GIL-attached equivalent to [`Py`]. +/// +/// This type can be thought of as equivalent to the tuple `(Py, Python<'py>)`. By having the `'py` +/// lifetime of the [`Python<'py>`] token, this ties the lifetime of the [`Bound<'py, T>`] smart pointer +/// to the lifetime of the GIL and allows PyO3 to call Python APIs at maximum efficiency. +/// +/// To access the object in situations where the GIL is not held, convert it to [`Py`] +/// using [`.unbind()`][Bound::unbind]. This includes situations where the GIL is temporarily +/// released, such as [`Python::allow_threads`](crate::Python::allow_threads)'s closure. +/// +/// See +#[doc = concat!("[the guide](https://pyo3.rs/v", env!("CARGO_PKG_VERSION"), "/types.html#boundpy-t)")] +/// for more detail. #[repr(transparent)] pub struct Bound<'py, T>(Python<'py>, ManuallyDrop>); diff --git a/src/lib.rs b/src/lib.rs index a9c5bd0b731..ef905ec356c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -461,7 +461,6 @@ pub mod marshal; #[macro_use] pub mod sync; pub mod panic; -pub mod prelude; pub mod pybacked; pub mod pycell; pub mod pyclass; @@ -495,6 +494,10 @@ mod macros; #[cfg(feature = "experimental-inspect")] pub mod inspect; +// Putting the declaration of prelude at the end seems to help encourage rustc and rustdoc to prefer using +// other paths to the same items. (e.g. `pyo3::types::PyAnyMethods` instead of `pyo3::prelude::PyAnyMethods`). +pub mod prelude; + /// Ths module only contains re-exports of pyo3 deprecation warnings and exists /// purely to make compiler error messages nicer. /// diff --git a/src/types/any.rs b/src/types/any.rs index c8c6d67e534..fdeab37712d 100644 --- a/src/types/any.rs +++ b/src/types/any.rs @@ -19,26 +19,15 @@ use std::os::raw::c_int; /// Represents any Python object. /// -/// It currently only appears as a *reference*, `&PyAny`, -/// with a lifetime that represents the scope during which the GIL is held. +/// Values of this type are accessed via PyO3's smart pointers, e.g. as +/// [`Py`][crate::Py] or [`Bound<'py, PyAny>`][Bound]. /// -/// `PyAny` has some interesting properties, which it shares -/// with the other [native Python types](crate::types): +/// For APIs available on all Python objects, see the [`PyAnyMethods`] trait which is implemented for +/// [`Bound<'py, PyAny>`][Bound]. /// -/// - It can only be obtained and used while the GIL is held, -/// therefore its API does not require a [`Python<'py>`](crate::Python) token. -/// - It can't be used in situations where the GIL is temporarily released, -/// such as [`Python::allow_threads`](crate::Python::allow_threads)'s closure. -/// - The underlying Python object, if mutable, can be mutated through any reference. -/// - It can be converted to the GIL-independent [`Py`]`<`[`PyAny`]`>`, -/// allowing it to outlive the GIL scope. However, using [`Py`]`<`[`PyAny`]`>`'s API -/// *does* require a [`Python<'py>`](crate::Python) token. -/// -/// It can be cast to a concrete type with PyAny::downcast (for native Python types only) -/// and FromPyObject::extract. See their documentation for more information. -/// -/// See [the guide](https://pyo3.rs/latest/types.html) for an explanation -/// of the different Python object types. +/// See +#[doc = concat!("[the guide](https://pyo3.rs/v", env!("CARGO_PKG_VERSION"), "/types.html#concrete-python-types)")] +/// for an explanation of the different Python object types. #[repr(transparent)] pub struct PyAny(UnsafeCell); diff --git a/src/types/boolobject.rs b/src/types/boolobject.rs index ee19797d66e..9ac66529e9a 100644 --- a/src/types/boolobject.rs +++ b/src/types/boolobject.rs @@ -11,6 +11,12 @@ use crate::{ use super::any::PyAnyMethods; /// Represents a Python `bool`. +/// +/// Values of this type are accessed via PyO3's smart pointers, e.g. as +/// [`Py`][crate::Py] or [`Bound<'py, PyBool>`][Bound]. +/// +/// For APIs available on `bool` objects, see the [`PyBoolMethods`] trait which is implemented for +/// [`Bound<'py, PyBool>`][Bound]. #[repr(transparent)] pub struct PyBool(PyAny); diff --git a/src/types/bytearray.rs b/src/types/bytearray.rs index c411e830340..57376069355 100644 --- a/src/types/bytearray.rs +++ b/src/types/bytearray.rs @@ -9,6 +9,12 @@ use crate::{AsPyPointer, PyNativeType}; use std::slice; /// Represents a Python `bytearray`. +/// +/// Values of this type are accessed via PyO3's smart pointers, e.g. as +/// [`Py`][crate::Py] or [`Bound<'py, PyByteArray>`][Bound]. +/// +/// For APIs available on `bytearray` objects, see the [`PyByteArrayMethods`] trait which is implemented for +/// [`Bound<'py, PyByteArray>`][Bound]. #[repr(transparent)] pub struct PyByteArray(PyAny); diff --git a/src/types/bytes.rs b/src/types/bytes.rs index 0513f4cec8c..512c835f87a 100644 --- a/src/types/bytes.rs +++ b/src/types/bytes.rs @@ -12,10 +12,16 @@ use std::str; /// /// This type is immutable. /// +/// Values of this type are accessed via PyO3's smart pointers, e.g. as +/// [`Py`][crate::Py] or [`Bound<'py, PyBytes>`][Bound]. +/// +/// For APIs available on `bytes` objects, see the [`PyBytesMethods`] trait which is implemented for +/// [`Bound<'py, PyBytes>`][Bound]. +/// /// # Equality /// -/// For convenience, [`Bound<'py, PyBytes>`] implements [`PartialEq<[u8]>`] to allow comparing the -/// data in the Python bytes to a Rust `[u8]`. +/// For convenience, [`Bound<'py, PyBytes>`][Bound] implements [`PartialEq<[u8]>`][PartialEq] to allow comparing the +/// data in the Python bytes to a Rust `[u8]` byte slice. /// /// This is not always the most appropriate way to compare Python bytes, as Python bytes subclasses /// may have different equality semantics. In situations where subclasses overriding equality might be diff --git a/src/types/capsule.rs b/src/types/capsule.rs index 9b9445cdffe..815b70ebc41 100644 --- a/src/types/capsule.rs +++ b/src/types/capsule.rs @@ -15,6 +15,11 @@ use std::os::raw::{c_char, c_int, c_void}; /// > in one module available to other modules, so the regular import mechanism can /// > be used to access C APIs defined in dynamically loaded modules. /// +/// Values of this type are accessed via PyO3's smart pointers, e.g. as +/// [`Py`][crate::Py] or [`Bound<'py, PyCapsule>`][Bound]. +/// +/// For APIs available on capsule objects, see the [`PyCapsuleMethods`] trait which is implemented for +/// [`Bound<'py, PyCapsule>`][Bound]. /// /// # Example /// ``` diff --git a/src/types/code.rs b/src/types/code.rs index f60e7783aa4..04e1efb9fe7 100644 --- a/src/types/code.rs +++ b/src/types/code.rs @@ -2,6 +2,9 @@ use crate::ffi; use crate::PyAny; /// Represents a Python code object. +/// +/// Values of this type are accessed via PyO3's smart pointers, e.g. as +/// [`Py`][crate::Py] or [`Bound<'py, PyCode>`][crate::Bound]. #[repr(transparent)] pub struct PyCode(PyAny); diff --git a/src/types/complex.rs b/src/types/complex.rs index 5ec9e2f00a4..887bc12e438 100644 --- a/src/types/complex.rs +++ b/src/types/complex.rs @@ -7,6 +7,12 @@ use std::os::raw::c_double; /// Represents a Python [`complex`](https://docs.python.org/3/library/functions.html#complex) object. /// +/// Values of this type are accessed via PyO3's smart pointers, e.g. as +/// [`Py`][crate::Py] or [`Bound<'py, PyComplex>`][Bound]. +/// +/// For APIs available on `complex` objects, see the [`PyComplexMethods`] trait which is implemented for +/// [`Bound<'py, PyComplex>`][Bound]. +/// /// Note that `PyComplex` supports only basic operations. For advanced operations /// consider using [num-complex](https://docs.rs/num-complex)'s [`Complex`] type instead. /// This optional dependency can be activated with the `num-complex` feature flag. diff --git a/src/types/datetime.rs b/src/types/datetime.rs index cdf3b011e6c..c07b6be9b2b 100644 --- a/src/types/datetime.rs +++ b/src/types/datetime.rs @@ -191,7 +191,10 @@ pub trait PyTzInfoAccess<'py> { fn get_tzinfo_bound(&self) -> Option>; } -/// Bindings around `datetime.date` +/// Bindings around `datetime.date`. +/// +/// Values of this type are accessed via PyO3's smart pointers, e.g. as +/// [`Py`][crate::Py] or [`Bound<'py, PyDate>`][Bound]. #[repr(transparent)] pub struct PyDate(PyAny); pyobject_native_type!( @@ -279,7 +282,10 @@ impl PyDateAccess for Bound<'_, PyDate> { } } -/// Bindings for `datetime.datetime` +/// Bindings for `datetime.datetime`. +/// +/// Values of this type are accessed via PyO3's smart pointers, e.g. as +/// [`Py`][crate::Py] or [`Bound<'py, PyDateTime>`][Bound]. #[repr(transparent)] pub struct PyDateTime(PyAny); pyobject_native_type!( @@ -578,7 +584,10 @@ impl<'py> PyTzInfoAccess<'py> for Bound<'py, PyDateTime> { } } -/// Bindings for `datetime.time` +/// Bindings for `datetime.time`. +/// +/// Values of this type are accessed via PyO3's smart pointers, e.g. as +/// [`Py`][crate::Py] or [`Bound<'py, PyTime>`][Bound]. #[repr(transparent)] pub struct PyTime(PyAny); pyobject_native_type!( @@ -781,6 +790,9 @@ impl<'py> PyTzInfoAccess<'py> for Bound<'py, PyTime> { /// Bindings for `datetime.tzinfo`. /// +/// Values of this type are accessed via PyO3's smart pointers, e.g. as +/// [`Py`][crate::Py] or [`Bound<'py, PyTzInfo>`][Bound]. +/// /// This is an abstract base class and cannot be constructed directly. /// For concrete time zone implementations, see [`timezone_utc_bound`] and /// the [`zoneinfo` module](https://docs.python.org/3/library/zoneinfo.html). @@ -834,7 +846,10 @@ pub(crate) fn timezone_from_offset<'py>( } } -/// Bindings for `datetime.timedelta` +/// Bindings for `datetime.timedelta`. +/// +/// Values of this type are accessed via PyO3's smart pointers, e.g. as +/// [`Py`][crate::Py] or [`Bound<'py, PyDelta>`][Bound]. #[repr(transparent)] pub struct PyDelta(PyAny); pyobject_native_type!( diff --git a/src/types/dict.rs b/src/types/dict.rs index 850e468c672..50d00477c2e 100644 --- a/src/types/dict.rs +++ b/src/types/dict.rs @@ -11,6 +11,12 @@ use crate::PyNativeType; use crate::{ffi, Python, ToPyObject}; /// Represents a Python `dict`. +/// +/// Values of this type are accessed via PyO3's smart pointers, e.g. as +/// [`Py`][crate::Py] or [`Bound<'py, PyDict>`][Bound]. +/// +/// For APIs available on `dict` objects, see the [`PyDictMethods`] trait which is implemented for +/// [`Bound<'py, PyDict>`][Bound]. #[repr(transparent)] pub struct PyDict(PyAny); diff --git a/src/types/ellipsis.rs b/src/types/ellipsis.rs index cbeaf489c17..1dc7da08b55 100644 --- a/src/types/ellipsis.rs +++ b/src/types/ellipsis.rs @@ -4,6 +4,9 @@ use crate::{ }; /// Represents the Python `Ellipsis` object. +/// +/// Values of this type are accessed via PyO3's smart pointers, e.g. as +/// [`Py`][crate::Py] or [`Bound<'py, PyEllipsis>`][Bound]. #[repr(transparent)] pub struct PyEllipsis(PyAny); diff --git a/src/types/float.rs b/src/types/float.rs index 8499d1e54aa..1e6cbe51eaf 100644 --- a/src/types/float.rs +++ b/src/types/float.rs @@ -11,9 +11,15 @@ use std::os::raw::c_double; /// Represents a Python `float` object. /// +/// Values of this type are accessed via PyO3's smart pointers, e.g. as +/// [`Py`][crate::Py] or [`Bound<'py, PyFloat>`][Bound]. +/// +/// For APIs available on `float` objects, see the [`PyFloatMethods`] trait which is implemented for +/// [`Bound<'py, PyFloat>`][Bound]. +/// /// You can usually avoid directly working with this type -/// by using [`ToPyObject`] and [`extract`](PyAnyMethods::extract) -/// with `f32`/`f64`. +/// by using [`ToPyObject`] and [`extract`][PyAnyMethods::extract] +/// with [`f32`]/[`f64`]. #[repr(transparent)] pub struct PyFloat(PyAny); diff --git a/src/types/frame.rs b/src/types/frame.rs index 0ab873b8003..8d88d4754ae 100644 --- a/src/types/frame.rs +++ b/src/types/frame.rs @@ -2,6 +2,9 @@ use crate::ffi; use crate::PyAny; /// Represents a Python frame. +/// +/// Values of this type are accessed via PyO3's smart pointers, e.g. as +/// [`Py`][crate::Py] or [`Bound<'py, PyFrame>`][crate::Bound]. #[repr(transparent)] pub struct PyFrame(PyAny); diff --git a/src/types/frozenset.rs b/src/types/frozenset.rs index 78cbf01df67..4ec83915c70 100644 --- a/src/types/frozenset.rs +++ b/src/types/frozenset.rs @@ -56,7 +56,13 @@ impl<'py> PyFrozenSetBuilder<'py> { } } -/// Represents a Python `frozenset` +/// Represents a Python `frozenset`. +/// +/// Values of this type are accessed via PyO3's smart pointers, e.g. as +/// [`Py`][crate::Py] or [`Bound<'py, PyFrozenSet>`][Bound]. +/// +/// For APIs available on `frozenset` objects, see the [`PyFrozenSetMethods`] trait which is implemented for +/// [`Bound<'py, PyFrozenSet>`][Bound]. #[repr(transparent)] pub struct PyFrozenSet(PyAny); diff --git a/src/types/function.rs b/src/types/function.rs index c2eec04d42f..62a2b30263b 100644 --- a/src/types/function.rs +++ b/src/types/function.rs @@ -16,6 +16,9 @@ use std::cell::UnsafeCell; use std::ffi::CStr; /// Represents a builtin Python function object. +/// +/// Values of this type are accessed via PyO3's smart pointers, e.g. as +/// [`Py`][crate::Py] or [`Bound<'py, PyCFunction>`][Bound]. #[repr(transparent)] pub struct PyCFunction(PyAny); @@ -241,6 +244,9 @@ struct ClosureDestructor { unsafe impl Send for ClosureDestructor {} /// Represents a Python function object. +/// +/// Values of this type are accessed via PyO3's smart pointers, e.g. as +/// [`Py`][crate::Py] or [`Bound<'py, PyFunction>`][Bound]. #[repr(transparent)] #[cfg(all(not(Py_LIMITED_API), not(all(PyPy, not(Py_3_8)))))] pub struct PyFunction(PyAny); diff --git a/src/types/iterator.rs b/src/types/iterator.rs index 1835f484adf..38f3131be90 100644 --- a/src/types/iterator.rs +++ b/src/types/iterator.rs @@ -7,6 +7,9 @@ use crate::{AsPyPointer, PyDowncastError, PyNativeType}; /// A Python iterator object. /// +/// Values of this type are accessed via PyO3's smart pointers, e.g. as +/// [`Py`][crate::Py] or [`Bound<'py, PyIterator>`][Bound]. +/// /// # Examples /// /// ```rust diff --git a/src/types/list.rs b/src/types/list.rs index 0d911e03199..9cfd574cd0f 100644 --- a/src/types/list.rs +++ b/src/types/list.rs @@ -14,6 +14,12 @@ use crate::types::any::PyAnyMethods; use crate::types::sequence::PySequenceMethods; /// Represents a Python `list`. +/// +/// Values of this type are accessed via PyO3's smart pointers, e.g. as +/// [`Py`][crate::Py] or [`Bound<'py, PyList>`][Bound]. +/// +/// For APIs available on `list` objects, see the [`PyListMethods`] trait which is implemented for +/// [`Bound<'py, PyDict>`][Bound]. #[repr(transparent)] pub struct PyList(PyAny); diff --git a/src/types/mapping.rs b/src/types/mapping.rs index aea2b484c3b..82e6b326810 100644 --- a/src/types/mapping.rs +++ b/src/types/mapping.rs @@ -11,6 +11,12 @@ use crate::{err::PyDowncastError, PyNativeType}; use crate::{ffi, Py, PyTypeCheck, Python, ToPyObject}; /// Represents a reference to a Python object supporting the mapping protocol. +/// +/// Values of this type are accessed via PyO3's smart pointers, e.g. as +/// [`Py`][crate::Py] or [`Bound<'py, PyMapping>`][Bound]. +/// +/// For APIs available on mapping objects, see the [`PyMappingMethods`] trait which is implemented for +/// [`Bound<'py, PyMapping>`][Bound]. #[repr(transparent)] pub struct PyMapping(PyAny); pyobject_native_type_named!(PyMapping); diff --git a/src/types/memoryview.rs b/src/types/memoryview.rs index 320b3f9f70b..bff1fdcd4c9 100644 --- a/src/types/memoryview.rs +++ b/src/types/memoryview.rs @@ -6,6 +6,9 @@ use crate::{ffi, Bound, PyAny}; use crate::{AsPyPointer, PyNativeType}; /// Represents a Python `memoryview`. +/// +/// Values of this type are accessed via PyO3's smart pointers, e.g. as +/// [`Py`][crate::Py] or [`Bound<'py, PyMemoryView>`][Bound]. #[repr(transparent)] pub struct PyMemoryView(PyAny); diff --git a/src/types/module.rs b/src/types/module.rs index e866ec9cb48..5438c22f681 100644 --- a/src/types/module.rs +++ b/src/types/module.rs @@ -15,6 +15,12 @@ use {super::PyStringMethods, crate::PyNativeType}; /// Represents a Python [`module`][1] object. /// +/// Values of this type are accessed via PyO3's smart pointers, e.g. as +/// [`Py`][crate::Py] or [`Bound<'py, PyModule>`][Bound]. +/// +/// For APIs available on `module` objects, see the [`PyModuleMethods`] trait which is implemented for +/// [`Bound<'py, PyModule>`][Bound]. +/// /// As with all other Python objects, modules are first class citizens. /// This means they can be passed to or returned from functions, /// created dynamically, assigned to variables and so forth. diff --git a/src/types/none.rs b/src/types/none.rs index 0ab1570b92d..78f14be2b25 100644 --- a/src/types/none.rs +++ b/src/types/none.rs @@ -5,6 +5,9 @@ use crate::{ }; /// Represents the Python `None` object. +/// +/// Values of this type are accessed via PyO3's smart pointers, e.g. as +/// [`Py`][crate::Py] or [`Bound<'py, PyNone>`][Bound]. #[repr(transparent)] pub struct PyNone(PyAny); diff --git a/src/types/notimplemented.rs b/src/types/notimplemented.rs index 7fad1220b26..6d1808070a6 100644 --- a/src/types/notimplemented.rs +++ b/src/types/notimplemented.rs @@ -4,6 +4,9 @@ use crate::{ }; /// Represents the Python `NotImplemented` object. +/// +/// Values of this type are accessed via PyO3's smart pointers, e.g. as +/// [`Py`][crate::Py] or [`Bound<'py, PyNotImplemented>`][Bound]. #[repr(transparent)] pub struct PyNotImplemented(PyAny); diff --git a/src/types/num.rs b/src/types/num.rs index 924d4b2c593..b43dddbef88 100644 --- a/src/types/num.rs +++ b/src/types/num.rs @@ -2,6 +2,9 @@ use crate::{ffi, PyAny}; /// Represents a Python `int` object. /// +/// Values of this type are accessed via PyO3's smart pointers, e.g. as +/// [`Py`][crate::Py] or [`Bound<'py, PyLong>`][crate::Bound]. +/// /// You can usually avoid directly working with this type /// by using [`ToPyObject`](crate::conversion::ToPyObject) /// and [`extract`](super::PyAnyMethods::extract) diff --git a/src/types/pysuper.rs b/src/types/pysuper.rs index 7c4d781525a..bd9d042a1f0 100644 --- a/src/types/pysuper.rs +++ b/src/types/pysuper.rs @@ -6,7 +6,8 @@ use crate::{PyAny, PyResult}; /// Represents a Python `super` object. /// -/// This type is immutable. +/// Values of this type are accessed via PyO3's smart pointers, e.g. as +/// [`Py`][crate::Py] or [`Bound<'py, PySuper>`][Bound]. #[repr(transparent)] pub struct PySuper(PyAny); diff --git a/src/types/sequence.rs b/src/types/sequence.rs index a5765ebc8b2..39de9efb272 100644 --- a/src/types/sequence.rs +++ b/src/types/sequence.rs @@ -14,6 +14,12 @@ use crate::{err::PyDowncastError, PyNativeType}; use crate::{ffi, FromPyObject, Py, PyTypeCheck, Python, ToPyObject}; /// Represents a reference to a Python object supporting the sequence protocol. +/// +/// Values of this type are accessed via PyO3's smart pointers, e.g. as +/// [`Py`][crate::Py] or [`Bound<'py, PySequence>`][Bound]. +/// +/// For APIs available on sequence objects, see the [`PySequenceMethods`] trait which is implemented for +/// [`Bound<'py, PySequence>`][Bound]. #[repr(transparent)] pub struct PySequence(PyAny); pyobject_native_type_named!(PySequence); diff --git a/src/types/set.rs b/src/types/set.rs index 1bc4c86be51..9dc44745df2 100644 --- a/src/types/set.rs +++ b/src/types/set.rs @@ -11,7 +11,13 @@ use crate::{ use crate::{ffi, PyAny, PyObject, Python, ToPyObject}; use std::ptr; -/// Represents a Python `set` +/// Represents a Python `set`. +/// +/// Values of this type are accessed via PyO3's smart pointers, e.g. as +/// [`Py`][crate::Py] or [`Bound<'py, PySet>`][Bound]. +/// +/// For APIs available on `set` objects, see the [`PySetMethods`] trait which is implemented for +/// [`Bound<'py, PySet>`][Bound]. #[repr(transparent)] pub struct PySet(PyAny); diff --git a/src/types/slice.rs b/src/types/slice.rs index 7daa2c030b6..dafe5053059 100644 --- a/src/types/slice.rs +++ b/src/types/slice.rs @@ -8,6 +8,12 @@ use crate::{Bound, PyAny, PyObject, Python, ToPyObject}; /// Represents a Python `slice`. /// +/// Values of this type are accessed via PyO3's smart pointers, e.g. as +/// [`Py`][crate::Py] or [`Bound<'py, PySlice>`][Bound]. +/// +/// For APIs available on `slice` objects, see the [`PySliceMethods`] trait which is implemented for +/// [`Bound<'py, PySlice>`][Bound]. +/// /// Only `isize` indices supported at the moment by the `PySlice` object. #[repr(transparent)] pub struct PySlice(PyAny); diff --git a/src/types/string.rs b/src/types/string.rs index 828e0024bda..fafeac83091 100644 --- a/src/types/string.rs +++ b/src/types/string.rs @@ -123,10 +123,11 @@ impl<'a> PyStringData<'a> { /// Represents a Python `string` (a Unicode string object). /// -/// This type is only seen inside PyO3's smart pointers as [`Py`], [`Bound<'py, PyString>`], -/// and [`Borrowed<'a, 'py, PyString>`]. +/// Values of this type are accessed via PyO3's smart pointers, e.g. as +/// [`Py`][crate::Py] or [`Bound<'py, PyString>`][Bound]. /// -/// All functionality on this type is implemented through the [`PyStringMethods`] trait. +/// For APIs available on `str` objects, see the [`PyStringMethods`] trait which is implemented for +/// [`Bound<'py, PyString>`][Bound]. /// /// # Equality /// diff --git a/src/types/traceback.rs b/src/types/traceback.rs index dbbdb6a85ea..5e3496145e2 100644 --- a/src/types/traceback.rs +++ b/src/types/traceback.rs @@ -5,6 +5,12 @@ use crate::PyNativeType; use crate::{ffi, Bound, PyAny}; /// Represents a Python traceback. +/// +/// Values of this type are accessed via PyO3's smart pointers, e.g. as +/// [`Py`][crate::Py] or [`Bound<'py, PyTraceback>`][Bound]. +/// +/// For APIs available on traceback objects, see the [`PyTracebackMethods`] trait which is implemented for +/// [`Bound<'py, PyTraceback>`][Bound]. #[repr(transparent)] pub struct PyTraceback(PyAny); diff --git a/src/types/tuple.rs b/src/types/tuple.rs index fcf931c1d8a..aacc1efe431 100644 --- a/src/types/tuple.rs +++ b/src/types/tuple.rs @@ -52,7 +52,11 @@ fn new_from_iter<'py>( /// Represents a Python `tuple` object. /// -/// This type is immutable. +/// Values of this type are accessed via PyO3's smart pointers, e.g. as +/// [`Py`][crate::Py] or [`Bound<'py, PyTuple>`][Bound]. +/// +/// For APIs available on `tuple` objects, see the [`PyTupleMethods`] trait which is implemented for +/// [`Bound<'py, PyTuple>`][Bound]. #[repr(transparent)] pub struct PyTuple(PyAny); diff --git a/src/types/typeobject.rs b/src/types/typeobject.rs index 9638a2731a3..dbe4c3efd10 100644 --- a/src/types/typeobject.rs +++ b/src/types/typeobject.rs @@ -9,7 +9,14 @@ use crate::PyNativeType; use crate::{ffi, Bound, PyAny, PyTypeInfo, Python}; use super::PyString; -/// Represents a reference to a Python `type object`. + +/// Represents a reference to a Python `type` object. +/// +/// Values of this type are accessed via PyO3's smart pointers, e.g. as +/// [`Py`][crate::Py] or [`Bound<'py, PyType>`][Bound]. +/// +/// For APIs available on `type` objects, see the [`PyTypeMethods`] trait which is implemented for +/// [`Bound<'py, PyType>`][Bound]. #[repr(transparent)] pub struct PyType(PyAny); diff --git a/tests/ui/invalid_pyfunctions.stderr b/tests/ui/invalid_pyfunctions.stderr index 0f94ef17254..9a42c59366e 100644 --- a/tests/ui/invalid_pyfunctions.stderr +++ b/tests/ui/invalid_pyfunctions.stderr @@ -47,11 +47,11 @@ error: expected `&PyModule` or `Py` as first argument with `pass_modul 32 | fn pass_module_but_no_arguments<'py>() {} | ^^ -error[E0277]: the trait bound `&str: From>` is not satisfied +error[E0277]: the trait bound `&str: From>` is not satisfied --> tests/ui/invalid_pyfunctions.rs:36:14 | 36 | _string: &str, - | ^ the trait `From>` is not implemented for `&str`, which is required by `BoundRef<'_, '_, pyo3::prelude::PyModule>: Into<_>` + | ^ the trait `From>` is not implemented for `&str`, which is required by `BoundRef<'_, '_, pyo3::types::PyModule>: Into<_>` | = help: the following other types implement trait `From`: > @@ -60,4 +60,4 @@ error[E0277]: the trait bound `&str: From>> >> > - = note: required for `BoundRef<'_, '_, pyo3::prelude::PyModule>` to implement `Into<&str>` + = note: required for `BoundRef<'_, '_, pyo3::types::PyModule>` to implement `Into<&str>` From e73112f3f6b9a3cf01f1b292726279e45366660e Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Tue, 9 Jul 2024 08:15:12 -0400 Subject: [PATCH 147/495] Automatically treat nested modules as submodules (#4308) fixes #4286 --- guide/src/module.md | 4 ++-- newsfragments/4308.changed.md | 1 + pyo3-macros-backend/src/module.rs | 12 ++++++++++-- tests/test_compile_error.rs | 1 + tests/test_declarative_module.rs | 2 +- tests/ui/duplicate_pymodule_submodule.rs | 7 +++++++ tests/ui/duplicate_pymodule_submodule.stderr | 11 +++++++++++ 7 files changed, 33 insertions(+), 5 deletions(-) create mode 100644 newsfragments/4308.changed.md create mode 100644 tests/ui/duplicate_pymodule_submodule.rs create mode 100644 tests/ui/duplicate_pymodule_submodule.stderr diff --git a/guide/src/module.md b/guide/src/module.md index a2cb8b37a05..ee5485e0ee2 100644 --- a/guide/src/module.md +++ b/guide/src/module.md @@ -151,7 +151,6 @@ For nested modules, the name of the parent module is automatically added. In the following example, the `Unit` class will have for `module` `my_extension.submodule` because it is properly nested but the `Ext` class will have for `module` the default `builtins` because it not nested. -You can provide the `submodule` argument to `pymodule()` for modules that are not top-level modules. ```rust # mod declarative_module_module_attr_test { use pyo3::prelude::*; @@ -166,7 +165,7 @@ mod my_extension { #[pymodule_export] use super::Ext; - #[pymodule(submodule)] + #[pymodule] mod submodule { use super::*; // This is a submodule @@ -179,3 +178,4 @@ mod my_extension { ``` It is possible to customize the `module` value for a `#[pymodule]` with the `#[pyo3(module = "MY_MODULE")]` option. +You can provide the `submodule` argument to `pymodule()` for modules that are not top-level modules -- it is automatically set for modules nested inside of a `#[pymodule]`. diff --git a/newsfragments/4308.changed.md b/newsfragments/4308.changed.md new file mode 100644 index 00000000000..6b5310cdd36 --- /dev/null +++ b/newsfragments/4308.changed.md @@ -0,0 +1 @@ +Nested declarative `#[pymodule]` are automatically treated as submodules (no `PyInit_` entrypoint is created) diff --git a/pyo3-macros-backend/src/module.rs b/pyo3-macros-backend/src/module.rs index 26e3816c8c4..70941cf5ec8 100644 --- a/pyo3-macros-backend/src/module.rs +++ b/pyo3-macros-backend/src/module.rs @@ -80,7 +80,7 @@ impl PyModuleOptions { fn set_submodule(&mut self, submod: SubmoduleAttribute) -> Result<()> { ensure_spanned!( !self.is_submodule, - submod.span() => "`submodule` may only be specified once" + submod.span() => "`submodule` may only be specified once (it is implicitly always specified for nested modules)" ); self.is_submodule = true; @@ -116,7 +116,14 @@ pub fn pymodule_module_impl( } else { name.to_string() }; - is_submodule = is_submodule || options.is_submodule; + + is_submodule = match (is_submodule, options.is_submodule) { + (true, true) => { + bail_spanned!(module.span() => "`submodule` may only be specified once (it is implicitly always specified for nested modules)") + } + (false, false) => false, + (true, false) | (false, true) => true, + }; let mut module_items = Vec::new(); let mut module_items_cfg_attrs = Vec::new(); @@ -273,6 +280,7 @@ pub fn pymodule_module_impl( )? { set_module_attribute(&mut item_mod.attrs, &full_name); } + item_mod.attrs.push(parse_quote!(#[pyo3(submodule)])); } } Item::ForeignMod(item) => { diff --git a/tests/test_compile_error.rs b/tests/test_compile_error.rs index bdfc4893ed1..0d3fa0459d3 100644 --- a/tests/test_compile_error.rs +++ b/tests/test_compile_error.rs @@ -66,4 +66,5 @@ fn test_compile_errors() { t.compile_fail("tests/ui/abi3_weakref.rs"); #[cfg(all(Py_LIMITED_API, not(Py_3_9)))] t.compile_fail("tests/ui/abi3_dict.rs"); + t.compile_fail("tests/ui/duplicate_pymodule_submodule.rs"); } diff --git a/tests/test_declarative_module.rs b/tests/test_declarative_module.rs index f62d51822ee..f0952438241 100644 --- a/tests/test_declarative_module.rs +++ b/tests/test_declarative_module.rs @@ -115,7 +115,7 @@ mod declarative_module { } } - #[pymodule(submodule)] + #[pymodule] #[pyo3(module = "custom_root")] mod inner_custom_root { use super::*; diff --git a/tests/ui/duplicate_pymodule_submodule.rs b/tests/ui/duplicate_pymodule_submodule.rs new file mode 100644 index 00000000000..774d3819645 --- /dev/null +++ b/tests/ui/duplicate_pymodule_submodule.rs @@ -0,0 +1,7 @@ +#[pyo3::pymodule] +mod mymodule { + #[pyo3::pymodule(submodule)] + mod submod {} +} + +fn main() {} diff --git a/tests/ui/duplicate_pymodule_submodule.stderr b/tests/ui/duplicate_pymodule_submodule.stderr new file mode 100644 index 00000000000..a16e9ac75b6 --- /dev/null +++ b/tests/ui/duplicate_pymodule_submodule.stderr @@ -0,0 +1,11 @@ +error: `submodule` may only be specified once (it is implicitly always specified for nested modules) + --> tests/ui/duplicate_pymodule_submodule.rs:4:2 + | +4 | mod submod {} + | ^^^ + +error[E0433]: failed to resolve: use of undeclared crate or module `submod` + --> tests/ui/duplicate_pymodule_submodule.rs:4:6 + | +4 | mod submod {} + | ^^^^^^ use of undeclared crate or module `submod` From 8652ac8e1cffab51b77b4f7069857b0eb99e42c1 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Tue, 9 Jul 2024 17:44:27 +0100 Subject: [PATCH 148/495] remove all functionality deprecated in 0.20 (#4322) * remove all functionality deprecated in 0.20 * bump version to 0.23.0-dev * undo unintended revert * fixup UI test --- Cargo.toml | 8 +-- 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/4322.changed.md | 1 + newsfragments/4322.removed.md | 1 + pyo3-build-config/Cargo.toml | 2 +- pyo3-ffi/Cargo.toml | 4 +- pyo3-macros-backend/Cargo.toml | 6 +-- pyo3-macros-backend/src/deprecations.rs | 53 ++----------------- pyo3-macros-backend/src/konst.rs | 19 +++---- pyo3-macros-backend/src/method.rs | 27 ++-------- pyo3-macros-backend/src/pyclass.rs | 30 +++-------- pyo3-macros-backend/src/pyfunction.rs | 2 - pyo3-macros-backend/src/pyimpl.rs | 6 +-- pyo3-macros-backend/src/pymethod.rs | 11 ++-- pyo3-macros/Cargo.toml | 4 +- pyproject.toml | 2 +- src/impl_.rs | 1 - src/impl_/deprecations.rs | 4 -- src/instance.rs | 25 --------- src/lib.rs | 10 ---- src/types/any.rs | 10 +--- src/types/dict.rs | 38 ------------- tests/ui/deprecations.rs | 15 +----- tests/ui/deprecations.stderr | 30 +++++------ 28 files changed, 62 insertions(+), 257 deletions(-) create mode 100644 newsfragments/4322.changed.md create mode 100644 newsfragments/4322.removed.md delete mode 100644 src/impl_/deprecations.rs diff --git a/Cargo.toml b/Cargo.toml index 364fbac81c1..9806fdb3e73 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3" -version = "0.22.1" +version = "0.23.0-dev" description = "Bindings to Python interpreter" authors = ["PyO3 Project and Contributors "] readme = "README.md" @@ -21,10 +21,10 @@ memoffset = "0.9" once_cell = "1.13.0" # ffi bindings to the python interpreter, split into a separate crate so they can be used independently -pyo3-ffi = { path = "pyo3-ffi", version = "=0.22.1" } +pyo3-ffi = { path = "pyo3-ffi", version = "=0.23.0-dev" } # support crates for macros feature -pyo3-macros = { path = "pyo3-macros", version = "=0.22.1", optional = true } +pyo3-macros = { path = "pyo3-macros", version = "=0.23.0-dev", optional = true } indoc = { version = "2.0.1", optional = true } unindent = { version = "0.2.1", optional = true } @@ -63,7 +63,7 @@ rayon = "1.6.1" futures = "0.3.28" [build-dependencies] -pyo3-build-config = { path = "pyo3-build-config", version = "=0.22.1", features = ["resolve-config"] } +pyo3-build-config = { path = "pyo3-build-config", version = "=0.23.0-dev", features = ["resolve-config"] } [features] default = ["macros"] diff --git a/examples/decorator/.template/pre-script.rhai b/examples/decorator/.template/pre-script.rhai index f21daafeb2a..659b9c14e2b 100644 --- a/examples/decorator/.template/pre-script.rhai +++ b/examples/decorator/.template/pre-script.rhai @@ -1,4 +1,4 @@ -variable::set("PYO3_VERSION", "0.22.1"); +variable::set("PYO3_VERSION", "0.23.0-dev"); 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 f21daafeb2a..659b9c14e2b 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.22.1"); +variable::set("PYO3_VERSION", "0.23.0-dev"); 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 086868dfc42..5c9f9686c0b 100644 --- a/examples/plugin/.template/pre-script.rhai +++ b/examples/plugin/.template/pre-script.rhai @@ -1,4 +1,4 @@ -variable::set("PYO3_VERSION", "0.22.1"); +variable::set("PYO3_VERSION", "0.23.0-dev"); 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 0679e89ab3f..00bb560445b 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.22.1"); +variable::set("PYO3_VERSION", "0.23.0-dev"); 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 f21daafeb2a..659b9c14e2b 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.22.1"); +variable::set("PYO3_VERSION", "0.23.0-dev"); file::rename(".template/Cargo.toml", "Cargo.toml"); file::rename(".template/pyproject.toml", "pyproject.toml"); file::delete(".template"); diff --git a/newsfragments/4322.changed.md b/newsfragments/4322.changed.md new file mode 100644 index 00000000000..dd15a89dcba --- /dev/null +++ b/newsfragments/4322.changed.md @@ -0,0 +1 @@ +Deprecate `PyAnyMethods::is_ellipsis` (`Py::is_ellpsis` was deprecated in PyO3 0.20). diff --git a/newsfragments/4322.removed.md b/newsfragments/4322.removed.md new file mode 100644 index 00000000000..4d8f62e4aef --- /dev/null +++ b/newsfragments/4322.removed.md @@ -0,0 +1 @@ +Remove all functionality deprecated in PyO3 0.20. diff --git a/pyo3-build-config/Cargo.toml b/pyo3-build-config/Cargo.toml index d8c84685c7c..a5549df3ecb 100644 --- a/pyo3-build-config/Cargo.toml +++ b/pyo3-build-config/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3-build-config" -version = "0.22.1" +version = "0.23.0-dev" 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 85de25c80f0..8e1b203fcd1 100644 --- a/pyo3-ffi/Cargo.toml +++ b/pyo3-ffi/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3-ffi" -version = "0.22.1" +version = "0.23.0-dev" description = "Python-API bindings for the PyO3 ecosystem" authors = ["PyO3 Project and Contributors "] keywords = ["pyo3", "python", "cpython", "ffi"] @@ -38,7 +38,7 @@ abi3-py312 = ["abi3", "pyo3-build-config/abi3-py312"] generate-import-lib = ["pyo3-build-config/python3-dll-a"] [build-dependencies] -pyo3-build-config = { path = "../pyo3-build-config", version = "=0.22.1", features = ["resolve-config"] } +pyo3-build-config = { path = "../pyo3-build-config", version = "=0.23.0-dev", features = ["resolve-config"] } [lints] workspace = true diff --git a/pyo3-macros-backend/Cargo.toml b/pyo3-macros-backend/Cargo.toml index 280e12e37a6..b3775ef8518 100644 --- a/pyo3-macros-backend/Cargo.toml +++ b/pyo3-macros-backend/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3-macros-backend" -version = "0.22.1" +version = "0.23.0-dev" description = "Code generation for PyO3 package" authors = ["PyO3 Project and Contributors "] keywords = ["pyo3", "python", "cpython", "ffi"] @@ -16,7 +16,7 @@ edition = "2021" [dependencies] heck = "0.5" proc-macro2 = { version = "1.0.60", default-features = false } -pyo3-build-config = { path = "../pyo3-build-config", version = "=0.22.1", features = ["resolve-config"] } +pyo3-build-config = { path = "../pyo3-build-config", version = "=0.23.0-dev", features = ["resolve-config"] } quote = { version = "1", default-features = false } [dependencies.syn] @@ -25,7 +25,7 @@ default-features = false features = ["derive", "parsing", "printing", "clone-impls", "full", "extra-traits"] [build-dependencies] -pyo3-build-config = { path = "../pyo3-build-config", version = "=0.22.1" } +pyo3-build-config = { path = "../pyo3-build-config", version = "=0.23.0-dev" } [lints] workspace = true diff --git a/pyo3-macros-backend/src/deprecations.rs b/pyo3-macros-backend/src/deprecations.rs index 426ca2c0c7d..df48c9da417 100644 --- a/pyo3-macros-backend/src/deprecations.rs +++ b/pyo3-macros-backend/src/deprecations.rs @@ -1,53 +1,6 @@ -use crate::{ - method::{FnArg, FnSpec}, - utils::Ctx, -}; -use proc_macro2::{Span, TokenStream}; -use quote::{quote_spanned, ToTokens}; - -pub enum Deprecation { - PyMethodsNewDeprecatedForm, -} - -impl Deprecation { - fn ident(&self, span: Span) -> syn::Ident { - let string = match self { - Deprecation::PyMethodsNewDeprecatedForm => "PYMETHODS_NEW_DEPRECATED_FORM", - }; - syn::Ident::new(string, span) - } -} - -pub struct Deprecations<'ctx>(Vec<(Deprecation, Span)>, &'ctx Ctx); - -impl<'ctx> Deprecations<'ctx> { - pub fn new(ctx: &'ctx Ctx) -> Self { - Deprecations(Vec::new(), ctx) - } - - pub fn push(&mut self, deprecation: Deprecation, span: Span) { - self.0.push((deprecation, span)) - } -} - -impl<'ctx> ToTokens for Deprecations<'ctx> { - fn to_tokens(&self, tokens: &mut TokenStream) { - let Self(deprecations, Ctx { pyo3_path, .. }) = self; - - for (deprecation, span) in deprecations { - let pyo3_path = pyo3_path.to_tokens_spanned(*span); - let ident = deprecation.ident(*span); - quote_spanned!( - *span => - #[allow(clippy::let_unit_value)] - { - let _ = #pyo3_path::impl_::deprecations::#ident; - } - ) - .to_tokens(tokens) - } - } -} +use crate::method::{FnArg, FnSpec}; +use proc_macro2::TokenStream; +use quote::quote_spanned; pub(crate) fn deprecate_trailing_option_default(spec: &FnSpec<'_>) -> TokenStream { if spec.signature.attribute.is_none() diff --git a/pyo3-macros-backend/src/konst.rs b/pyo3-macros-backend/src/konst.rs index 3547698dc62..ae88f785249 100644 --- a/pyo3-macros-backend/src/konst.rs +++ b/pyo3-macros-backend/src/konst.rs @@ -1,11 +1,8 @@ use std::borrow::Cow; use std::ffi::CString; +use crate::attributes::{self, get_pyo3_options, take_attributes, NameAttribute}; use crate::utils::{Ctx, LitCStr}; -use crate::{ - attributes::{self, get_pyo3_options, take_attributes, NameAttribute}, - deprecations::Deprecations, -}; use proc_macro2::{Ident, Span}; use syn::{ ext::IdentExt, @@ -14,12 +11,12 @@ use syn::{ Result, }; -pub struct ConstSpec<'ctx> { +pub struct ConstSpec { pub rust_ident: syn::Ident, - pub attributes: ConstAttributes<'ctx>, + pub attributes: ConstAttributes, } -impl ConstSpec<'_> { +impl ConstSpec { pub fn python_name(&self) -> Cow<'_, Ident> { if let Some(name) = &self.attributes.name { Cow::Borrowed(&name.value.0) @@ -35,10 +32,9 @@ impl ConstSpec<'_> { } } -pub struct ConstAttributes<'ctx> { +pub struct ConstAttributes { pub is_class_attr: bool, pub name: Option, - pub deprecations: Deprecations<'ctx>, } pub enum PyO3ConstAttribute { @@ -56,12 +52,11 @@ impl Parse for PyO3ConstAttribute { } } -impl<'ctx> ConstAttributes<'ctx> { - pub fn from_attrs(attrs: &mut Vec, ctx: &'ctx Ctx) -> syn::Result { +impl ConstAttributes { + pub fn from_attrs(attrs: &mut Vec) -> syn::Result { let mut attributes = ConstAttributes { is_class_attr: false, name: None, - deprecations: Deprecations::new(ctx), }; take_attributes(attrs, |attr| { diff --git a/pyo3-macros-backend/src/method.rs b/pyo3-macros-backend/src/method.rs index 3efcc3e9967..d5e26f694f3 100644 --- a/pyo3-macros-backend/src/method.rs +++ b/pyo3-macros-backend/src/method.rs @@ -10,7 +10,6 @@ use crate::deprecations::deprecate_trailing_option_default; use crate::utils::{Ctx, LitCStr}; use crate::{ attributes::{FromPyWithAttribute, TextSignatureAttribute, TextSignatureAttributeValue}, - deprecations::{Deprecation, Deprecations}, params::{impl_arg_params, Holders}, pyfunction::{ FunctionSignature, PyFunctionArgPyO3Attributes, PyFunctionOptions, SignatureAttribute, @@ -411,7 +410,6 @@ pub struct FnSpec<'a> { pub text_signature: Option, pub asyncness: Option, pub unsafety: Option, - pub deprecations: Deprecations<'a>, } pub fn parse_method_receiver(arg: &syn::FnArg) -> Result { @@ -443,7 +441,6 @@ impl<'a> FnSpec<'a> { sig: &'a mut syn::Signature, meth_attrs: &mut Vec, options: PyFunctionOptions, - ctx: &'a Ctx, ) -> Result> { let PyFunctionOptions { text_signature, @@ -453,9 +450,8 @@ impl<'a> FnSpec<'a> { } = options; let mut python_name = name.map(|name| name.value.0); - let mut deprecations = Deprecations::new(ctx); - let fn_type = Self::parse_fn_type(sig, meth_attrs, &mut python_name, &mut deprecations)?; + let fn_type = Self::parse_fn_type(sig, meth_attrs, &mut python_name)?; ensure_signatures_on_valid_method(&fn_type, signature.as_ref(), text_signature.as_ref())?; let name = &sig.ident; @@ -493,7 +489,6 @@ impl<'a> FnSpec<'a> { text_signature, asyncness: sig.asyncness, unsafety: sig.unsafety, - deprecations, }) } @@ -507,9 +502,8 @@ impl<'a> FnSpec<'a> { sig: &syn::Signature, meth_attrs: &mut Vec, python_name: &mut Option, - deprecations: &mut Deprecations<'_>, ) -> Result { - let mut method_attributes = parse_method_attributes(meth_attrs, deprecations)?; + let mut method_attributes = parse_method_attributes(meth_attrs)?; let name = &sig.ident; let parse_receiver = |msg: &'static str| { @@ -982,10 +976,7 @@ impl MethodTypeAttribute { /// If the attribute does not match one of the attribute names, returns `Ok(None)`. /// /// Otherwise will either return a parse error or the attribute. - fn parse_if_matching_attribute( - attr: &syn::Attribute, - deprecations: &mut Deprecations<'_>, - ) -> Result> { + fn parse_if_matching_attribute(attr: &syn::Attribute) -> Result> { fn ensure_no_arguments(meta: &syn::Meta, ident: &str) -> syn::Result<()> { match meta { syn::Meta::Path(_) => Ok(()), @@ -1029,11 +1020,6 @@ impl MethodTypeAttribute { if path.is_ident("new") { ensure_no_arguments(meta, "new")?; Ok(Some(MethodTypeAttribute::New(path.span()))) - } else if path.is_ident("__new__") { - let span = path.span(); - deprecations.push(Deprecation::PyMethodsNewDeprecatedForm, span); - ensure_no_arguments(meta, "__new__")?; - Ok(Some(MethodTypeAttribute::New(span))) } else if path.is_ident("classmethod") { ensure_no_arguments(meta, "classmethod")?; Ok(Some(MethodTypeAttribute::ClassMethod(path.span()))) @@ -1068,15 +1054,12 @@ impl Display for MethodTypeAttribute { } } -fn parse_method_attributes( - attrs: &mut Vec, - deprecations: &mut Deprecations<'_>, -) -> Result> { +fn parse_method_attributes(attrs: &mut Vec) -> Result> { let mut new_attrs = Vec::new(); let mut found_attrs = Vec::new(); for attr in attrs.drain(..) { - match MethodTypeAttribute::parse_if_matching_attribute(&attr, deprecations)? { + match MethodTypeAttribute::parse_if_matching_attribute(&attr)? { Some(attr) => found_attrs.push(attr), None => new_attrs.push(attr), } diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index fd85cfa3bb6..dab0102bbce 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -12,7 +12,6 @@ use crate::attributes::{ self, kw, take_pyo3_options, CrateAttribute, ExtendsAttribute, FreelistAttribute, ModuleAttribute, NameAttribute, NameLitStr, RenameAllAttribute, }; -use crate::deprecations::Deprecations; use crate::konst::{ConstAttributes, ConstSpec}; use crate::method::{FnArg, FnSpec, PyArg, RegularArg}; use crate::pyfunction::ConstructorAttribute; @@ -384,7 +383,7 @@ fn impl_class( ctx: &Ctx, ) -> syn::Result { let Ctx { pyo3_path, .. } = ctx; - let pytypeinfo_impl = impl_pytypeinfo(cls, args, None, ctx); + let pytypeinfo_impl = impl_pytypeinfo(cls, args, ctx); let (default_richcmp, default_richcmp_slot) = pyclass_richcmp(&args.options, &syn::parse_quote!(#cls), ctx)?; @@ -779,7 +778,7 @@ fn impl_simple_enum( let cls = simple_enum.ident; let ty: syn::Type = syn::parse_quote!(#cls); let variants = simple_enum.variants; - let pytypeinfo = impl_pytypeinfo(cls, args, None, ctx); + let pytypeinfo = impl_pytypeinfo(cls, args, ctx); for variant in &variants { ensure_spanned!(variant.options.constructor.is_none(), variant.options.constructor.span() => "`constructor` can't be used on a simple enum variant"); @@ -889,7 +888,7 @@ fn impl_complex_enum( let ctx = &Ctx::new(&args.options.krate, None); let cls = complex_enum.ident; let variants = complex_enum.variants; - let pytypeinfo = impl_pytypeinfo(cls, &args, None, ctx); + let pytypeinfo = impl_pytypeinfo(cls, &args, ctx); let (default_richcmp, default_richcmp_slot) = pyclass_richcmp(&args.options, &ty, ctx)?; let (default_hash, default_hash_slot) = pyclass_hash(&args.options, &ty, ctx)?; @@ -977,7 +976,7 @@ fn impl_complex_enum( }, }; - let variant_cls_pytypeinfo = impl_pytypeinfo(&variant_cls, &variant_args, None, ctx); + let variant_cls_pytypeinfo = impl_pytypeinfo(&variant_cls, &variant_args, ctx); variant_cls_pytypeinfos.push(variant_cls_pytypeinfo); let (variant_cls_impl, field_getters, mut slots) = @@ -1057,7 +1056,6 @@ fn impl_complex_enum_variant_match_args( attributes: ConstAttributes { is_class_attr: true, name: None, - deprecations: Deprecations::new(ctx), }, }; @@ -1318,7 +1316,6 @@ fn generate_protocol_slot( &mut method.sig, &mut Vec::new(), PyFunctionOptions::default(), - ctx, ) .unwrap(); slot.generate_type_slot(&syn::parse_quote!(#cls), &spec, name, ctx) @@ -1334,7 +1331,6 @@ fn generate_default_protocol_slot( &mut method.sig, &mut Vec::new(), PyFunctionOptions::default(), - ctx, ) .unwrap(); let name = spec.name.to_string(); @@ -1360,7 +1356,6 @@ fn simple_enum_default_methods<'a>( kw: syn::parse_quote! { name }, value: NameLitStr(py_ident.clone()), }), - deprecations: Deprecations::new(ctx), }, }; unit_variant_names @@ -1383,7 +1378,6 @@ fn complex_enum_default_methods<'a>( kw: syn::parse_quote! { name }, value: NameLitStr(py_ident.clone()), }), - deprecations: Deprecations::new(ctx), }, }; variant_names @@ -1397,19 +1391,17 @@ fn complex_enum_default_methods<'a>( pub fn gen_complex_enum_variant_attr( cls: &syn::Ident, cls_type: &syn::Type, - spec: &ConstSpec<'_>, + spec: &ConstSpec, ctx: &Ctx, ) -> MethodAndMethodDef { let Ctx { pyo3_path, .. } = ctx; let member = &spec.rust_ident; let wrapper_ident = format_ident!("__pymethod_variant_cls_{}__", member); - let deprecations = &spec.attributes.deprecations; let python_name = spec.null_terminated_python_name(ctx); let variant_cls = format_ident!("{}_{}", cls, member); let associated_method = quote! { fn #wrapper_ident(py: #pyo3_path::Python<'_>) -> #pyo3_path::PyResult<#pyo3_path::PyObject> { - #deprecations ::std::result::Result::Ok(py.get_type_bound::<#variant_cls>().into_any().unbind()) } }; @@ -1497,7 +1489,6 @@ fn complex_enum_struct_variant_new<'a>( text_signature: None, asyncness: None, unsafety: None, - deprecations: Deprecations::new(ctx), }; crate::pymethod::impl_py_method_def_new(&variant_cls_type, &spec, ctx) @@ -1552,7 +1543,6 @@ fn complex_enum_tuple_variant_new<'a>( text_signature: None, asyncness: None, unsafety: None, - deprecations: Deprecations::new(ctx), }; crate::pymethod::impl_py_method_def_new(&variant_cls_type, &spec, ctx) @@ -1577,7 +1567,6 @@ fn complex_enum_variant_field_getter<'a>( text_signature: None, asyncness: None, unsafety: None, - deprecations: Deprecations::new(ctx), }; let property_type = crate::pymethod::PropertyType::Function { @@ -1641,12 +1630,7 @@ fn descriptors_to_items( Ok(items) } -fn impl_pytypeinfo( - cls: &syn::Ident, - attr: &PyClassArgs, - deprecations: Option<&Deprecations<'_>>, - ctx: &Ctx, -) -> TokenStream { +fn impl_pytypeinfo(cls: &syn::Ident, attr: &PyClassArgs, ctx: &Ctx) -> TokenStream { let Ctx { pyo3_path, .. } = ctx; let cls_name = get_class_python_name(cls, attr).to_string(); @@ -1677,8 +1661,6 @@ fn impl_pytypeinfo( #[inline] fn type_object_raw(py: #pyo3_path::Python<'_>) -> *mut #pyo3_path::ffi::PyTypeObject { use #pyo3_path::prelude::PyTypeMethods; - #deprecations - <#cls as #pyo3_path::impl_::pyclass::PyClassImpl>::lazy_type_object() .get_or_init(py) .as_type_ptr() diff --git a/pyo3-macros-backend/src/pyfunction.rs b/pyo3-macros-backend/src/pyfunction.rs index 25f0d5b37ae..3059025caf7 100644 --- a/pyo3-macros-backend/src/pyfunction.rs +++ b/pyo3-macros-backend/src/pyfunction.rs @@ -4,7 +4,6 @@ use crate::{ self, get_pyo3_options, take_attributes, take_pyo3_options, CrateAttribute, FromPyWithAttribute, NameAttribute, TextSignatureAttribute, }, - deprecations::Deprecations, method::{self, CallingConvention, FnArg}, pymethod::check_generic, }; @@ -252,7 +251,6 @@ pub fn impl_wrap_pyfunction( text_signature, asyncness: func.sig.asyncness, unsafety: func.sig.unsafety, - deprecations: Deprecations::new(ctx), }; let vis = &func.vis; diff --git a/pyo3-macros-backend/src/pyimpl.rs b/pyo3-macros-backend/src/pyimpl.rs index 6807f90831e..786682f3882 100644 --- a/pyo3-macros-backend/src/pyimpl.rs +++ b/pyo3-macros-backend/src/pyimpl.rs @@ -130,7 +130,7 @@ pub fn impl_methods( } syn::ImplItem::Const(konst) => { let ctx = &Ctx::new(&options.krate, None); - let attributes = ConstAttributes::from_attrs(&mut konst.attrs, ctx)?; + let attributes = ConstAttributes::from_attrs(&mut konst.attrs)?; if attributes.is_class_attr { let spec = ConstSpec { rust_ident: konst.ident.clone(), @@ -182,16 +182,14 @@ pub fn impl_methods( }) } -pub fn gen_py_const(cls: &syn::Type, spec: &ConstSpec<'_>, ctx: &Ctx) -> MethodAndMethodDef { +pub fn gen_py_const(cls: &syn::Type, spec: &ConstSpec, ctx: &Ctx) -> MethodAndMethodDef { let member = &spec.rust_ident; let wrapper_ident = format_ident!("__pymethod_{}__", member); - let deprecations = &spec.attributes.deprecations; let python_name = spec.null_terminated_python_name(ctx); let Ctx { pyo3_path, .. } = ctx; let associated_method = quote! { fn #wrapper_ident(py: #pyo3_path::Python<'_>) -> #pyo3_path::PyResult<#pyo3_path::PyObject> { - #deprecations ::std::result::Result::Ok(#pyo3_path::IntoPy::into_py(#cls::#member, py)) } }; diff --git a/pyo3-macros-backend/src/pymethod.rs b/pyo3-macros-backend/src/pymethod.rs index c02599903e1..150c29ae64f 100644 --- a/pyo3-macros-backend/src/pymethod.rs +++ b/pyo3-macros-backend/src/pymethod.rs @@ -164,9 +164,8 @@ impl<'a> PyMethod<'a> { sig: &'a mut syn::Signature, meth_attrs: &mut Vec, options: PyFunctionOptions, - ctx: &'a Ctx, ) -> Result { - let spec = FnSpec::parse(sig, meth_attrs, options, ctx)?; + let spec = FnSpec::parse(sig, meth_attrs, options)?; let method_name = spec.python_name.to_string(); let kind = PyMethodKind::from_name(&method_name); @@ -195,7 +194,7 @@ pub fn gen_py_method( ) -> Result { check_generic(sig)?; ensure_function_options_valid(&options)?; - let method = PyMethod::parse(sig, meth_attrs, options, ctx)?; + let method = PyMethod::parse(sig, meth_attrs, options)?; let spec = &method.spec; let Ctx { pyo3_path, .. } = ctx; @@ -356,7 +355,6 @@ pub fn impl_py_method_def_new( || quote!(::std::option::Option::None), |text_signature| quote!(::std::option::Option::Some(#text_signature)), ); - let deprecations = &spec.deprecations; let slot_def = quote! { #pyo3_path::ffi::PyType_Slot { slot: #pyo3_path::ffi::Py_tp_new, @@ -365,10 +363,7 @@ pub fn impl_py_method_def_new( subtype: *mut #pyo3_path::ffi::PyTypeObject, args: *mut #pyo3_path::ffi::PyObject, kwargs: *mut #pyo3_path::ffi::PyObject, - ) -> *mut #pyo3_path::ffi::PyObject - { - #deprecations - + ) -> *mut #pyo3_path::ffi::PyObject { use #pyo3_path::impl_::pyclass::*; #[allow(unknown_lints, non_local_definitions)] impl PyClassNewTextSignature<#cls> for PyClassImplCollector<#cls> { diff --git a/pyo3-macros/Cargo.toml b/pyo3-macros/Cargo.toml index 23da8ccd697..42d6d801c89 100644 --- a/pyo3-macros/Cargo.toml +++ b/pyo3-macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3-macros" -version = "0.22.1" +version = "0.23.0-dev" description = "Proc macros for PyO3 package" authors = ["PyO3 Project and Contributors "] keywords = ["pyo3", "python", "cpython", "ffi"] @@ -22,7 +22,7 @@ gil-refs = ["pyo3-macros-backend/gil-refs"] proc-macro2 = { version = "1.0.60", default-features = false } quote = "1" syn = { version = "2", features = ["full", "extra-traits"] } -pyo3-macros-backend = { path = "../pyo3-macros-backend", version = "=0.22.1" } +pyo3-macros-backend = { path = "../pyo3-macros-backend", version = "=0.23.0-dev" } [lints] workspace = true diff --git a/pyproject.toml b/pyproject.toml index 26cbfee0064..27178a2e402 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ [tool.towncrier] filename = "CHANGELOG.md" -version = "0.22.1" +version = "0.23.0-dev" start_string = "\n" template = ".towncrier.template.md" title_format = "## [{version}] - {project_date}" diff --git a/src/impl_.rs b/src/impl_.rs index 71ba397cb94..5bfeda39f65 100644 --- a/src/impl_.rs +++ b/src/impl_.rs @@ -8,7 +8,6 @@ #[cfg(feature = "experimental-async")] pub mod coroutine; -pub mod deprecations; pub mod exceptions; pub mod extract_argument; pub mod freelist; diff --git a/src/impl_/deprecations.rs b/src/impl_/deprecations.rs deleted file mode 100644 index 6b9930ac69b..00000000000 --- a/src/impl_/deprecations.rs +++ /dev/null @@ -1,4 +0,0 @@ -//! Symbols used to denote deprecated usages of PyO3's proc macros. - -#[deprecated(since = "0.20.0", note = "use `#[new]` instead of `#[__new__]`")] -pub const PYMETHODS_NEW_DEPRECATED_FORM: () = (); diff --git a/src/instance.rs b/src/instance.rs index 499f751027c..697df2c7bae 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -1384,14 +1384,6 @@ impl Py { unsafe { ffi::Py_None() == self.as_ptr() } } - /// Returns whether the object is Ellipsis, e.g. `...`. - /// - /// This is equivalent to the Python expression `self is ...`. - #[deprecated(since = "0.20.0", note = "use `.is(py.Ellipsis())` instead")] - pub fn is_ellipsis(&self) -> bool { - unsafe { ffi::Py_Ellipsis() == self.as_ptr() } - } - /// Returns whether the object is considered to be true. /// /// This is equivalent to the Python expression `bool(self)`. @@ -2173,23 +2165,6 @@ a = A() }) } - #[test] - #[allow(deprecated)] - fn test_is_ellipsis() { - Python::with_gil(|py| { - let v = py - .eval_bound("...", None, None) - .map_err(|e| e.display(py)) - .unwrap() - .to_object(py); - - assert!(v.is_ellipsis()); - - let not_ellipsis = 5.to_object(py); - assert!(!not_ellipsis.is_ellipsis()); - }); - } - #[test] fn test_debug_fmt() { Python::with_gil(|py| { diff --git a/src/lib.rs b/src/lib.rs index ef905ec356c..94dc168590c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -498,16 +498,6 @@ pub mod inspect; // other paths to the same items. (e.g. `pyo3::types::PyAnyMethods` instead of `pyo3::prelude::PyAnyMethods`). pub mod prelude; -/// Ths module only contains re-exports of pyo3 deprecation warnings and exists -/// purely to make compiler error messages nicer. -/// -/// (The compiler uses this module in error messages, probably because it's a public -/// re-export at a shorter path than `pyo3::impl_::deprecations`.) -#[doc(hidden)] -pub mod deprecations { - pub use crate::impl_::deprecations::*; -} - /// Test readme and user guide #[cfg(doctest)] pub mod doc_test { diff --git a/src/types/any.rs b/src/types/any.rs index fdeab37712d..c991b69b4a4 100644 --- a/src/types/any.rs +++ b/src/types/any.rs @@ -604,14 +604,6 @@ impl PyAny { self.as_borrowed().is_none() } - /// Returns whether the object is Ellipsis, e.g. `...`. - /// - /// This is equivalent to the Python expression `self is ...`. - #[deprecated(since = "0.20.0", note = "use `.is(py.Ellipsis())` instead")] - pub fn is_ellipsis(&self) -> bool { - self.as_borrowed().is_ellipsis() - } - /// Returns true if the sequence or mapping has a length of 0. /// /// This is equivalent to the Python expression `len(self) == 0`. @@ -1495,6 +1487,7 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed { /// Returns whether the object is Ellipsis, e.g. `...`. /// /// This is equivalent to the Python expression `self is ...`. + #[deprecated(since = "0.23.0", note = "use `.is(py.Ellipsis())` instead")] fn is_ellipsis(&self) -> bool; /// Returns true if the sequence or mapping has a length of 0. @@ -2785,6 +2778,7 @@ class SimpleClass: } #[test] + #[allow(deprecated)] fn test_is_ellipsis() { Python::with_gil(|py| { let v = py diff --git a/src/types/dict.rs b/src/types/dict.rs index 50d00477c2e..001e8584028 100644 --- a/src/types/dict.rs +++ b/src/types/dict.rs @@ -197,19 +197,6 @@ impl PyDict { } } - /// Deprecated version of `get_item`. - #[deprecated( - since = "0.20.0", - note = "this is now equivalent to `PyDict::get_item`" - )] - #[inline] - pub fn get_item_with_error(&self, key: K) -> PyResult> - where - K: ToPyObject, - { - self.get_item(key) - } - /// Sets an item value. /// /// This is equivalent to the Python statement `self[key] = value`. @@ -957,31 +944,6 @@ mod tests { }); } - #[test] - #[allow(deprecated)] - #[cfg(all(not(any(PyPy, GraalPy)), feature = "gil-refs"))] - fn test_get_item_with_error() { - Python::with_gil(|py| { - let mut v = HashMap::new(); - v.insert(7, 32); - let ob = v.to_object(py); - let dict = ob.downcast::(py).unwrap(); - assert_eq!( - 32, - dict.get_item_with_error(7i32) - .unwrap() - .unwrap() - .extract::() - .unwrap() - ); - assert!(dict.get_item_with_error(8i32).unwrap().is_none()); - assert!(dict - .get_item_with_error(dict) - .unwrap_err() - .is_instance_of::(py)); - }); - } - #[test] fn test_set_item() { Python::with_gil(|py| { diff --git a/tests/ui/deprecations.rs b/tests/ui/deprecations.rs index 97ad6629f2f..da78a826cae 100644 --- a/tests/ui/deprecations.rs +++ b/tests/ui/deprecations.rs @@ -3,19 +3,6 @@ use pyo3::prelude::*; -#[pyclass] -struct MyClass; - -#[pymethods] -impl MyClass { - #[__new__] - fn new() -> Self { - Self - } -} - -fn main() {} - fn extract_options(obj: &Bound<'_, PyAny>) -> PyResult> { obj.extract() } @@ -43,3 +30,5 @@ pub enum SimpleEnumWithoutEq { VariamtA, VariantB, } + +fn main() {} diff --git a/tests/ui/deprecations.stderr b/tests/ui/deprecations.stderr index f9e9652e377..f93f99179cf 100644 --- a/tests/ui/deprecations.stderr +++ b/tests/ui/deprecations.stderr @@ -1,8 +1,10 @@ -error: use of deprecated constant `pyo3::deprecations::PYMETHODS_NEW_DEPRECATED_FORM`: use `#[new]` instead of `#[__new__]` - --> tests/ui/deprecations.rs:11:7 +error: use of deprecated constant `__pyfunction_pyfunction_option_2::SIGNATURE`: this function has implicit defaults for the trailing `Option` arguments + = note: these implicit defaults are being phased out + = help: add `#[pyo3(signature = (_i, _any=None))]` to this function to silence this warning and keep the current behavior + --> tests/ui/deprecations.rs:15:4 | -11 | #[__new__] - | ^^^^^^^ +15 | fn pyfunction_option_2(_i: u32, _any: Option) {} + | ^^^^^^^^^^^^^^^^^^^ | note: the lint level is defined here --> tests/ui/deprecations.rs:1:9 @@ -10,34 +12,26 @@ note: the lint level is defined here 1 | #![deny(deprecated)] | ^^^^^^^^^^ -error: use of deprecated constant `__pyfunction_pyfunction_option_2::SIGNATURE`: this function has implicit defaults for the trailing `Option` arguments - = note: these implicit defaults are being phased out - = help: add `#[pyo3(signature = (_i, _any=None))]` to this function to silence this warning and keep the current behavior - --> tests/ui/deprecations.rs:28:4 - | -28 | fn pyfunction_option_2(_i: u32, _any: Option) {} - | ^^^^^^^^^^^^^^^^^^^ - error: use of deprecated constant `__pyfunction_pyfunction_option_3::SIGNATURE`: this function has implicit defaults for the trailing `Option` arguments = note: these implicit defaults are being phased out = help: add `#[pyo3(signature = (_i, _any=None, _foo=None))]` to this function to silence this warning and keep the current behavior - --> tests/ui/deprecations.rs:31:4 + --> tests/ui/deprecations.rs:18:4 | -31 | fn pyfunction_option_3(_i: u32, _any: Option, _foo: Option) {} +18 | fn pyfunction_option_3(_i: u32, _any: Option, _foo: Option) {} | ^^^^^^^^^^^^^^^^^^^ error: use of deprecated constant `__pyfunction_pyfunction_option_4::SIGNATURE`: this function has implicit defaults for the trailing `Option` arguments = note: these implicit defaults are being phased out = help: add `#[pyo3(signature = (_i, _any=None, _foo=None))]` to this function to silence this warning and keep the current behavior - --> tests/ui/deprecations.rs:34:4 + --> tests/ui/deprecations.rs:21:4 | -34 | fn pyfunction_option_4( +21 | fn pyfunction_option_4( | ^^^^^^^^^^^^^^^^^^^ error: use of deprecated constant `SimpleEnumWithoutEq::__pyo3__generated____richcmp__::DEPRECATION`: Implicit equality for simple enums is deprecated. Use `#[pyclass(eq, eq_int)` to keep the current behavior. - --> tests/ui/deprecations.rs:41:1 + --> tests/ui/deprecations.rs:28:1 | -41 | #[pyclass] +28 | #[pyclass] | ^^^^^^^^^^ | = note: this error originates in the attribute macro `pyclass` (in Nightly builds, run with -Z macro-backtrace for more info) From 97d60e869bbfca1eab05f6fd917f694444cc2ff8 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Tue, 9 Jul 2024 22:28:15 +0100 Subject: [PATCH 149/495] clean up hygiene tests (#4328) --- newsfragments/4328.fixed.md | 1 + pyo3-macros-backend/src/module.rs | 2 +- src/tests/hygiene/misc.rs | 18 +++------------- src/tests/hygiene/mod.rs | 3 +++ src/tests/hygiene/pyclass.rs | 3 --- src/tests/hygiene/pyfunction.rs | 15 +------------ src/tests/hygiene/pymethods.rs | 3 --- src/tests/hygiene/pymodule.rs | 35 +++++++++---------------------- 8 files changed, 19 insertions(+), 61 deletions(-) create mode 100644 newsfragments/4328.fixed.md diff --git a/newsfragments/4328.fixed.md b/newsfragments/4328.fixed.md new file mode 100644 index 00000000000..f21fdfbcb76 --- /dev/null +++ b/newsfragments/4328.fixed.md @@ -0,0 +1 @@ +Fix compile failure in declarative `#[pymodule]` under presence of `#![no_implicit_prelude]`. diff --git a/pyo3-macros-backend/src/module.rs b/pyo3-macros-backend/src/module.rs index 70941cf5ec8..78e8999483c 100644 --- a/pyo3-macros-backend/src/module.rs +++ b/pyo3-macros-backend/src/module.rs @@ -373,7 +373,7 @@ pub fn pymodule_module_impl( #module_items::_PYO3_DEF.add_to_module(module)?; )* #pymodule_init - Ok(()) + ::std::result::Result::Ok(()) } } )) diff --git a/src/tests/hygiene/misc.rs b/src/tests/hygiene/misc.rs index 7a2f58818a1..e10a3b46f38 100644 --- a/src/tests/hygiene/misc.rs +++ b/src/tests/hygiene/misc.rs @@ -1,17 +1,13 @@ -#![no_implicit_prelude] - #[derive(crate::FromPyObject)] #[pyo3(crate = "crate")] -struct Derive1(#[allow(dead_code)] i32); // newtype case +struct Derive1(i32); // newtype case #[derive(crate::FromPyObject)] #[pyo3(crate = "crate")] -#[allow(dead_code)] struct Derive2(i32, i32); // tuple case #[derive(crate::FromPyObject)] #[pyo3(crate = "crate")] -#[allow(dead_code)] struct Derive3 { f: i32, #[pyo3(item(42))] @@ -20,7 +16,6 @@ struct Derive3 { #[derive(crate::FromPyObject)] #[pyo3(crate = "crate")] -#[allow(dead_code)] enum Derive4 { A(i32), B { f: i32 }, @@ -29,23 +24,16 @@ enum Derive4 { crate::create_exception!(mymodule, CustomError, crate::exceptions::PyException); crate::import_exception!(socket, gaierror); -#[allow(dead_code)] fn intern(py: crate::Python<'_>) { let _foo = crate::intern!(py, "foo"); let _bar = crate::intern!(py, stringify!(bar)); } -#[allow(dead_code)] #[cfg(not(PyPy))] fn append_to_inittab() { #[crate::pymodule] #[pyo3(crate = "crate")] - #[allow(clippy::unnecessary_wraps)] - fn module_for_inittab( - _: crate::Python<'_>, - _: &crate::Bound<'_, crate::types::PyModule>, - ) -> crate::PyResult<()> { - ::std::result::Result::Ok(()) - } + mod module_for_inittab {} + crate::append_to_inittab!(module_for_inittab); } diff --git a/src/tests/hygiene/mod.rs b/src/tests/hygiene/mod.rs index f612c2d7162..c950e18da94 100644 --- a/src/tests/hygiene/mod.rs +++ b/src/tests/hygiene/mod.rs @@ -1,3 +1,6 @@ +#![no_implicit_prelude] +#![allow(dead_code, unused_variables, clippy::unnecessary_wraps)] + // The modules in this test are used to check PyO3 macro expansion is hygienic. By locating the test // inside the crate the global `::pyo3` namespace is not available, so in combination with // #[pyo3(crate = "crate")] this validates that all macro expansion respects the setting. diff --git a/src/tests/hygiene/pyclass.rs b/src/tests/hygiene/pyclass.rs index 8654e538728..3c078f580d5 100644 --- a/src/tests/hygiene/pyclass.rs +++ b/src/tests/hygiene/pyclass.rs @@ -1,6 +1,3 @@ -#![no_implicit_prelude] -#![allow(unused_variables)] - #[crate::pyclass] #[pyo3(crate = "crate")] #[derive(::std::clone::Clone)] diff --git a/src/tests/hygiene/pyfunction.rs b/src/tests/hygiene/pyfunction.rs index c1bca213933..8dcdc369c47 100644 --- a/src/tests/hygiene/pyfunction.rs +++ b/src/tests/hygiene/pyfunction.rs @@ -1,6 +1,3 @@ -#![no_implicit_prelude] -#![allow(unused_variables, clippy::unnecessary_wraps)] - #[crate::pyfunction] #[pyo3(crate = "crate")] fn do_something(x: i32) -> crate::PyResult { @@ -8,19 +5,9 @@ fn do_something(x: i32) -> crate::PyResult { } #[test] -#[cfg(feature = "gil-refs")] fn invoke_wrap_pyfunction() { crate::Python::with_gil(|py| { - #[allow(deprecated)] - let func = crate::wrap_pyfunction!(do_something)(py).unwrap(); - crate::py_run!(py, func, r#"func(5)"#); - }); -} - -#[test] -fn invoke_wrap_pyfunction_bound() { - crate::Python::with_gil(|py| { - let func = crate::wrap_pyfunction_bound!(do_something, py).unwrap(); + let func = crate::wrap_pyfunction!(do_something, py).unwrap(); crate::py_run!(py, func, r#"func(5)"#); }); } diff --git a/src/tests/hygiene/pymethods.rs b/src/tests/hygiene/pymethods.rs index 95d670c63a6..b6d090d9aa8 100644 --- a/src/tests/hygiene/pymethods.rs +++ b/src/tests/hygiene/pymethods.rs @@ -1,6 +1,3 @@ -#![no_implicit_prelude] -#![allow(unused_variables, clippy::unnecessary_wraps)] - #[crate::pyclass] #[pyo3(crate = "crate")] pub struct Dummy; diff --git a/src/tests/hygiene/pymodule.rs b/src/tests/hygiene/pymodule.rs index 91f9808bcc4..5a45ab189ee 100644 --- a/src/tests/hygiene/pymodule.rs +++ b/src/tests/hygiene/pymodule.rs @@ -1,51 +1,36 @@ -#![no_implicit_prelude] -#![allow(unused_variables, clippy::unnecessary_wraps)] - #[crate::pyfunction] #[pyo3(crate = "crate")] fn do_something(x: i32) -> crate::PyResult { ::std::result::Result::Ok(x) } -#[cfg(feature = "gil-refs")] -#[allow(deprecated)] -#[crate::pymodule] -#[pyo3(crate = "crate")] -fn foo(_py: crate::Python<'_>, _m: &crate::types::PyModule) -> crate::PyResult<()> { - ::std::result::Result::Ok(()) -} - #[crate::pymodule] #[pyo3(crate = "crate")] -fn foo_bound( +fn foo( _py: crate::Python<'_>, _m: &crate::Bound<'_, crate::types::PyModule>, ) -> crate::PyResult<()> { ::std::result::Result::Ok(()) } -#[cfg(feature = "gil-refs")] -#[allow(deprecated)] #[crate::pymodule] #[pyo3(crate = "crate")] -fn my_module(_py: crate::Python<'_>, m: &crate::types::PyModule) -> crate::PyResult<()> { - m.add_function(crate::wrap_pyfunction!(do_something, m)?)?; - m.add_wrapped(crate::wrap_pymodule!(foo))?; - - ::std::result::Result::Ok(()) -} - -#[crate::pymodule] -#[pyo3(crate = "crate")] -fn my_module_bound(m: &crate::Bound<'_, crate::types::PyModule>) -> crate::PyResult<()> { +fn my_module(m: &crate::Bound<'_, crate::types::PyModule>) -> crate::PyResult<()> { as crate::types::PyModuleMethods>::add_function( m, crate::wrap_pyfunction_bound!(do_something, m)?, )?; as crate::types::PyModuleMethods>::add_wrapped( m, - crate::wrap_pymodule!(foo_bound), + crate::wrap_pymodule!(foo), )?; ::std::result::Result::Ok(()) } + +#[crate::pymodule(submodule)] +#[pyo3(crate = "crate")] +mod my_module_declarative { + #[pymodule_export] + use super::{do_something, foo}; +} From 12792327019dd16b725823633e45e8e3463b268c Mon Sep 17 00:00:00 2001 From: Giovanni Barillari Date: Wed, 10 Jul 2024 11:16:23 +0200 Subject: [PATCH 150/495] Add Granian to example projects (#4329) --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index d5be31967a7..fd4acdd4154 100644 --- a/README.md +++ b/README.md @@ -193,6 +193,7 @@ about this topic. - [fastuuid](https://github.com/thedrow/fastuuid/) _Python bindings to Rust's UUID library._ - [feos](https://github.com/feos-org/feos) _Lightning fast thermodynamic modeling in Rust with fully developed Python interface._ - [forust](https://github.com/jinlow/forust) _A lightweight gradient boosted decision tree library written in Rust._ +- [granian](https://github.com/emmett-framework/granian) _A Rust HTTP server for Python applications._ - [greptimedb](https://github.com/GreptimeTeam/greptimedb/tree/main/src/script) _Support [Python scripting](https://docs.greptime.com/user-guide/python-scripts/overview) in the database_ - [haem](https://github.com/BooleanCat/haem) _A Python library for working on Bioinformatics problems._ - [html-py-ever](https://github.com/PyO3/setuptools-rust/tree/main/examples/html-py-ever) _Using [html5ever](https://github.com/servo/html5ever) through [kuchiki](https://github.com/kuchiki-rs/kuchiki) to speed up html parsing and css-selecting._ From c7c1dff7104f790ff54dfb9cf5667e1e92a1cf21 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Wed, 10 Jul 2024 12:36:31 +0100 Subject: [PATCH 151/495] ci: check minimal-versions on MSRV feature powerset (#4273) * ci: check minimal-versions on MSRV feature powerset * fix msrv arg * bump num-bigint floor to 0.4.2 * try fix build syntax --- .github/workflows/build.yml | 9 ++++--- .github/workflows/ci.yml | 52 +++++++++++++++++++++++++++---------- Cargo.toml | 8 +++--- noxfile.py | 10 ++++--- 4 files changed, 56 insertions(+), 23 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e952592da3a..077ca08cf4e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -16,6 +16,9 @@ on: rust-target: required: true type: string + MSRV: + required: true + type: string jobs: build: @@ -51,9 +54,9 @@ jobs: name: Prepare LD_LIBRARY_PATH (Ubuntu only) run: echo LD_LIBRARY_PATH=${pythonLocation}/lib >> $GITHUB_ENV - - if: inputs.rust == '1.63.0' - name: Prepare minimal package versions (MSRV only) - run: nox -s set-minimal-package-versions + - if: inputs.rust == inputs.MSRV + name: Prepare MSRV package versions + run: nox -s set-msrv-package-versions - if: inputs.rust != 'stable' name: Ignore changed error messages when using trybuild diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8379232b7fb..c8f4b5dcc67 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,6 +31,18 @@ jobs: - name: Check rust formatting (rustfmt) run: nox -s rustfmt + resolve: + runs-on: ubuntu-latest + outputs: + MSRV: ${{ steps.resolve-msrv.outputs.MSRV }} + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + - name: resolve MSRV + id: resolve-msrv + run: + echo MSRV=`python -c 'import tomllib; print(tomllib.load(open("Cargo.toml", "rb"))["package"]["rust-version"])'` >> $GITHUB_OUTPUT + semver-checks: if: github.ref != 'refs/heads/main' needs: [fmt] @@ -41,13 +53,13 @@ jobs: - uses: obi1kenobi/cargo-semver-checks-action@v2 check-msrv: - needs: [fmt] + needs: [fmt, resolve] runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@master with: - toolchain: 1.63.0 + toolchain: ${{ needs.resolve.outputs.MSRV }} targets: x86_64-unknown-linux-gnu components: rust-src - uses: actions/setup-python@v5 @@ -57,9 +69,11 @@ jobs: with: save-if: ${{ github.event_name != 'merge_group' }} - run: python -m pip install --upgrade pip && pip install nox - - name: Prepare minimal package versions - run: nox -s set-minimal-package-versions - - run: nox -s check-all + # This is a smoke test to confirm that CI will run on MSRV (including dev dependencies) + - name: Check with MSRV package versions + run: | + nox -s set-msrv-package-versions + nox -s check-all env: CARGO_BUILD_TARGET: x86_64-unknown-linux-gnu @@ -141,7 +155,7 @@ jobs: build-pr: if: ${{ !contains(github.event.pull_request.labels.*.name, 'CI-build-full') && github.event_name == 'pull_request' }} name: python${{ matrix.python-version }}-${{ matrix.platform.python-architecture }} ${{ matrix.platform.os }} rust-${{ matrix.rust }} - needs: [fmt] + needs: [fmt, resolve] uses: ./.github/workflows/build.yml with: os: ${{ matrix.platform.os }} @@ -149,6 +163,7 @@ jobs: python-architecture: ${{ matrix.platform.python-architecture }} rust: ${{ matrix.rust }} rust-target: ${{ matrix.platform.rust-target }} + MSRV: ${{ needs.resolve.outputs.MSRV }} secrets: inherit strategy: # If one platform fails, allow the rest to keep testing if `CI-no-fail-fast` label is present @@ -198,7 +213,7 @@ jobs: build-full: if: ${{ contains(github.event.pull_request.labels.*.name, 'CI-build-full') || github.event_name != 'pull_request' }} name: python${{ matrix.python-version }}-${{ matrix.platform.python-architecture }} ${{ matrix.platform.os }} rust-${{ matrix.rust }} - needs: [fmt] + needs: [fmt, resolve] uses: ./.github/workflows/build.yml with: os: ${{ matrix.platform.os }} @@ -206,6 +221,7 @@ jobs: python-architecture: ${{ matrix.platform.python-architecture }} rust: ${{ matrix.rust }} rust-target: ${{ matrix.platform.rust-target }} + MSRV: ${{ needs.resolve.outputs.MSRV }} secrets: inherit strategy: # If one platform fails, allow the rest to keep testing if `CI-no-fail-fast` label is present @@ -251,7 +267,7 @@ jobs: ] include: # Test minimal supported Rust version - - rust: 1.63.0 + - rust: ${{ needs.resolve.outputs.MSRV }} python-version: "3.12" platform: { @@ -487,21 +503,31 @@ jobs: - run: python3 -m nox -s test-version-limits check-feature-powerset: - needs: [fmt] + needs: [fmt, resolve] if: ${{ contains(github.event.pull_request.labels.*.name, 'CI-build-full') || github.event_name != 'pull_request' }} runs-on: ubuntu-latest + name: check-feature-powerset ${{ matrix.rust }} + strategy: + # run on stable and MSRV to check that all combinations of features are expected to build fine on our supported + # range of compilers + matrix: + rust: ["stable"] + include: + - rust: ${{ needs.resolve.outputs.MSRV }} steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 - uses: Swatinem/rust-cache@v2 with: save-if: ${{ github.event_name != 'merge_group' }} - - uses: dtolnay/rust-toolchain@stable + - uses: dtolnay/rust-toolchain@master with: - components: rust-src - - uses: taiki-e/install-action@cargo-hack + toolchain: stable + - uses: taiki-e/install-action@v2 + with: + tool: cargo-hack,cargo-minimal-versions - run: python3 -m pip install --upgrade pip && pip install nox - - run: python3 -m nox -s check-feature-powerset + - run: python3 -m nox -s check-feature-powerset -- ${{ matrix.rust != 'stable' && 'minimal-versions' || '' }} test-cross-compilation: needs: [fmt] diff --git a/Cargo.toml b/Cargo.toml index 9806fdb3e73..38673913049 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,7 @@ rust-version = "1.63" cfg-if = "1.0" libc = "0.2.62" memoffset = "0.9" -once_cell = "1.13.0" +once_cell = "1.13" # ffi bindings to the python interpreter, split into a separate crate so they can be used independently pyo3-ffi = { path = "pyo3-ffi", version = "=0.23.0-dev" } @@ -32,17 +32,17 @@ unindent = { version = "0.2.1", optional = true } inventory = { version = "0.3.0", optional = true } # crate integrations that can be added using the eponymous features -anyhow = { version = "1.0", optional = true } +anyhow = { version = "1.0.1", optional = true } chrono = { version = "0.4.25", default-features = false, optional = true } chrono-tz = { version = ">= 0.6, < 0.10", default-features = false, optional = true } either = { version = "1.9", optional = true } eyre = { version = ">= 0.4, < 0.7", optional = true } hashbrown = { version = ">= 0.9, < 0.15", optional = true } indexmap = { version = ">= 1.6, < 3", optional = true } -num-bigint = { version = "0.4", optional = true } +num-bigint = { version = "0.4.2", optional = true } num-complex = { version = ">= 0.2, < 0.5", optional = true } num-rational = {version = "0.4.1", optional = true } -rust_decimal = { version = "1.0.0", default-features = false, optional = true } +rust_decimal = { version = "1.15", default-features = false, optional = true } serde = { version = "1.0", optional = true } smallvec = { version = "1.0", optional = true } diff --git a/noxfile.py b/noxfile.py index 96bd587bee8..6b2411d5f17 100644 --- a/noxfile.py +++ b/noxfile.py @@ -543,8 +543,8 @@ def check_changelog(session: nox.Session): print(fragment.name) -@nox.session(name="set-minimal-package-versions", venv_backend="none") -def set_minimal_package_versions(session: nox.Session): +@nox.session(name="set-msrv-package-versions", venv_backend="none") +def set_msrv_package_versions(session: nox.Session): from collections import defaultdict if toml is None: @@ -708,10 +708,14 @@ def check_feature_powerset(session: nox.Session): rust_flags = env.get("RUSTFLAGS", "") env["RUSTFLAGS"] = f"{rust_flags} -Dwarnings" + subcommand = "hack" + if "minimal-versions" in session.posargs: + subcommand = "minimal-versions" + comma_join = ",".join _run_cargo( session, - "hack", + subcommand, "--feature-powerset", '--optional-deps=""', f'--skip="{comma_join(features_to_skip)}"', From 32b6a1aef1c9625d4dec9a8850562397df483959 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Wed, 10 Jul 2024 13:30:01 +0100 Subject: [PATCH 152/495] docs: use versioned links from docs to guide (#4331) * use versioned links from docs to guide * use standard python version in `guide-build` ci job --- .github/workflows/gh-pages.yml | 2 +- guide/src/building-and-distribution.md | 2 +- noxfile.py | 12 ++++++++++-- pyo3-build-config/src/lib.rs | 4 +++- pyo3-ffi/src/lib.rs | 5 ++--- pyo3-macros-backend/src/pyclass.rs | 14 ++++++++------ pyo3-macros/src/lib.rs | 24 ++++++++++++------------ src/instance.rs | 3 ++- src/lib.rs | 10 +++++----- src/sync.rs | 4 +++- src/types/module.rs | 4 ++-- tests/ui/reject_generics.stderr | 4 ++-- 12 files changed, 51 insertions(+), 37 deletions(-) diff --git a/.github/workflows/gh-pages.yml b/.github/workflows/gh-pages.yml index 3237440a99a..7c10a075db4 100644 --- a/.github/workflows/gh-pages.yml +++ b/.github/workflows/gh-pages.yml @@ -22,7 +22,7 @@ jobs: tag_name: ${{ steps.prepare_tag.outputs.tag_name }} steps: - uses: actions/checkout@v4 - + - uses: actions/setup-python@v5 - uses: dtolnay/rust-toolchain@nightly - name: Setup mdBook diff --git a/guide/src/building-and-distribution.md b/guide/src/building-and-distribution.md index 33280a5a180..c137a1a3995 100644 --- a/guide/src/building-and-distribution.md +++ b/guide/src/building-and-distribution.md @@ -163,7 +163,7 @@ fn main() { For more discussion on and workarounds for MacOS linking problems [see this issue](https://github.com/PyO3/pyo3/issues/1800#issuecomment-906786649). -Finally, don't forget that on MacOS the `extension-module` feature will cause `cargo test` to fail without the `--no-default-features` flag (see [the FAQ](https://pyo3.rs/main/faq.html#i-cant-run-cargo-test-im-having-linker-issues-like-symbol-not-found-or-undefined-reference-to-_pyexc_systemerror)). +Finally, don't forget that on MacOS the `extension-module` feature will cause `cargo test` to fail without the `--no-default-features` flag (see [the FAQ](https://pyo3.rs/main/faq.html#i-cant-run-cargo-test-or-i-cant-build-in-a-cargo-workspace-im-having-linker-issues-like-symbol-not-found-or-undefined-reference-to-_pyexc_systemerror)). ### The `extension-module` feature diff --git a/noxfile.py b/noxfile.py index 6b2411d5f17..4757a282e84 100644 --- a/noxfile.py +++ b/noxfile.py @@ -394,8 +394,17 @@ def check_guide(session: nox.Session): docs(session) session.posargs.extend(posargs) + if toml is None: + session.error("requires Python 3.11 or `toml` to be installed") + pyo3_version = toml.loads((PYO3_DIR / "Cargo.toml").read_text())["package"][ + "version" + ] + remaps = { f"file://{PYO3_GUIDE_SRC}/([^/]*/)*?%7B%7B#PYO3_DOCS_URL}}}}": f"file://{PYO3_DOCS_TARGET}", + f"https://pyo3.rs/v{pyo3_version}": f"file://{PYO3_GUIDE_TARGET}", + "https://pyo3.rs/main/": f"file://{PYO3_GUIDE_TARGET}/", + "https://pyo3.rs/latest/": f"file://{PYO3_GUIDE_TARGET}/", "%7B%7B#PYO3_DOCS_VERSION}}": "latest", } remap_args = [] @@ -416,8 +425,7 @@ def check_guide(session: nox.Session): session, "lychee", str(PYO3_DOCS_TARGET), - f"--remap=https://pyo3.rs/main/ file://{PYO3_GUIDE_TARGET}/", - f"--remap=https://pyo3.rs/latest/ file://{PYO3_GUIDE_TARGET}/", + *remap_args, f"--exclude=file://{PYO3_DOCS_TARGET}", "--exclude=http://www.adobe.com/", *session.posargs, diff --git a/pyo3-build-config/src/lib.rs b/pyo3-build-config/src/lib.rs index a2d4298c524..2da3e56d3b6 100644 --- a/pyo3-build-config/src/lib.rs +++ b/pyo3-build-config/src/lib.rs @@ -39,7 +39,9 @@ use target_lexicon::OperatingSystem; /// | `#[cfg(PyPy)]` | This marks code which is run when compiling for PyPy. | /// | `#[cfg(GraalPy)]` | This marks code which is run when compiling for GraalPy. | /// -/// For examples of how to use these attributes, [see PyO3's guide](https://pyo3.rs/latest/building-and-distribution/multiple_python_versions.html). +/// For examples of how to use these attributes, +#[doc = concat!("[see PyO3's guide](https://pyo3.rs/v", env!("CARGO_PKG_VERSION"), "/building-and-distribution/multiple_python_versions.html)")] +/// . #[cfg(feature = "resolve-config")] pub fn use_pyo3_cfgs() { print_expected_cfgs(); diff --git a/pyo3-ffi/src/lib.rs b/pyo3-ffi/src/lib.rs index 3f6d6732bf3..ff4d03d3a44 100644 --- a/pyo3-ffi/src/lib.rs +++ b/pyo3-ffi/src/lib.rs @@ -221,11 +221,10 @@ //! [`maturin`]: https://github.com/PyO3/maturin "Build and publish crates with pyo3, rust-cpython and cffi bindings as well as rust binaries as python packages" //! [`pyo3-build-config`]: https://docs.rs/pyo3-build-config //! [feature flags]: https://doc.rust-lang.org/cargo/reference/features.html "Features - The Cargo Book" -//! [manual_builds]: https://pyo3.rs/latest/building-and-distribution.html#manual-builds "Manual builds - Building and Distribution - PyO3 user guide" +#![doc = concat!("[manual_builds]: https://pyo3.rs/v", env!("CARGO_PKG_VERSION"), "/building-and-distribution.html#manual-builds \"Manual builds - Building and Distribution - PyO3 user guide\"")] //! [setuptools-rust]: https://github.com/PyO3/setuptools-rust "Setuptools plugin for Rust extensions" //! [PEP 384]: https://www.python.org/dev/peps/pep-0384 "PEP 384 -- Defining a Stable ABI" -//! [Features chapter of the guide]: https://pyo3.rs/latest/features.html#features-reference "Features Reference - PyO3 user guide" - +#![doc = concat!("[Features chapter of the guide]: https://pyo3.rs/v", env!("CARGO_PKG_VERSION"), "/features.html#features-reference \"Features Reference - PyO3 user guide\"")] #![allow( missing_docs, non_camel_case_types, diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index dab0102bbce..078a7b7f4af 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -231,17 +231,19 @@ pub fn build_py_class( if let Some(lt) = class.generics.lifetimes().next() { bail_spanned!( - lt.span() => - "#[pyclass] cannot have lifetime parameters. \ - For an explanation, see https://pyo3.rs/latest/class.html#no-lifetime-parameters" + lt.span() => concat!( + "#[pyclass] cannot have lifetime parameters. For an explanation, see \ + https://pyo3.rs/v", env!("CARGO_PKG_VERSION"), "/class.html#no-lifetime-parameters" + ) ); } ensure_spanned!( class.generics.params.is_empty(), - class.generics.span() => - "#[pyclass] cannot have generic parameters. \ - For an explanation, see https://pyo3.rs/latest/class.html#no-generic-parameters" + class.generics.span() => concat!( + "#[pyclass] cannot have generic parameters. For an explanation, see \ + https://pyo3.rs/v", env!("CARGO_PKG_VERSION"), "/class.html#no-generic-parameters" + ) ); let mut field_options: Vec<(&syn::Field, FieldPyO3Options)> = match &mut class.fields { diff --git a/pyo3-macros/src/lib.rs b/pyo3-macros/src/lib.rs index 95e983079f1..d9e22a94ede 100644 --- a/pyo3-macros/src/lib.rs +++ b/pyo3-macros/src/lib.rs @@ -32,7 +32,7 @@ use syn::{parse::Nothing, parse_macro_input, Item}; /// `#[pymodule]` implementation generates a hidden module with the same name containing /// metadata about the module, which is used by `wrap_pymodule!`). /// -/// [1]: https://pyo3.rs/latest/module.html +#[doc = concat!("[1]: https://pyo3.rs/v", env!("CARGO_PKG_VERSION"), "/module.html")] #[proc_macro_attribute] pub fn pymodule(args: TokenStream, input: TokenStream) -> TokenStream { match parse_macro_input!(input as Item) { @@ -99,17 +99,17 @@ pub fn pyclass(attr: TokenStream, input: TokenStream) -> TokenStream { /// multiple `#[pymethods]` blocks for a single `#[pyclass]`. /// This will add a transitive dependency on the [`inventory`][3] crate. /// -/// [1]: https://pyo3.rs/latest/class.html#instance-methods -/// [2]: https://pyo3.rs/latest/features.html#multiple-pymethods +#[doc = concat!("[1]: https://pyo3.rs/v", env!("CARGO_PKG_VERSION"), "/class.html#instance-methods")] +#[doc = concat!("[2]: https://pyo3.rs/v", env!("CARGO_PKG_VERSION"), "/features.html#multiple-pymethods")] /// [3]: https://docs.rs/inventory/ -/// [4]: https://pyo3.rs/latest/class.html#constructor -/// [5]: https://pyo3.rs/latest/class.html#object-properties-using-getter-and-setter -/// [6]: https://pyo3.rs/latest/class.html#static-methods -/// [7]: https://pyo3.rs/latest/class.html#class-methods -/// [8]: https://pyo3.rs/latest/class.html#callable-objects -/// [9]: https://pyo3.rs/latest/class.html#class-attributes -/// [10]: https://pyo3.rs/latest/class.html#method-arguments -/// [11]: https://pyo3.rs/latest/class.html#object-properties-using-pyo3get-set +#[doc = concat!("[4]: https://pyo3.rs/v", env!("CARGO_PKG_VERSION"), "/class.html#constructor")] +#[doc = concat!("[5]: https://pyo3.rs/v", env!("CARGO_PKG_VERSION"), "/class.html#object-properties-using-getter-and-setter")] +#[doc = concat!("[6]: https://pyo3.rs/v", env!("CARGO_PKG_VERSION"), "/class.html#static-methods")] +#[doc = concat!("[7]: https://pyo3.rs/v", env!("CARGO_PKG_VERSION"), "/class.html#class-methods")] +#[doc = concat!("[8]: https://pyo3.rs/v", env!("CARGO_PKG_VERSION"), "/class.html#callable-objects")] +#[doc = concat!("[9]: https://pyo3.rs/v", env!("CARGO_PKG_VERSION"), "/class.html#class-attributes")] +#[doc = concat!("[10]: https://pyo3.rs/v", env!("CARGO_PKG_VERSION"), "/class.html#method-arguments")] +#[doc = concat!("[11]: https://pyo3.rs/v", env!("CARGO_PKG_VERSION"), "/class.html#object-properties-using-pyo3get-set")] #[proc_macro_attribute] pub fn pymethods(attr: TokenStream, input: TokenStream) -> TokenStream { let methods_type = if cfg!(feature = "multiple-pymethods") { @@ -138,7 +138,7 @@ pub fn pymethods(attr: TokenStream, input: TokenStream) -> TokenStream { /// `#[pyfunction]` implementation generates a hidden module with the same name containing /// metadata about the function, which is used by `wrap_pyfunction!`). /// -/// [1]: https://pyo3.rs/latest/function.html +#[doc = concat!("[1]: https://pyo3.rs/v", env!("CARGO_PKG_VERSION"), "/function.html")] #[proc_macro_attribute] pub fn pyfunction(attr: TokenStream, input: TokenStream) -> TokenStream { let mut ast = parse_macro_input!(input as syn::ItemFn); diff --git a/src/instance.rs b/src/instance.rs index 697df2c7bae..fad0c55bc3d 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -744,7 +744,8 @@ impl IntoPy for Borrowed<'_, '_, T> { /// - [`Py::borrow`], [`Py::try_borrow`], [`Py::borrow_mut`], or [`Py::try_borrow_mut`], /// /// to get a (mutable) reference to a contained pyclass, using a scheme similar to std's [`RefCell`]. -/// See the [guide entry](https://pyo3.rs/latest/class.html#bound-and-interior-mutability) +/// See the +#[doc = concat!("[guide entry](https://pyo3.rs/v", env!("CARGO_PKG_VERSION"), "/class.html#bound-and-interior-mutability)")] /// for more information. /// - You can call methods directly on `Py` with [`Py::call_bound`], [`Py::call_method_bound`] and friends. /// diff --git a/src/lib.rs b/src/lib.rs index 94dc168590c..0ff48dbb460 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -295,25 +295,25 @@ //! [`rust_decimal`]: ./rust_decimal/index.html "Documenation about the `rust_decimal` feature." //! [`Decimal`]: https://docs.rs/rust_decimal/latest/rust_decimal/struct.Decimal.html //! [`serde`]: <./serde/index.html> "Documentation about the `serde` feature." -//! [calling_rust]: https://pyo3.rs/latest/python-from-rust.html "Calling Python from Rust - PyO3 user guide" +#![doc = concat!("[calling_rust]: https://pyo3.rs/v", env!("CARGO_PKG_VERSION"), "/python-from-rust.html \"Calling Python from Rust - PyO3 user guide\"")] //! [examples subdirectory]: https://github.com/PyO3/pyo3/tree/main/examples //! [feature flags]: https://doc.rust-lang.org/cargo/reference/features.html "Features - The Cargo Book" //! [global interpreter lock]: https://docs.python.org/3/glossary.html#term-global-interpreter-lock //! [hashbrown]: https://docs.rs/hashbrown //! [smallvec]: https://docs.rs/smallvec //! [indexmap]: https://docs.rs/indexmap -//! [manual_builds]: https://pyo3.rs/latest/building-and-distribution.html#manual-builds "Manual builds - Building and Distribution - PyO3 user guide" +#![doc = concat!("[manual_builds]: https://pyo3.rs/v", env!("CARGO_PKG_VERSION"), "/building-and-distribution.html#manual-builds \"Manual builds - Building and Distribution - PyO3 user guide\"")] //! [num-bigint]: https://docs.rs/num-bigint //! [num-complex]: https://docs.rs/num-complex //! [num-rational]: https://docs.rs/num-rational //! [serde]: https://docs.rs/serde //! [setuptools-rust]: https://github.com/PyO3/setuptools-rust "Setuptools plugin for Rust extensions" //! [the guide]: https://pyo3.rs "PyO3 user guide" -//! [types]: https://pyo3.rs/latest/types.html "GIL lifetimes, mutability and Python object types" +#![doc = concat!("[types]: https://pyo3.rs/v", env!("CARGO_PKG_VERSION"), "/types.html \"GIL lifetimes, mutability and Python object types\"")] //! [PEP 384]: https://www.python.org/dev/peps/pep-0384 "PEP 384 -- Defining a Stable ABI" //! [Python from Rust]: https://github.com/PyO3/pyo3#using-python-from-rust //! [Rust from Python]: https://github.com/PyO3/pyo3#using-rust-from-python -//! [Features chapter of the guide]: https://pyo3.rs/latest/features.html#features-reference "Features Reference - PyO3 user guide" +#![doc = concat!("[Features chapter of the guide]: https://pyo3.rs/v", env!("CARGO_PKG_VERSION"), "/features.html#features-reference \"Features Reference - PyO3 user guide\"")] //! [`Ungil`]: crate::marker::Ungil pub use crate::class::*; pub use crate::conversion::{AsPyPointer, FromPyObject, IntoPy, ToPyObject}; @@ -483,7 +483,7 @@ pub use pyo3_macros::{pyfunction, pymethods, pymodule, FromPyObject}; /// For more on creating Python classes, /// see the [class section of the guide][1]. /// -/// [1]: https://pyo3.rs/latest/class.html +#[doc = concat!("[1]: https://pyo3.rs/v", env!("CARGO_PKG_VERSION"), "/class.html")] #[cfg(feature = "macros")] pub use pyo3_macros::pyclass; diff --git a/src/sync.rs b/src/sync.rs index a8265eabdbd..390011fdd5b 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -59,7 +59,9 @@ unsafe impl Sync for GILProtected where T: Send {} /// Unlike `once_cell::sync` which blocks threads to achieve thread safety, this implementation /// uses the Python GIL to mediate concurrent access. This helps in cases where `once_cell` or /// `lazy_static`'s synchronization strategy can lead to deadlocks when interacting with the Python -/// GIL. For an example, see [the FAQ section](https://pyo3.rs/latest/faq.html) of the guide. +/// GIL. For an example, see +#[doc = concat!("[the FAQ section](https://pyo3.rs/v", env!("CARGO_PKG_VERSION"), "/faq.html)")] +/// of the guide. /// /// Note that: /// 1) `get_or_init` and `get_or_try_init` do not protect against infinite recursion diff --git a/src/types/module.rs b/src/types/module.rs index 5438c22f681..c805c09a239 100644 --- a/src/types/module.rs +++ b/src/types/module.rs @@ -304,7 +304,7 @@ impl PyModule { /// make an *instance* of `Foo` (or *get* one for that matter, as we haven't exported /// anything that can return instances of `Foo`). /// - /// [1]: https://pyo3.rs/latest/class.html#constructor + #[doc = concat!("[1]: https://pyo3.rs/v", env!("CARGO_PKG_VERSION"), "/class.html#constructor")] pub fn add_class(&self) -> PyResult<()> where T: PyClass, @@ -509,7 +509,7 @@ pub trait PyModuleMethods<'py>: crate::sealed::Sealed { /// make an *instance* of `Foo` (or *get* one for that matter, as we haven't exported /// anything that can return instances of `Foo`). /// - /// [1]: https://pyo3.rs/latest/class.html#constructor + #[doc = concat!("[1]: https://pyo3.rs/v", env!("CARGO_PKG_VERSION"), "/class.html#constructor")] fn add_class(&self) -> PyResult<()> where T: PyClass; diff --git a/tests/ui/reject_generics.stderr b/tests/ui/reject_generics.stderr index 2285b9271fa..78c3d00e624 100644 --- a/tests/ui/reject_generics.stderr +++ b/tests/ui/reject_generics.stderr @@ -1,10 +1,10 @@ -error: #[pyclass] cannot have generic parameters. For an explanation, see https://pyo3.rs/latest/class.html#no-generic-parameters +error: #[pyclass] cannot have generic parameters. For an explanation, see https://pyo3.rs/v0.23.0-dev/class.html#no-generic-parameters --> tests/ui/reject_generics.rs:4:25 | 4 | struct ClassWithGenerics { | ^ -error: #[pyclass] cannot have lifetime parameters. For an explanation, see https://pyo3.rs/latest/class.html#no-lifetime-parameters +error: #[pyclass] cannot have lifetime parameters. For an explanation, see https://pyo3.rs/v0.23.0-dev/class.html#no-lifetime-parameters --> tests/ui/reject_generics.rs:9:27 | 9 | struct ClassWithLifetimes<'a> { From 3c65132da57f1282bdc38b868c2919ea97394fc5 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Wed, 10 Jul 2024 14:38:30 +0100 Subject: [PATCH 153/495] remove all functionality deprecated in PyO3 0.21 (#4323) * remove all functionality deprecated in PyO3 0.21 * further adjustments after removing deprecated APIs --- guide/src/migration.md | 3 +- newsfragments/4323.removed.md | 1 + src/conversion.rs | 168 +--------------------------------- src/instance.rs | 21 ----- src/lib.rs | 27 ------ src/marker.rs | 40 +------- src/prelude.rs | 3 - src/pyclass.rs | 110 +--------------------- src/types/any.rs | 8 -- src/types/iterator.rs | 39 +------- src/types/mapping.rs | 42 +-------- src/types/sequence.rs | 41 +-------- 12 files changed, 9 insertions(+), 494 deletions(-) create mode 100644 newsfragments/4323.removed.md diff --git a/guide/src/migration.md b/guide/src/migration.md index 8ac3ff16c47..626458b46d1 100644 --- a/guide/src/migration.md +++ b/guide/src/migration.md @@ -235,8 +235,7 @@ The `__next__` and `__anext__` magic methods can now return any type convertible Starting with an implementation of a Python iterator using `IterNextOutput`, e.g. -```rust -#![allow(deprecated)] +```rust,ignore use pyo3::prelude::*; use pyo3::iter::IterNextOutput; diff --git a/newsfragments/4323.removed.md b/newsfragments/4323.removed.md new file mode 100644 index 00000000000..c9d46f6a886 --- /dev/null +++ b/newsfragments/4323.removed.md @@ -0,0 +1 @@ +Remove all functionality deprecated in PyO3 0.21. diff --git a/src/conversion.rs b/src/conversion.rs index 6a089e186bc..451d0df4abf 100644 --- a/src/conversion.rs +++ b/src/conversion.rs @@ -8,10 +8,7 @@ use crate::types::PyTuple; use crate::{ffi, Borrowed, Bound, Py, PyAny, PyClass, PyObject, PyRef, PyRefMut, Python}; #[cfg(feature = "gil-refs")] use { - crate::{ - err::{self, PyDowncastError}, - gil, PyNativeType, - }, + crate::{err, gil, PyNativeType}, std::ptr::NonNull, }; @@ -395,121 +392,6 @@ where } } -/// Trait implemented by Python object types that allow a checked downcast. -/// If `T` implements `PyTryFrom`, we can convert `&PyAny` to `&T`. -/// -/// This trait is similar to `std::convert::TryFrom` -#[cfg(feature = "gil-refs")] -#[deprecated(since = "0.21.0")] -pub trait PyTryFrom<'v>: Sized + PyNativeType { - /// Cast from a concrete Python object type to PyObject. - #[deprecated( - since = "0.21.0", - note = "use `value.downcast::()` instead of `T::try_from(value)`" - )] - fn try_from>(value: V) -> Result<&'v Self, PyDowncastError<'v>>; - - /// Cast from a concrete Python object type to PyObject. With exact type check. - #[deprecated( - since = "0.21.0", - note = "use `value.downcast_exact::()` instead of `T::try_from_exact(value)`" - )] - fn try_from_exact>(value: V) -> Result<&'v Self, PyDowncastError<'v>>; - - /// Cast a PyAny to a specific type of PyObject. The caller must - /// have already verified the reference is for this type. - /// - /// # Safety - /// - /// Callers must ensure that the type is valid or risk type confusion. - #[deprecated( - since = "0.21.0", - note = "use `value.downcast_unchecked::()` instead of `T::try_from_unchecked(value)`" - )] - unsafe fn try_from_unchecked>(value: V) -> &'v Self; -} - -/// Trait implemented by Python object types that allow a checked downcast. -/// This trait is similar to `std::convert::TryInto` -#[cfg(feature = "gil-refs")] -#[deprecated(since = "0.21.0")] -pub trait PyTryInto: Sized { - /// Cast from PyObject to a concrete Python object type. - #[deprecated( - since = "0.21.0", - note = "use `value.downcast()` instead of `value.try_into()`" - )] - fn try_into(&self) -> Result<&T, PyDowncastError<'_>>; - - /// Cast from PyObject to a concrete Python object type. With exact type check. - #[deprecated( - since = "0.21.0", - note = "use `value.downcast()` instead of `value.try_into_exact()`" - )] - fn try_into_exact(&self) -> Result<&T, PyDowncastError<'_>>; -} - -#[cfg(feature = "gil-refs")] -#[allow(deprecated)] -mod implementations { - use super::*; - use crate::type_object::PyTypeInfo; - - // TryFrom implies TryInto - impl PyTryInto for PyAny - where - U: for<'v> PyTryFrom<'v>, - { - fn try_into(&self) -> Result<&U, PyDowncastError<'_>> { - >::try_from(self) - } - fn try_into_exact(&self) -> Result<&U, PyDowncastError<'_>> { - U::try_from_exact(self) - } - } - - impl<'v, T> PyTryFrom<'v> for T - where - T: PyTypeInfo + PyNativeType, - { - fn try_from>(value: V) -> Result<&'v Self, PyDowncastError<'v>> { - value.into().downcast() - } - - fn try_from_exact>(value: V) -> Result<&'v Self, PyDowncastError<'v>> { - value.into().downcast_exact() - } - - #[inline] - unsafe fn try_from_unchecked>(value: V) -> &'v Self { - value.into().downcast_unchecked() - } - } - - impl<'v, T> PyTryFrom<'v> for crate::PyCell - where - T: 'v + PyClass, - { - fn try_from>(value: V) -> Result<&'v Self, PyDowncastError<'v>> { - value.into().downcast() - } - fn try_from_exact>(value: V) -> Result<&'v Self, PyDowncastError<'v>> { - let value = value.into(); - unsafe { - if T::is_exact_type_of(value) { - Ok(Self::try_from_unchecked(value)) - } else { - Err(PyDowncastError::new(value, T::NAME)) - } - } - } - #[inline] - unsafe fn try_from_unchecked>(value: V) -> &'v Self { - value.into().downcast_unchecked() - } - } -} - /// Converts `()` to an empty Python tuple. impl IntoPy> for () { fn into_py(self, py: Python<'_>) -> Py { @@ -665,51 +547,3 @@ where /// }) /// ``` mod test_no_clone {} - -#[cfg(test)] -mod tests { - #[cfg(feature = "gil-refs")] - #[allow(deprecated)] - mod deprecated { - use super::super::PyTryFrom; - use crate::types::{IntoPyDict, PyAny, PyDict, PyList}; - use crate::{Python, ToPyObject}; - - #[test] - fn test_try_from() { - Python::with_gil(|py| { - let list: &PyAny = vec![3, 6, 5, 4, 7].to_object(py).into_ref(py); - let dict: &PyAny = vec![("reverse", true)].into_py_dict(py).as_ref(); - - assert!(>::try_from(list).is_ok()); - assert!(>::try_from(dict).is_ok()); - - assert!(>::try_from(list).is_ok()); - assert!(>::try_from(dict).is_ok()); - }); - } - - #[test] - fn test_try_from_exact() { - Python::with_gil(|py| { - let list: &PyAny = vec![3, 6, 5, 4, 7].to_object(py).into_ref(py); - let dict: &PyAny = vec![("reverse", true)].into_py_dict(py).as_ref(); - - assert!(PyList::try_from_exact(list).is_ok()); - assert!(PyDict::try_from_exact(dict).is_ok()); - - assert!(PyAny::try_from_exact(list).is_err()); - assert!(PyAny::try_from_exact(dict).is_err()); - }); - } - - #[test] - fn test_try_from_unchecked() { - Python::with_gil(|py| { - let list = PyList::new(py, [1, 2, 3]); - let val = unsafe { ::try_from_unchecked(list.as_ref()) }; - assert!(list.is(val)); - }); - } - } -} diff --git a/src/instance.rs b/src/instance.rs index fad0c55bc3d..d80072afa98 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -1385,14 +1385,6 @@ impl Py { unsafe { ffi::Py_None() == self.as_ptr() } } - /// Returns whether the object is considered to be true. - /// - /// This is equivalent to the Python expression `bool(self)`. - #[deprecated(since = "0.21.0", note = "use `.is_truthy()` instead")] - pub fn is_true(&self, py: Python<'_>) -> PyResult { - self.is_truthy(py) - } - /// Returns whether the object is considered to be true. /// /// This applies truth value testing equivalent to the Python expression `bool(self)`. @@ -2365,18 +2357,5 @@ a = A() } }) } - - #[test] - #[cfg(feature = "gil-refs")] - #[allow(deprecated)] - fn cell_tryfrom() { - use crate::{PyCell, PyTryInto}; - // More detailed tests of the underlying semantics in pycell.rs - Python::with_gil(|py| { - let instance: &PyAny = Py::new(py, SomeClass(0)).unwrap().into_ref(py); - let _: &PyCell = PyTryInto::try_into(instance).unwrap(); - let _: &PyCell = PyTryInto::try_into_exact(instance).unwrap(); - }) - } } } diff --git a/src/lib.rs b/src/lib.rs index 0ff48dbb460..5792a63e9b5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -318,9 +318,6 @@ pub use crate::class::*; pub use crate::conversion::{AsPyPointer, FromPyObject, IntoPy, ToPyObject}; #[cfg(feature = "gil-refs")] -#[allow(deprecated)] -pub use crate::conversion::{FromPyPointer, PyTryFrom, PyTryInto}; -#[cfg(feature = "gil-refs")] pub use crate::err::PyDowncastError; pub use crate::err::{DowncastError, DowncastIntoError, PyErr, PyErrArguments, PyResult, ToPyErr}; #[cfg(feature = "gil-refs")] @@ -382,30 +379,6 @@ pub mod class { pub use crate::pyclass::CompareOp; } - /// Old module which contained some implementation details of the `#[pyproto]` module. - /// - /// Prefer using the same content from `pyo3::pyclass`, e.g. `use pyo3::pyclass::IterANextOutput` instead - /// of `use pyo3::class::pyasync::IterANextOutput`. - /// - /// For compatibility reasons this has not yet been removed, however will be done so - /// once is resolved. - pub mod pyasync { - #[allow(deprecated)] - pub use crate::pyclass::{IterANextOutput, PyIterANextOutput}; - } - - /// Old module which contained some implementation details of the `#[pyproto]` module. - /// - /// Prefer using the same content from `pyo3::pyclass`, e.g. `use pyo3::pyclass::IterNextOutput` instead - /// of `use pyo3::class::pyasync::IterNextOutput`. - /// - /// For compatibility reasons this has not yet been removed, however will be done so - /// once is resolved. - pub mod iter { - #[allow(deprecated)] - pub use crate::pyclass::{IterNextOutput, PyIterNextOutput}; - } - /// Old module which contained some implementation details of the `#[pyproto]` module. /// /// Prefer using the same content from `pyo3::pyclass`, e.g. `use pyo3::pyclass::PyTraverseError` instead diff --git a/src/marker.rs b/src/marker.rs index 065adc7dda9..689f58db311 100644 --- a/src/marker.rs +++ b/src/marker.rs @@ -126,10 +126,10 @@ use crate::types::{ PyAny, PyDict, PyEllipsis, PyModule, PyNone, PyNotImplemented, PyString, PyType, }; use crate::version::PythonVersionInfo; -use crate::{ffi, Bound, IntoPy, Py, PyObject, PyTypeInfo}; #[allow(deprecated)] #[cfg(feature = "gil-refs")] -use crate::{gil::GILPool, FromPyPointer, PyNativeType}; +use crate::{conversion::FromPyPointer, gil::GILPool, PyNativeType}; +use crate::{ffi, Bound, IntoPy, Py, PyObject, PyTypeInfo}; use std::ffi::{CStr, CString}; use std::marker::PhantomData; use std::os::raw::c_int; @@ -801,42 +801,6 @@ impl<'py> Python<'py> { PythonVersionInfo::from_str(version_number_str).unwrap() } - /// Registers the object in the release pool, and tries to downcast to specific type. - #[cfg(feature = "gil-refs")] - #[deprecated( - since = "0.21.0", - note = "use `obj.downcast_bound::(py)` instead of `py.checked_cast_as::(obj)`" - )] - pub fn checked_cast_as( - self, - obj: PyObject, - ) -> Result<&'py T, crate::err::PyDowncastError<'py>> - where - T: crate::PyTypeCheck, - { - #[allow(deprecated)] - obj.into_ref(self).downcast() - } - - /// Registers the object in the release pool, and does an unchecked downcast - /// to the specific type. - /// - /// # Safety - /// - /// Callers must ensure that ensure that the cast is valid. - #[cfg(feature = "gil-refs")] - #[deprecated( - since = "0.21.0", - note = "use `obj.downcast_bound_unchecked::(py)` instead of `py.cast_as::(obj)`" - )] - pub unsafe fn cast_as(self, obj: PyObject) -> &'py T - where - T: crate::type_object::HasPyGilRef, - { - #[allow(deprecated)] - obj.into_ref(self).downcast_unchecked() - } - /// Registers the object pointer in the release pool, /// and does an unchecked downcast to the specific type. /// diff --git a/src/prelude.rs b/src/prelude.rs index 6a0657c8a98..3f45cb52cf0 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -9,9 +9,6 @@ //! ``` pub use crate::conversion::{FromPyObject, IntoPy, ToPyObject}; -#[cfg(feature = "gil-refs")] -#[allow(deprecated)] -pub use crate::conversion::{PyTryFrom, PyTryInto}; pub use crate::err::{PyErr, PyResult}; pub use crate::instance::{Borrowed, Bound, Py, PyObject}; pub use crate::marker::Python; diff --git a/src/pyclass.rs b/src/pyclass.rs index 29cd1251974..91510e11151 100644 --- a/src/pyclass.rs +++ b/src/pyclass.rs @@ -1,8 +1,5 @@ //! `PyClass` and related traits. -use crate::{ - callback::IntoPyCallbackOutput, ffi, impl_::pyclass::PyClassImpl, IntoPy, PyObject, PyResult, - PyTypeInfo, Python, -}; +use crate::{ffi, impl_::pyclass::PyClassImpl, PyTypeInfo}; use std::{cmp::Ordering, os::raw::c_int}; mod create_type_object; @@ -99,111 +96,6 @@ impl CompareOp { } } -/// Output of `__next__` which can either `yield` the next value in the iteration, or -/// `return` a value to raise `StopIteration` in Python. -/// -/// Usage example: -/// -/// ```rust -/// # #![allow(deprecated)] -/// use pyo3::prelude::*; -/// use pyo3::iter::IterNextOutput; -/// -/// #[pyclass] -/// struct PyClassIter { -/// count: usize, -/// } -/// -/// #[pymethods] -/// impl PyClassIter { -/// #[new] -/// pub fn new() -> Self { -/// PyClassIter { count: 0 } -/// } -/// -/// fn __next__(&mut self) -> IterNextOutput { -/// if self.count < 5 { -/// self.count += 1; -/// // Given an instance `counter`, First five `next(counter)` calls yield 1, 2, 3, 4, 5. -/// IterNextOutput::Yield(self.count) -/// } else { -/// // At the sixth time, we get a `StopIteration` with `'Ended'`. -/// // try: -/// // next(counter) -/// // except StopIteration as e: -/// // assert e.value == 'Ended' -/// IterNextOutput::Return("Ended") -/// } -/// } -/// } -/// ``` -#[deprecated(since = "0.21.0", note = "Use `Option` or `PyStopIteration` instead.")] -pub enum IterNextOutput { - /// The value yielded by the iterator. - Yield(T), - /// The `StopIteration` object. - Return(U), -} - -/// Alias of `IterNextOutput` with `PyObject` yield & return values. -#[deprecated(since = "0.21.0", note = "Use `Option` or `PyStopIteration` instead.")] -#[allow(deprecated)] -pub type PyIterNextOutput = IterNextOutput; - -#[allow(deprecated)] -impl IntoPyCallbackOutput<*mut ffi::PyObject> for IterNextOutput -where - T: IntoPy, - U: IntoPy, -{ - fn convert(self, py: Python<'_>) -> PyResult<*mut ffi::PyObject> { - match self { - IterNextOutput::Yield(o) => Ok(o.into_py(py).into_ptr()), - IterNextOutput::Return(o) => { - Err(crate::exceptions::PyStopIteration::new_err(o.into_py(py))) - } - } - } -} - -/// Output of `__anext__`. -/// -/// -#[deprecated( - since = "0.21.0", - note = "Use `Option` or `PyStopAsyncIteration` instead." -)] -pub enum IterANextOutput { - /// An expression which the generator yielded. - Yield(T), - /// A `StopAsyncIteration` object. - Return(U), -} - -/// An [IterANextOutput] of Python objects. -#[deprecated( - since = "0.21.0", - note = "Use `Option` or `PyStopAsyncIteration` instead." -)] -#[allow(deprecated)] -pub type PyIterANextOutput = IterANextOutput; - -#[allow(deprecated)] -impl IntoPyCallbackOutput<*mut ffi::PyObject> for IterANextOutput -where - T: IntoPy, - U: IntoPy, -{ - fn convert(self, py: Python<'_>) -> PyResult<*mut ffi::PyObject> { - match self { - IterANextOutput::Yield(o) => Ok(o.into_py(py).into_ptr()), - IterANextOutput::Return(o) => Err(crate::exceptions::PyStopAsyncIteration::new_err( - o.into_py(py), - )), - } - } -} - /// A workaround for [associated const equality](https://github.com/rust-lang/rust/issues/92827). /// /// This serves to have True / False values in the [`PyClass`] trait's `Frozen` type. diff --git a/src/types/any.rs b/src/types/any.rs index c991b69b4a4..0b45dab2c92 100644 --- a/src/types/any.rs +++ b/src/types/any.rs @@ -581,14 +581,6 @@ impl PyAny { .map(Bound::into_gil_ref) } - /// Returns whether the object is considered to be true. - /// - /// This is equivalent to the Python expression `bool(self)`. - #[deprecated(since = "0.21.0", note = "use `.is_truthy()` instead")] - pub fn is_true(&self) -> PyResult { - self.is_truthy() - } - /// Returns whether the object is considered to be true. /// /// This applies truth value testing equivalent to the Python expression `bool(self)`. diff --git a/src/types/iterator.rs b/src/types/iterator.rs index 38f3131be90..1ae9f9f471a 100644 --- a/src/types/iterator.rs +++ b/src/types/iterator.rs @@ -3,7 +3,7 @@ use crate::instance::Borrowed; use crate::py_result_ext::PyResultExt; use crate::{ffi, Bound, PyAny, PyErr, PyResult, PyTypeCheck}; #[cfg(feature = "gil-refs")] -use crate::{AsPyPointer, PyDowncastError, PyNativeType}; +use crate::{AsPyPointer, PyNativeType}; /// A Python iterator object. /// @@ -130,31 +130,6 @@ impl PyTypeCheck for PyIterator { } } -#[cfg(feature = "gil-refs")] -#[allow(deprecated)] -impl<'v> crate::PyTryFrom<'v> for PyIterator { - fn try_from>(value: V) -> Result<&'v PyIterator, PyDowncastError<'v>> { - let value = value.into(); - unsafe { - if ffi::PyIter_Check(value.as_ptr()) != 0 { - Ok(value.downcast_unchecked()) - } else { - Err(PyDowncastError::new(value, "Iterator")) - } - } - } - - fn try_from_exact>(value: V) -> Result<&'v PyIterator, PyDowncastError<'v>> { - value.into().downcast() - } - - #[inline] - unsafe fn try_from_unchecked>(value: V) -> &'v PyIterator { - let ptr = value.into() as *const _ as *const PyIterator; - &*ptr - } -} - #[cfg(test)] mod tests { use super::PyIterator; @@ -298,18 +273,6 @@ def fibonacci(target): }); } - #[test] - #[cfg(feature = "gil-refs")] - #[allow(deprecated)] - fn iterator_try_from() { - Python::with_gil(|py| { - let obj: crate::Py = - vec![10, 20].to_object(py).as_ref(py).iter().unwrap().into(); - let iter = ::try_from(obj.as_ref(py)).unwrap(); - assert!(obj.is(iter)); - }); - } - #[test] #[cfg(feature = "macros")] fn python_class_not_iterator() { diff --git a/src/types/mapping.rs b/src/types/mapping.rs index 82e6b326810..9ff0ad85762 100644 --- a/src/types/mapping.rs +++ b/src/types/mapping.rs @@ -7,7 +7,7 @@ use crate::type_object::PyTypeInfo; use crate::types::any::PyAnyMethods; use crate::types::{PyAny, PyDict, PySequence, PyType}; #[cfg(feature = "gil-refs")] -use crate::{err::PyDowncastError, PyNativeType}; +use crate::PyNativeType; use crate::{ffi, Py, PyTypeCheck, Python, ToPyObject}; /// Represents a reference to a Python object supporting the mapping protocol. @@ -266,34 +266,6 @@ impl PyTypeCheck for PyMapping { } } -#[cfg(feature = "gil-refs")] -#[allow(deprecated)] -impl<'v> crate::PyTryFrom<'v> for PyMapping { - /// Downcasting to `PyMapping` requires the concrete class to be a subclass (or registered - /// subclass) of `collections.abc.Mapping` (from the Python standard library) - i.e. - /// `isinstance(, collections.abc.Mapping) == True`. - fn try_from>(value: V) -> Result<&'v PyMapping, PyDowncastError<'v>> { - let value = value.into(); - - if PyMapping::type_check(&value.as_borrowed()) { - unsafe { return Ok(value.downcast_unchecked()) } - } - - Err(PyDowncastError::new(value, "Mapping")) - } - - #[inline] - fn try_from_exact>(value: V) -> Result<&'v PyMapping, PyDowncastError<'v>> { - value.into().downcast() - } - - #[inline] - unsafe fn try_from_unchecked>(value: V) -> &'v PyMapping { - let ptr = value.into() as *const _ as *const PyMapping; - &*ptr - } -} - #[cfg(test)] mod tests { use std::collections::HashMap; @@ -445,16 +417,4 @@ mod tests { assert_eq!(32 + 42 + 123, values_sum); }); } - - #[test] - #[cfg(feature = "gil-refs")] - #[allow(deprecated)] - fn test_mapping_try_from() { - use crate::PyTryFrom; - Python::with_gil(|py| { - let dict = PyDict::new(py); - let _ = ::try_from(dict).unwrap(); - let _ = PyMapping::try_from_exact(dict).unwrap(); - }); - } } diff --git a/src/types/sequence.rs b/src/types/sequence.rs index 39de9efb272..faf371160dd 100644 --- a/src/types/sequence.rs +++ b/src/types/sequence.rs @@ -10,7 +10,7 @@ use crate::sync::GILOnceCell; use crate::type_object::PyTypeInfo; use crate::types::{any::PyAnyMethods, PyAny, PyList, PyString, PyTuple, PyType}; #[cfg(feature = "gil-refs")] -use crate::{err::PyDowncastError, PyNativeType}; +use crate::PyNativeType; use crate::{ffi, FromPyObject, Py, PyTypeCheck, Python, ToPyObject}; /// Represents a reference to a Python object supporting the sequence protocol. @@ -553,33 +553,6 @@ impl PyTypeCheck for PySequence { } } -#[cfg(feature = "gil-refs")] -#[allow(deprecated)] -impl<'v> crate::PyTryFrom<'v> for PySequence { - /// Downcasting to `PySequence` requires the concrete class to be a subclass (or registered - /// subclass) of `collections.abc.Sequence` (from the Python standard library) - i.e. - /// `isinstance(, collections.abc.Sequence) == True`. - fn try_from>(value: V) -> Result<&'v PySequence, PyDowncastError<'v>> { - let value = value.into(); - - if PySequence::type_check(&value.as_borrowed()) { - unsafe { return Ok(value.downcast_unchecked::()) } - } - - Err(PyDowncastError::new(value, "Sequence")) - } - - fn try_from_exact>(value: V) -> Result<&'v PySequence, PyDowncastError<'v>> { - value.into().downcast() - } - - #[inline] - unsafe fn try_from_unchecked>(value: V) -> &'v PySequence { - let ptr = value.into() as *const _ as *const PySequence; - &*ptr - } -} - #[cfg(test)] mod tests { use crate::types::{PyAnyMethods, PyList, PySequence, PySequenceMethods, PyTuple}; @@ -1107,16 +1080,4 @@ mod tests { assert!(seq_from.to_list().is_ok()); }); } - - #[test] - #[cfg(feature = "gil-refs")] - #[allow(deprecated)] - fn test_seq_try_from() { - use crate::PyTryFrom; - Python::with_gil(|py| { - let list = PyList::empty(py); - let _ = ::try_from(list).unwrap(); - let _ = PySequence::try_from_exact(list).unwrap(); - }); - } } From 90c479995102963022ea9fb0e6423fbe5a3de211 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Wed, 10 Jul 2024 15:18:13 +0100 Subject: [PATCH 154/495] docs: fixups to 0.22 migration guide (#4332) --- guide/src/migration.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/guide/src/migration.md b/guide/src/migration.md index 626458b46d1..ab301affac0 100644 --- a/guide/src/migration.md +++ b/guide/src/migration.md @@ -5,6 +5,15 @@ For a detailed list of all changes, see the [CHANGELOG](changelog.md). ## from 0.21.* to 0.22 +### Deprecation of `gil-refs` feature continues + + ### Deprecation of implicit default for trailing optional arguments
Click to expand @@ -528,6 +537,7 @@ assert_eq!(&*name, "list"); # } # Python::with_gil(example).unwrap(); ``` +
## from 0.19.* to 0.20 From 6be80647cbfb091dfb46228ba1943663966bbb22 Mon Sep 17 00:00:00 2001 From: Larry Z Date: Wed, 10 Jul 2024 19:39:39 +0100 Subject: [PATCH 155/495] Prevent building in GIL-less environment (#4327) * Prevent building in GIL-less environment * Add change log * add "yet" to phrasing * Add testing to build script * add link to issue * Fix formatting issues --------- Co-authored-by: David Hewitt --- newsfragments/4327.packaging.md | 3 +++ noxfile.py | 15 +++++++++++++-- pyo3-build-config/src/impl_.rs | 5 ++++- pyo3-ffi/build.rs | 22 +++++++++++++++++++++- 4 files changed, 41 insertions(+), 4 deletions(-) create mode 100644 newsfragments/4327.packaging.md diff --git a/newsfragments/4327.packaging.md b/newsfragments/4327.packaging.md new file mode 100644 index 00000000000..c98d06f7cba --- /dev/null +++ b/newsfragments/4327.packaging.md @@ -0,0 +1,3 @@ +This PR lets PyO3 checks `Py_GIL_DISABLED` build flag and prevents `pyo3-ffi` crate building against GIL-less Python, +unless +explicitly opt using the `UNSAFE_PYO3_BUILD_FREE_THREADED` environment flag. \ No newline at end of file diff --git a/noxfile.py b/noxfile.py index 4757a282e84..5d8123c7eb4 100644 --- a/noxfile.py +++ b/noxfile.py @@ -9,7 +9,7 @@ from functools import lru_cache from glob import glob from pathlib import Path -from typing import Any, Callable, Dict, Iterator, List, Optional, Tuple +from typing import Any, Callable, Dict, Iterable, Iterator, List, Optional, Tuple import nox import nox.command @@ -655,6 +655,14 @@ def test_version_limits(session: nox.Session): config_file.set("PyPy", "3.11") _run_cargo(session, "check", env=env, expect_error=True) + # Python build with GIL disabled should fail building + config_file.set("CPython", "3.13", build_flags=["Py_GIL_DISABLED"]) + _run_cargo(session, "check", env=env, expect_error=True) + + # Python build with GIL disabled should pass with env flag on + env["UNSAFE_PYO3_BUILD_FREE_THREADED"] = "1" + _run_cargo(session, "check", env=env) + @nox.session(name="check-feature-powerset", venv_backend="none") def check_feature_powerset(session: nox.Session): @@ -919,7 +927,9 @@ class _ConfigFile: def __init__(self, config_file) -> None: self._config_file = config_file - def set(self, implementation: str, version: str) -> None: + def set( + self, implementation: str, version: str, build_flags: Iterable[str] = () + ) -> None: """Set the contents of this config file to the given implementation and version.""" self._config_file.seek(0) self._config_file.truncate(0) @@ -927,6 +937,7 @@ def set(self, implementation: str, version: str) -> None: f"""\ implementation={implementation} version={version} +build_flags={','.join(build_flags)} suppress_build_script_link_lines=true """ ) diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index 3dc1e912447..d38d41ed552 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -996,6 +996,7 @@ pub enum BuildFlag { Py_DEBUG, Py_REF_DEBUG, Py_TRACE_REFS, + Py_GIL_DISABLED, COUNT_ALLOCS, Other(String), } @@ -1016,6 +1017,7 @@ impl FromStr for BuildFlag { "Py_DEBUG" => Ok(BuildFlag::Py_DEBUG), "Py_REF_DEBUG" => Ok(BuildFlag::Py_REF_DEBUG), "Py_TRACE_REFS" => Ok(BuildFlag::Py_TRACE_REFS), + "Py_GIL_DISABLED" => Ok(BuildFlag::Py_GIL_DISABLED), "COUNT_ALLOCS" => Ok(BuildFlag::COUNT_ALLOCS), other => Ok(BuildFlag::Other(other.to_owned())), } @@ -1039,10 +1041,11 @@ impl FromStr for BuildFlag { pub struct BuildFlags(pub HashSet); impl BuildFlags { - const ALL: [BuildFlag; 4] = [ + const ALL: [BuildFlag; 5] = [ BuildFlag::Py_DEBUG, BuildFlag::Py_REF_DEBUG, BuildFlag::Py_TRACE_REFS, + BuildFlag::Py_GIL_DISABLED, BuildFlag::COUNT_ALLOCS, ]; diff --git a/pyo3-ffi/build.rs b/pyo3-ffi/build.rs index b4521678ba9..83408b31222 100644 --- a/pyo3-ffi/build.rs +++ b/pyo3-ffi/build.rs @@ -4,8 +4,9 @@ use pyo3_build_config::{ cargo_env_var, env_var, errors::Result, is_linking_libpython, resolve_interpreter_config, InterpreterConfig, PythonVersion, }, - warn, PythonImplementation, + warn, BuildFlag, PythonImplementation, }; +use std::ops::Not; /// Minimum Python version PyO3 supports. struct SupportedVersions { @@ -120,6 +121,24 @@ fn ensure_python_version(interpreter_config: &InterpreterConfig) -> Result<()> { Ok(()) } +fn ensure_gil_enabled(interpreter_config: &InterpreterConfig) -> Result<()> { + let gil_enabled = interpreter_config + .build_flags + .0 + .contains(&BuildFlag::Py_GIL_DISABLED) + .not(); + ensure!( + gil_enabled || std::env::var("UNSAFE_PYO3_BUILD_FREE_THREADED").map_or(false, |os_str| os_str == "1"), + "the Python interpreter was built with the GIL disabled, which is not yet supported by PyO3\n\ + = help: see https://github.com/PyO3/pyo3/issues/4265 for more information\n\ + = help: please check if an updated version of PyO3 is available. Current version: {}\n\ + = help: set UNSAFE_PYO3_BUILD_FREE_THREADED=1 to suppress this check and build anyway for free-threaded Python", + std::env::var("CARGO_PKG_VERSION").unwrap() + ); + + Ok(()) +} + fn ensure_target_pointer_width(interpreter_config: &InterpreterConfig) -> Result<()> { if let Some(pointer_width) = interpreter_config.pointer_width { // Try to check whether the target architecture matches the python library @@ -185,6 +204,7 @@ fn configure_pyo3() -> Result<()> { ensure_python_version(&interpreter_config)?; ensure_target_pointer_width(&interpreter_config)?; + ensure_gil_enabled(&interpreter_config)?; // Serialize the whole interpreter config into DEP_PYTHON_PYO3_CONFIG env var. interpreter_config.to_cargo_dep_env()?; From a5a3f3f7f28b6a542b29fa552059b8e7bee496fa Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Wed, 10 Jul 2024 23:38:38 +0100 Subject: [PATCH 156/495] allow `#[pymodule(...)]` to accept all relevant `#[pyo3(...)]` options (#4330) --- examples/getitem/src/lib.rs | 3 +- guide/src/module.md | 3 +- newsfragments/4330.changed.md | 1 + pyo3-macros-backend/src/module.rs | 137 ++++++++---------- pyo3-macros/src/lib.rs | 44 +++--- tests/test_declarative_module.rs | 6 +- tests/test_module.rs | 3 +- tests/ui/duplicate_pymodule_submodule.stderr | 14 +- tests/ui/empty.rs | 1 + tests/ui/invalid_pymodule_args.stderr | 2 +- tests/ui/invalid_pymodule_glob.rs | 2 + tests/ui/invalid_pymodule_glob.stderr | 4 +- tests/ui/invalid_pymodule_in_root.rs | 1 + tests/ui/invalid_pymodule_in_root.stderr | 8 +- tests/ui/invalid_pymodule_trait.stderr | 6 + .../ui/invalid_pymodule_two_pymodule_init.rs | 6 +- .../invalid_pymodule_two_pymodule_init.stderr | 4 +- 17 files changed, 125 insertions(+), 120 deletions(-) create mode 100644 newsfragments/4330.changed.md create mode 100644 tests/ui/empty.rs diff --git a/examples/getitem/src/lib.rs b/examples/getitem/src/lib.rs index ce162a70bf9..ba850a06b8d 100644 --- a/examples/getitem/src/lib.rs +++ b/examples/getitem/src/lib.rs @@ -75,8 +75,7 @@ impl ExampleContainer { } } -#[pymodule] -#[pyo3(name = "getitem")] +#[pymodule(name = "getitem")] fn example(m: &Bound<'_, PyModule>) -> PyResult<()> { // ? -https://github.com/PyO3/maturin/issues/475 m.add_class::()?; diff --git a/guide/src/module.md b/guide/src/module.md index ee5485e0ee2..78616946357 100644 --- a/guide/src/module.md +++ b/guide/src/module.md @@ -31,8 +31,7 @@ fn double(x: usize) -> usize { x * 2 } -#[pymodule] -#[pyo3(name = "custom_name")] +#[pymodule(name = "custom_name")] fn my_extension(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(double, m)?) } diff --git a/newsfragments/4330.changed.md b/newsfragments/4330.changed.md new file mode 100644 index 00000000000..d465ec99cec --- /dev/null +++ b/newsfragments/4330.changed.md @@ -0,0 +1 @@ +`#[pymodule(...)]` now directly accepts all relevant `#[pyo3(...)]` options. diff --git a/pyo3-macros-backend/src/module.rs b/pyo3-macros-backend/src/module.rs index 78e8999483c..1e764176550 100644 --- a/pyo3-macros-backend/src/module.rs +++ b/pyo3-macros-backend/src/module.rs @@ -2,8 +2,8 @@ use crate::{ attributes::{ - self, take_attributes, take_pyo3_options, CrateAttribute, ModuleAttribute, NameAttribute, - SubmoduleAttribute, + self, kw, take_attributes, take_pyo3_options, CrateAttribute, ModuleAttribute, + NameAttribute, SubmoduleAttribute, }, get_doc, pyclass::PyClassPyO3Option, @@ -16,7 +16,7 @@ use std::ffi::CString; use syn::{ ext::IdentExt, parse::{Parse, ParseStream}, - parse_quote, + parse_quote, parse_quote_spanned, punctuated::Punctuated, spanned::Spanned, token::Comma, @@ -26,105 +26,89 @@ use syn::{ #[derive(Default)] pub struct PyModuleOptions { krate: Option, - name: Option, + name: Option, module: Option, - is_submodule: bool, + submodule: Option, } -impl PyModuleOptions { - pub fn from_attrs(attrs: &mut Vec) -> Result { +impl Parse for PyModuleOptions { + fn parse(input: ParseStream<'_>) -> syn::Result { let mut options: PyModuleOptions = Default::default(); - for option in take_pyo3_options(attrs)? { - match option { - PyModulePyO3Option::Name(name) => options.set_name(name.value.0)?, - PyModulePyO3Option::Crate(path) => options.set_crate(path)?, - PyModulePyO3Option::Module(module) => options.set_module(module)?, - PyModulePyO3Option::Submodule(submod) => options.set_submodule(submod)?, - } - } + options.add_attributes( + Punctuated::::parse_terminated(input)?, + )?; Ok(options) } +} - fn set_name(&mut self, name: syn::Ident) -> Result<()> { - ensure_spanned!( - self.name.is_none(), - name.span() => "`name` may only be specified once" - ); - - self.name = Some(name); - Ok(()) - } - - fn set_crate(&mut self, path: CrateAttribute) -> Result<()> { - ensure_spanned!( - self.krate.is_none(), - path.span() => "`crate` may only be specified once" - ); - - self.krate = Some(path); - Ok(()) - } - - fn set_module(&mut self, name: ModuleAttribute) -> Result<()> { - ensure_spanned!( - self.module.is_none(), - name.span() => "`module` may only be specified once" - ); - - self.module = Some(name); - Ok(()) +impl PyModuleOptions { + fn take_pyo3_options(&mut self, attrs: &mut Vec) -> Result<()> { + self.add_attributes(take_pyo3_options(attrs)?) } - fn set_submodule(&mut self, submod: SubmoduleAttribute) -> Result<()> { - ensure_spanned!( - !self.is_submodule, - submod.span() => "`submodule` may only be specified once (it is implicitly always specified for nested modules)" - ); - - self.is_submodule = true; + fn add_attributes( + &mut self, + attrs: impl IntoIterator, + ) -> Result<()> { + macro_rules! set_option { + ($key:ident $(, $extra:literal)?) => { + { + ensure_spanned!( + self.$key.is_none(), + $key.span() => concat!("`", stringify!($key), "` may only be specified once" $(, $extra)?) + ); + self.$key = Some($key); + } + }; + } + for attr in attrs { + match attr { + PyModulePyO3Option::Crate(krate) => set_option!(krate), + PyModulePyO3Option::Name(name) => set_option!(name), + PyModulePyO3Option::Module(module) => set_option!(module), + PyModulePyO3Option::Submodule(submodule) => set_option!( + submodule, + " (it is implicitly always specified for nested modules)" + ), + } + } Ok(()) } } pub fn pymodule_module_impl( - mut module: syn::ItemMod, - mut is_submodule: bool, + module: &mut syn::ItemMod, + mut options: PyModuleOptions, ) -> Result { let syn::ItemMod { attrs, vis, unsafety: _, ident, - mod_token: _, + mod_token, content, semi: _, - } = &mut module; + } = module; let items = if let Some((_, items)) = content { items } else { - bail_spanned!(module.span() => "`#[pymodule]` can only be used on inline modules") + bail_spanned!(mod_token.span() => "`#[pymodule]` can only be used on inline modules") }; - let options = PyModuleOptions::from_attrs(attrs)?; + options.take_pyo3_options(attrs)?; let ctx = &Ctx::new(&options.krate, None); let Ctx { pyo3_path, .. } = ctx; let doc = get_doc(attrs, None, ctx); - let name = options.name.unwrap_or_else(|| ident.unraw()); + let name = options + .name + .map_or_else(|| ident.unraw(), |name| name.value.0); let full_name = if let Some(module) = &options.module { format!("{}.{}", module.value.value(), name) } else { name.to_string() }; - is_submodule = match (is_submodule, options.is_submodule) { - (true, true) => { - bail_spanned!(module.span() => "`submodule` may only be specified once (it is implicitly always specified for nested modules)") - } - (false, false) => false, - (true, false) | (false, true) => true, - }; - let mut module_items = Vec::new(); let mut module_items_cfg_attrs = Vec::new(); @@ -280,7 +264,9 @@ pub fn pymodule_module_impl( )? { set_module_attribute(&mut item_mod.attrs, &full_name); } - item_mod.attrs.push(parse_quote!(#[pyo3(submodule)])); + item_mod + .attrs + .push(parse_quote_spanned!(item_mod.mod_token.span()=> #[pyo3(submodule)])); } } Item::ForeignMod(item) => { @@ -358,10 +344,11 @@ pub fn pymodule_module_impl( ) } }}; - let initialization = module_initialization(&name, ctx, module_def, is_submodule); + let initialization = module_initialization(&name, ctx, module_def, options.submodule.is_some()); + Ok(quote!( #(#attrs)* - #vis mod #ident { + #vis #mod_token #ident { #(#items)* #initialization @@ -381,13 +368,18 @@ pub fn pymodule_module_impl( /// Generates the function that is called by the python interpreter to initialize the native /// module -pub fn pymodule_function_impl(mut function: syn::ItemFn) -> Result { - let options = PyModuleOptions::from_attrs(&mut function.attrs)?; - process_functions_in_module(&options, &mut function)?; +pub fn pymodule_function_impl( + function: &mut syn::ItemFn, + mut options: PyModuleOptions, +) -> Result { + options.take_pyo3_options(&mut function.attrs)?; + process_functions_in_module(&options, function)?; let ctx = &Ctx::new(&options.krate, None); let Ctx { pyo3_path, .. } = ctx; let ident = &function.sig.ident; - let name = options.name.unwrap_or_else(|| ident.unraw()); + let name = options + .name + .map_or_else(|| ident.unraw(), |name| name.value.0); let vis = &function.vis; let doc = get_doc(&function.attrs, None, ctx); @@ -402,7 +394,6 @@ pub fn pymodule_function_impl(mut function: syn::ItemFn) -> Result .push(quote!(::std::convert::Into::into(#pyo3_path::impl_::pymethods::BoundRef(module)))); Ok(quote! { - #function #[doc(hidden)] #vis mod #ident { #initialization diff --git a/pyo3-macros/src/lib.rs b/pyo3-macros/src/lib.rs index d9e22a94ede..c4263a512d3 100644 --- a/pyo3-macros/src/lib.rs +++ b/pyo3-macros/src/lib.rs @@ -3,14 +3,14 @@ #![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] use proc_macro::TokenStream; -use proc_macro2::{Span, TokenStream as TokenStream2}; +use proc_macro2::TokenStream as TokenStream2; use pyo3_macros_backend::{ build_derive_from_pyobject, build_py_class, build_py_enum, build_py_function, build_py_methods, pymodule_function_impl, pymodule_module_impl, PyClassArgs, PyClassMethodsType, - PyFunctionOptions, + PyFunctionOptions, PyModuleOptions, }; use quote::quote; -use syn::{parse::Nothing, parse_macro_input, Item}; +use syn::{parse_macro_input, Item}; /// A proc macro used to implement Python modules. /// @@ -24,6 +24,9 @@ use syn::{parse::Nothing, parse_macro_input, Item}; /// | Annotation | Description | /// | :- | :- | /// | `#[pyo3(name = "...")]` | Defines the name of the module in Python. | +/// | `#[pyo3(submodule)]` | Skips adding a `PyInit_` FFI symbol to the compiled binary. | +/// | `#[pyo3(module = "...")]` | Defines the Python `dotted.path` to the parent module for use in introspection. | +/// | `#[pyo3(crate = "pyo3")]` | Defines the path to PyO3 to use code generated by the macro. | /// /// For more on creating Python modules see the [module section of the guide][1]. /// @@ -35,32 +38,29 @@ use syn::{parse::Nothing, parse_macro_input, Item}; #[doc = concat!("[1]: https://pyo3.rs/v", env!("CARGO_PKG_VERSION"), "/module.html")] #[proc_macro_attribute] pub fn pymodule(args: TokenStream, input: TokenStream) -> TokenStream { - match parse_macro_input!(input as Item) { + let options = parse_macro_input!(args as PyModuleOptions); + + let mut ast = parse_macro_input!(input as Item); + let expanded = match &mut ast { Item::Mod(module) => { - let is_submodule = match parse_macro_input!(args as Option) { - Some(i) if i == "submodule" => true, - Some(_) => { - return syn::Error::new( - Span::call_site(), - "#[pymodule] only accepts submodule as an argument", - ) - .into_compile_error() - .into(); - } - None => false, - }; - pymodule_module_impl(module, is_submodule) - } - Item::Fn(function) => { - parse_macro_input!(args as Nothing); - pymodule_function_impl(function) + match pymodule_module_impl(module, options) { + // #[pymodule] on a module will rebuild the original ast, so we don't emit it here + Ok(expanded) => return expanded.into(), + Err(e) => Err(e), + } } + Item::Fn(function) => pymodule_function_impl(function, options), unsupported => Err(syn::Error::new_spanned( unsupported, "#[pymodule] only supports modules and functions.", )), } - .unwrap_or_compile_error() + .unwrap_or_compile_error(); + + quote!( + #ast + #expanded + ) .into() } diff --git a/tests/test_declarative_module.rs b/tests/test_declarative_module.rs index f0952438241..060da188aaa 100644 --- a/tests/test_declarative_module.rs +++ b/tests/test_declarative_module.rs @@ -49,8 +49,7 @@ create_exception!( "Some description." ); -#[pymodule] -#[pyo3(submodule)] +#[pymodule(submodule)] mod external_submodule {} /// A module written using declarative syntax. @@ -144,8 +143,7 @@ mod declarative_submodule { use super::{double, double_value}; } -#[pymodule] -#[pyo3(name = "declarative_module_renamed")] +#[pymodule(name = "declarative_module_renamed")] mod declarative_module2 { #[pymodule_export] use super::double; diff --git a/tests/test_module.rs b/tests/test_module.rs index b2487cfd8b3..eba1bcce6d2 100644 --- a/tests/test_module.rs +++ b/tests/test_module.rs @@ -138,8 +138,7 @@ fn test_module_with_explicit_py_arg() { }); } -#[pymodule] -#[pyo3(name = "other_name")] +#[pymodule(name = "other_name")] fn some_name(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add("other_name", "other_name")?; Ok(()) diff --git a/tests/ui/duplicate_pymodule_submodule.stderr b/tests/ui/duplicate_pymodule_submodule.stderr index a16e9ac75b6..a2141b0dcd3 100644 --- a/tests/ui/duplicate_pymodule_submodule.stderr +++ b/tests/ui/duplicate_pymodule_submodule.stderr @@ -4,8 +4,14 @@ error: `submodule` may only be specified once (it is implicitly always specified 4 | mod submod {} | ^^^ -error[E0433]: failed to resolve: use of undeclared crate or module `submod` - --> tests/ui/duplicate_pymodule_submodule.rs:4:6 +error[E0425]: cannot find value `_PYO3_DEF` in module `submod` + --> tests/ui/duplicate_pymodule_submodule.rs:1:1 + | +1 | #[pyo3::pymodule] + | ^^^^^^^^^^^^^^^^^ not found in `submod` + | + = note: this error originates in the attribute macro `pyo3::pymodule` (in Nightly builds, run with -Z macro-backtrace for more info) +help: consider importing this static + | +3 + use crate::mymodule::_PYO3_DEF; | -4 | mod submod {} - | ^^^^^^ use of undeclared crate or module `submod` diff --git a/tests/ui/empty.rs b/tests/ui/empty.rs new file mode 100644 index 00000000000..be89c636426 --- /dev/null +++ b/tests/ui/empty.rs @@ -0,0 +1 @@ +// see invalid_pymodule_in_root.rs diff --git a/tests/ui/invalid_pymodule_args.stderr b/tests/ui/invalid_pymodule_args.stderr index 933b6d6081c..261d8115e15 100644 --- a/tests/ui/invalid_pymodule_args.stderr +++ b/tests/ui/invalid_pymodule_args.stderr @@ -1,4 +1,4 @@ -error: unexpected token +error: expected one of: `name`, `crate`, `module`, `submodule` --> tests/ui/invalid_pymodule_args.rs:3:12 | 3 | #[pymodule(some_arg)] diff --git a/tests/ui/invalid_pymodule_glob.rs b/tests/ui/invalid_pymodule_glob.rs index 107cdf9382a..853493b535e 100644 --- a/tests/ui/invalid_pymodule_glob.rs +++ b/tests/ui/invalid_pymodule_glob.rs @@ -1,3 +1,5 @@ +#![allow(unused_imports)] + use pyo3::prelude::*; #[pyfunction] diff --git a/tests/ui/invalid_pymodule_glob.stderr b/tests/ui/invalid_pymodule_glob.stderr index 237e02037aa..1c033083e0c 100644 --- a/tests/ui/invalid_pymodule_glob.stderr +++ b/tests/ui/invalid_pymodule_glob.stderr @@ -1,5 +1,5 @@ error: #[pymodule] cannot import glob statements - --> tests/ui/invalid_pymodule_glob.rs:11:16 + --> tests/ui/invalid_pymodule_glob.rs:13:16 | -11 | use super::*; +13 | use super::*; | ^ diff --git a/tests/ui/invalid_pymodule_in_root.rs b/tests/ui/invalid_pymodule_in_root.rs index 47af4205f71..76ced6c3fb6 100644 --- a/tests/ui/invalid_pymodule_in_root.rs +++ b/tests/ui/invalid_pymodule_in_root.rs @@ -1,6 +1,7 @@ use pyo3::prelude::*; #[pymodule] +#[path = "empty.rs"] // to silence error related to missing file mod invalid_pymodule_in_root_module; fn main() {} diff --git a/tests/ui/invalid_pymodule_in_root.stderr b/tests/ui/invalid_pymodule_in_root.stderr index 91783be0e97..06152161e5d 100644 --- a/tests/ui/invalid_pymodule_in_root.stderr +++ b/tests/ui/invalid_pymodule_in_root.stderr @@ -1,13 +1,13 @@ error[E0658]: non-inline modules in proc macro input are unstable - --> tests/ui/invalid_pymodule_in_root.rs:4:1 + --> tests/ui/invalid_pymodule_in_root.rs:5:1 | -4 | mod invalid_pymodule_in_root_module; +5 | mod invalid_pymodule_in_root_module; | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | = note: see issue #54727 for more information error: `#[pymodule]` can only be used on inline modules - --> tests/ui/invalid_pymodule_in_root.rs:4:1 + --> tests/ui/invalid_pymodule_in_root.rs:5:1 | -4 | mod invalid_pymodule_in_root_module; +5 | mod invalid_pymodule_in_root_module; | ^^^ diff --git a/tests/ui/invalid_pymodule_trait.stderr b/tests/ui/invalid_pymodule_trait.stderr index 4b02f14a540..0b0d46da93d 100644 --- a/tests/ui/invalid_pymodule_trait.stderr +++ b/tests/ui/invalid_pymodule_trait.stderr @@ -3,3 +3,9 @@ error: `#[pymodule_export]` may only be used on `use` statements | 5 | #[pymodule_export] | ^ + +error: cannot find attribute `pymodule_export` in this scope + --> tests/ui/invalid_pymodule_trait.rs:5:7 + | +5 | #[pymodule_export] + | ^^^^^^^^^^^^^^^ diff --git a/tests/ui/invalid_pymodule_two_pymodule_init.rs b/tests/ui/invalid_pymodule_two_pymodule_init.rs index d676b0fa277..ed72cbb7237 100644 --- a/tests/ui/invalid_pymodule_two_pymodule_init.rs +++ b/tests/ui/invalid_pymodule_two_pymodule_init.rs @@ -2,13 +2,15 @@ use pyo3::prelude::*; #[pymodule] mod module { + use pyo3::prelude::*; + #[pymodule_init] - fn init(m: &PyModule) -> PyResult<()> { + fn init(_m: &Bound<'_, PyModule>) -> PyResult<()> { Ok(()) } #[pymodule_init] - fn init2(m: &PyModule) -> PyResult<()> { + fn init2(_m: &Bound<'_, PyModule>) -> PyResult<()> { Ok(()) } } diff --git a/tests/ui/invalid_pymodule_two_pymodule_init.stderr b/tests/ui/invalid_pymodule_two_pymodule_init.stderr index c117ebd573f..8fbd12f2e45 100644 --- a/tests/ui/invalid_pymodule_two_pymodule_init.stderr +++ b/tests/ui/invalid_pymodule_two_pymodule_init.stderr @@ -1,5 +1,5 @@ error: only one `#[pymodule_init]` may be specified - --> tests/ui/invalid_pymodule_two_pymodule_init.rs:11:5 + --> tests/ui/invalid_pymodule_two_pymodule_init.rs:13:5 | -11 | fn init2(m: &PyModule) -> PyResult<()> { +13 | fn init2(_m: &Bound<'_, PyModule>) -> PyResult<()> { | ^^ From a0bc8f21fecf36fc8d051d4b3c57adfca64b5f1c Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Fri, 12 Jul 2024 13:39:30 +0200 Subject: [PATCH 157/495] remove most deprecated gil-ref APIs in `src/types` (#4338) --- guide/src/types.md | 6 +- src/conversion.rs | 4 - src/conversions/std/slice.rs | 27 --- src/conversions/std/string.rs | 29 +-- src/err/mod.rs | 1 - src/instance.rs | 3 +- src/internal_tricks.rs | 148 ------------- src/types/boolobject.rs | 24 -- src/types/bytearray.rs | 194 ---------------- src/types/bytes.rs | 54 ----- src/types/capsule.rs | 114 ---------- src/types/complex.rs | 34 +-- src/types/datetime.rs | 179 --------------- src/types/dict.rs | 228 ------------------- src/types/ellipsis.rs | 11 - src/types/float.rs | 20 -- src/types/frozenset.rs | 61 ----- src/types/function.rs | 68 ------ src/types/iterator.rs | 10 - src/types/list.rs | 308 -------------------------- src/types/mapping.rs | 83 ------- src/types/memoryview.rs | 10 - src/types/mod.rs | 9 - src/types/module.rs | 250 --------------------- src/types/none.rs | 12 - src/types/notimplemented.rs | 11 - src/types/pysuper.rs | 11 - src/types/sequence.rs | 278 ----------------------- src/types/set.rs | 86 -------- src/types/slice.rs | 31 --- src/types/string.rs | 84 ------- src/types/traceback.rs | 40 ---- src/types/tuple.rs | 229 ------------------- src/types/typeobject.rs | 66 ------ src/types/weakref/anyref.rs | 358 ------------------------------ src/types/weakref/proxy.rs | 391 --------------------------------- src/types/weakref/reference.rs | 389 +------------------------------- tests/test_no_imports.rs | 14 -- 38 files changed, 7 insertions(+), 3868 deletions(-) diff --git a/guide/src/types.md b/guide/src/types.md index ab95998bcfb..564937124ba 100644 --- a/guide/src/types.md +++ b/guide/src/types.md @@ -329,7 +329,7 @@ The following sections note some historical detail about the GIL Refs API. For a `&PyAny` object reference `any` where the underlying object is a Python-native type such as a list: -```rust +```rust,ignore # #![allow(unused_imports)] # use pyo3::prelude::*; # use pyo3::types::PyList; @@ -352,7 +352,7 @@ let _: Py = obj.extract()?; For a `&PyAny` object reference `any` where the underlying object is a `#[pyclass]`: -```rust +```rust,ignore # #![allow(unused_imports)] # use pyo3::prelude::*; # #[pyclass] #[derive(Clone)] struct MyClass { } @@ -393,7 +393,7 @@ To see all Python types exposed by `PyO3` consult the [`pyo3::types`][pyo3::type **Conversions:** -```rust +```rust,ignore # #![allow(unused_imports)] # use pyo3::prelude::*; # use pyo3::types::PyList; diff --git a/src/conversion.rs b/src/conversion.rs index 451d0df4abf..22ec199a035 100644 --- a/src/conversion.rs +++ b/src/conversion.rs @@ -258,13 +258,9 @@ mod from_py_object_bound_sealed { // This generic implementation is why the seal is separate from // `crate::sealed::Sealed`. impl<'py, T> Sealed for T where T: super::FromPyObject<'py> {} - #[cfg(not(feature = "gil-refs"))] impl Sealed for &'_ str {} - #[cfg(not(feature = "gil-refs"))] impl Sealed for std::borrow::Cow<'_, str> {} - #[cfg(not(feature = "gil-refs"))] impl Sealed for &'_ [u8] {} - #[cfg(not(feature = "gil-refs"))] impl Sealed for std::borrow::Cow<'_, [u8]> {} } diff --git a/src/conversions/std/slice.rs b/src/conversions/std/slice.rs index 9c9cde06fc7..34b3e61eaf1 100644 --- a/src/conversions/std/slice.rs +++ b/src/conversions/std/slice.rs @@ -18,19 +18,6 @@ impl<'a> IntoPy for &'a [u8] { } } -#[cfg(feature = "gil-refs")] -impl<'py> crate::FromPyObject<'py> for &'py [u8] { - fn extract_bound(obj: &crate::Bound<'py, PyAny>) -> PyResult { - Ok(obj.clone().into_gil_ref().downcast::()?.as_bytes()) - } - - #[cfg(feature = "experimental-inspect")] - fn type_input() -> TypeInfo { - Self::type_output() - } -} - -#[cfg(not(feature = "gil-refs"))] impl<'a> crate::conversion::FromPyObjectBound<'a, '_> for &'a [u8] { fn from_py_object_bound(obj: crate::Borrowed<'a, '_, PyAny>) -> PyResult { Ok(obj.downcast::()?.as_bytes()) @@ -47,20 +34,6 @@ impl<'a> crate::conversion::FromPyObjectBound<'a, '_> for &'a [u8] { /// If the source object is a `bytes` object, the `Cow` will be borrowed and /// pointing into the source object, and no copying or heap allocations will happen. /// If it is a `bytearray`, its contents will be copied to an owned `Cow`. -#[cfg(feature = "gil-refs")] -impl<'py> crate::FromPyObject<'py> for Cow<'py, [u8]> { - fn extract_bound(ob: &crate::Bound<'py, PyAny>) -> PyResult { - use crate::types::PyAnyMethods; - if let Ok(bytes) = ob.downcast::() { - return Ok(Cow::Borrowed(bytes.clone().into_gil_ref().as_bytes())); - } - - let byte_array = ob.downcast::()?; - Ok(Cow::Owned(byte_array.to_vec())) - } -} - -#[cfg(not(feature = "gil-refs"))] impl<'a> crate::conversion::FromPyObjectBound<'a, '_> for Cow<'a, [u8]> { fn from_py_object_bound(ob: crate::Borrowed<'a, '_, PyAny>) -> PyResult { if let Ok(bytes) = ob.downcast::() { diff --git a/src/conversions/std/string.rs b/src/conversions/std/string.rs index 5bc05c1a091..91b50aa1096 100644 --- a/src/conversions/std/string.rs +++ b/src/conversions/std/string.rs @@ -112,21 +112,7 @@ impl<'a> IntoPy for &'a String { } } -/// Allows extracting strings from Python objects. -/// Accepts Python `str` objects. -#[cfg(feature = "gil-refs")] -impl<'py> FromPyObject<'py> for &'py str { - fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult { - ob.clone().into_gil_ref().downcast::()?.to_str() - } - - #[cfg(feature = "experimental-inspect")] - fn type_input() -> TypeInfo { - ::type_input() - } -} - -#[cfg(all(not(feature = "gil-refs"), any(Py_3_10, not(Py_LIMITED_API))))] +#[cfg(any(Py_3_10, not(Py_LIMITED_API)))] impl<'a> crate::conversion::FromPyObjectBound<'a, '_> for &'a str { fn from_py_object_bound(ob: crate::Borrowed<'a, '_, PyAny>) -> PyResult { ob.downcast::()?.to_str() @@ -138,19 +124,6 @@ impl<'a> crate::conversion::FromPyObjectBound<'a, '_> for &'a str { } } -#[cfg(feature = "gil-refs")] -impl<'py> FromPyObject<'py> for Cow<'py, str> { - fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult { - ob.extract().map(Cow::Owned) - } - - #[cfg(feature = "experimental-inspect")] - fn type_input() -> TypeInfo { - ::type_input() - } -} - -#[cfg(not(feature = "gil-refs"))] impl<'a> crate::conversion::FromPyObjectBound<'a, '_> for Cow<'a, str> { fn from_py_object_bound(ob: crate::Borrowed<'a, '_, PyAny>) -> PyResult { ob.downcast::()?.to_cow() diff --git a/src/err/mod.rs b/src/err/mod.rs index 205145d4e15..ad7d9b20dde 100644 --- a/src/err/mod.rs +++ b/src/err/mod.rs @@ -91,7 +91,6 @@ impl<'a, 'py> DowncastError<'a, 'py> { to: to.into(), } } - #[cfg(not(feature = "gil-refs"))] pub(crate) fn new_from_borrowed( from: Borrowed<'a, 'py, PyAny>, to: impl Into>, diff --git a/src/instance.rs b/src/instance.rs index d80072afa98..4bbdbbb184e 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -650,7 +650,6 @@ impl<'a, 'py> Borrowed<'a, 'py, PyAny> { } #[inline] - #[cfg(not(feature = "gil-refs"))] pub(crate) fn downcast(self) -> Result, DowncastError<'a, 'py>> where T: PyTypeCheck, @@ -992,7 +991,7 @@ where /// /// Get access to `&PyList` from `Py`: /// - /// ``` + /// ```ignore /// # use pyo3::prelude::*; /// # use pyo3::types::PyList; /// # diff --git a/src/internal_tricks.rs b/src/internal_tricks.rs index 0ee424f9db4..97b13aff2a8 100644 --- a/src/internal_tricks.rs +++ b/src/internal_tricks.rs @@ -36,154 +36,6 @@ pub(crate) fn get_ssize_index(index: usize) -> Py_ssize_t { index.min(PY_SSIZE_T_MAX as usize) as Py_ssize_t } -/// Implementations used for slice indexing PySequence, PyTuple, and PyList -#[cfg(feature = "gil-refs")] -macro_rules! index_impls { - ( - $ty:ty, - $ty_name:literal, - $len:expr, - $get_slice:expr $(,)? - ) => { - impl std::ops::Index for $ty { - // Always PyAny output (even if the slice operation returns something else) - type Output = PyAny; - - #[track_caller] - fn index(&self, index: usize) -> &Self::Output { - self.get_item(index).unwrap_or_else(|_| { - crate::internal_tricks::index_len_fail(index, $ty_name, $len(self)) - }) - } - } - - impl std::ops::Index> for $ty { - type Output = $ty; - - #[track_caller] - fn index( - &self, - std::ops::Range { start, end }: std::ops::Range, - ) -> &Self::Output { - let len = $len(self); - if start > len { - crate::internal_tricks::slice_start_index_len_fail(start, $ty_name, len) - } else if end > len { - crate::internal_tricks::slice_end_index_len_fail(end, $ty_name, len) - } else if start > end { - crate::internal_tricks::slice_index_order_fail(start, end) - } else { - $get_slice(self, start, end) - } - } - } - - impl std::ops::Index> for $ty { - type Output = $ty; - - #[track_caller] - fn index( - &self, - std::ops::RangeFrom { start }: std::ops::RangeFrom, - ) -> &Self::Output { - let len = $len(self); - if start > len { - crate::internal_tricks::slice_start_index_len_fail(start, $ty_name, len) - } else { - $get_slice(self, start, len) - } - } - } - - impl std::ops::Index for $ty { - type Output = $ty; - - #[track_caller] - fn index(&self, _: std::ops::RangeFull) -> &Self::Output { - let len = $len(self); - $get_slice(self, 0, len) - } - } - - impl std::ops::Index> for $ty { - type Output = $ty; - - #[track_caller] - fn index(&self, range: std::ops::RangeInclusive) -> &Self::Output { - let exclusive_end = range - .end() - .checked_add(1) - .expect("range end exceeds Python limit"); - &self[*range.start()..exclusive_end] - } - } - - impl std::ops::Index> for $ty { - type Output = $ty; - - #[track_caller] - fn index(&self, std::ops::RangeTo { end }: std::ops::RangeTo) -> &Self::Output { - &self[0..end] - } - } - - impl std::ops::Index> for $ty { - type Output = $ty; - - #[track_caller] - fn index( - &self, - std::ops::RangeToInclusive { end }: std::ops::RangeToInclusive, - ) -> &Self::Output { - &self[0..=end] - } - } - }; -} - -// these error messages are shamelessly "borrowed" from std. - -#[inline(never)] -#[cold] -#[track_caller] -#[cfg(feature = "gil-refs")] -pub(crate) fn index_len_fail(index: usize, ty_name: &str, len: usize) -> ! { - panic!( - "index {} out of range for {} of length {}", - index, ty_name, len - ); -} - -#[inline(never)] -#[cold] -#[track_caller] -#[cfg(feature = "gil-refs")] -pub(crate) fn slice_start_index_len_fail(index: usize, ty_name: &str, len: usize) -> ! { - panic!( - "range start index {} out of range for {} of length {}", - index, ty_name, len - ); -} - -#[inline(never)] -#[cold] -#[track_caller] -#[cfg(feature = "gil-refs")] -pub(crate) fn slice_end_index_len_fail(index: usize, ty_name: &str, len: usize) -> ! { - panic!( - "range end index {} out of range for {} of length {}", - index, ty_name, len - ); -} - -#[inline(never)] -#[cold] -#[track_caller] -#[cfg(feature = "gil-refs")] -pub(crate) fn slice_index_order_fail(index: usize, end: usize) -> ! { - panic!("slice index starts at {} but ends at {}", index, end); -} - // TODO: use ptr::from_ref on MSRV 1.76 #[inline] pub(crate) const fn ptr_from_ref(t: &T) -> *const T { diff --git a/src/types/boolobject.rs b/src/types/boolobject.rs index 9ac66529e9a..4da827de6a8 100644 --- a/src/types/boolobject.rs +++ b/src/types/boolobject.rs @@ -1,7 +1,5 @@ #[cfg(feature = "experimental-inspect")] use crate::inspect::types::TypeInfo; -#[cfg(feature = "gil-refs")] -use crate::PyNativeType; use crate::{ exceptions::PyTypeError, ffi, ffi_ptr_ext::FfiPtrExt, instance::Bound, types::typeobject::PyTypeMethods, Borrowed, FromPyObject, IntoPy, PyAny, PyObject, PyResult, @@ -38,28 +36,6 @@ impl PyBool { } } -#[cfg(feature = "gil-refs")] -impl PyBool { - /// Deprecated form of [`PyBool::new_bound`] - #[deprecated( - since = "0.21.0", - note = "`PyBool::new` will be replaced by `PyBool::new_bound` in a future PyO3 version" - )] - #[inline] - pub fn new(py: Python<'_>, val: bool) -> &PyBool { - #[allow(deprecated)] - unsafe { - py.from_borrowed_ptr(if val { ffi::Py_True() } else { ffi::Py_False() }) - } - } - - /// Gets whether this boolean is `true`. - #[inline] - pub fn is_true(&self) -> bool { - self.as_borrowed().is_true() - } -} - /// Implementation of functionality for [`PyBool`]. /// /// These methods are defined for the `Bound<'py, PyBool>` smart pointer, so to use method call diff --git a/src/types/bytearray.rs b/src/types/bytearray.rs index 57376069355..f93fc791865 100644 --- a/src/types/bytearray.rs +++ b/src/types/bytearray.rs @@ -94,200 +94,6 @@ impl PyByteArray { } } -#[cfg(feature = "gil-refs")] -impl PyByteArray { - /// Deprecated form of [`PyByteArray::new_bound`] - #[deprecated( - since = "0.21.0", - note = "`PyByteArray::new` will be replaced by `PyByteArray::new_bound` in a future PyO3 version" - )] - pub fn new<'py>(py: Python<'py>, src: &[u8]) -> &'py PyByteArray { - Self::new_bound(py, src).into_gil_ref() - } - - /// Deprecated form of [`PyByteArray::new_bound_with`] - #[deprecated( - since = "0.21.0", - note = "`PyByteArray::new_with` will be replaced by `PyByteArray::new_bound_with` in a future PyO3 version" - )] - pub fn new_with(py: Python<'_>, len: usize, init: F) -> PyResult<&PyByteArray> - where - F: FnOnce(&mut [u8]) -> PyResult<()>, - { - Self::new_bound_with(py, len, init).map(Bound::into_gil_ref) - } - - /// Deprecated form of [`PyByteArray::from_bound`] - #[deprecated( - since = "0.21.0", - note = "`PyByteArray::from` will be replaced by `PyByteArray::from_bound` in a future PyO3 version" - )] - pub fn from(src: &PyAny) -> PyResult<&PyByteArray> { - PyByteArray::from_bound(&src.as_borrowed()).map(Bound::into_gil_ref) - } - - /// Gets the length of the bytearray. - #[inline] - pub fn len(&self) -> usize { - self.as_borrowed().len() - } - - /// Checks if the bytearray is empty. - pub fn is_empty(&self) -> bool { - self.as_borrowed().is_empty() - } - - /// Gets the start of the buffer containing the contents of the bytearray. - /// - /// # Safety - /// - /// See the safety requirements of [`PyByteArray::as_bytes`] and [`PyByteArray::as_bytes_mut`]. - pub fn data(&self) -> *mut u8 { - self.as_borrowed().data() - } - - /// Extracts a slice of the `ByteArray`'s entire buffer. - /// - /// # Safety - /// - /// Mutation of the `bytearray` invalidates the slice. If it is used afterwards, the behavior is - /// undefined. - /// - /// These mutations may occur in Python code as well as from Rust: - /// - Calling methods like [`PyByteArray::as_bytes_mut`] and [`PyByteArray::resize`] will - /// invalidate the slice. - /// - Actions like dropping objects or raising exceptions can invoke `__del__`methods or signal - /// handlers, which may execute arbitrary Python code. This means that if Python code has a - /// reference to the `bytearray` you cannot safely use the vast majority of PyO3's API whilst - /// using the slice. - /// - /// As a result, this slice should only be used for short-lived operations without executing any - /// Python code, such as copying into a Vec. - /// - /// # Examples - /// - /// ```rust - /// use pyo3::prelude::*; - /// use pyo3::exceptions::PyRuntimeError; - /// use pyo3::types::PyByteArray; - /// - /// #[pyfunction] - /// fn a_valid_function(bytes: &Bound<'_, PyByteArray>) -> PyResult<()> { - /// let section = { - /// // SAFETY: We promise to not let the interpreter regain control - /// // or invoke any PyO3 APIs while using the slice. - /// let slice = unsafe { bytes.as_bytes() }; - /// - /// // Copy only a section of `bytes` while avoiding - /// // `to_vec` which copies the entire thing. - /// let section = slice - /// .get(6..11) - /// .ok_or_else(|| PyRuntimeError::new_err("input is not long enough"))?; - /// Vec::from(section) - /// }; - /// - /// // Now we can do things with `section` and call PyO3 APIs again. - /// // ... - /// # assert_eq!(§ion, b"world"); - /// - /// Ok(()) - /// } - /// # fn main() -> PyResult<()> { - /// # Python::with_gil(|py| -> PyResult<()> { - /// # let fun = wrap_pyfunction_bound!(a_valid_function, py)?; - /// # let locals = pyo3::types::PyDict::new_bound(py); - /// # locals.set_item("a_valid_function", fun)?; - /// # - /// # py.run_bound( - /// # r#"b = bytearray(b"hello world") - /// # a_valid_function(b) - /// # - /// # try: - /// # a_valid_function(bytearray()) - /// # except RuntimeError as e: - /// # assert str(e) == 'input is not long enough'"#, - /// # None, - /// # Some(&locals), - /// # )?; - /// # - /// # Ok(()) - /// # }) - /// # } - /// ``` - /// - /// # Incorrect usage - /// - /// The following `bug` function is unsound ⚠️ - /// - /// ```rust,no_run - /// # use pyo3::prelude::*; - /// # use pyo3::types::PyByteArray; - /// - /// # #[allow(dead_code)] - /// #[pyfunction] - /// fn bug(py: Python<'_>, bytes: &Bound<'_, PyByteArray>) { - /// let slice = unsafe { bytes.as_bytes() }; - /// - /// // This explicitly yields control back to the Python interpreter... - /// // ...but it's not always this obvious. Many things do this implicitly. - /// py.allow_threads(|| { - /// // Python code could be mutating through its handle to `bytes`, - /// // which makes reading it a data race, which is undefined behavior. - /// println!("{:?}", slice[0]); - /// }); - /// - /// // Python code might have mutated it, so we can not rely on the slice - /// // remaining valid. As such this is also undefined behavior. - /// println!("{:?}", slice[0]); - /// } - /// ``` - pub unsafe fn as_bytes(&self) -> &[u8] { - self.as_borrowed().as_bytes() - } - - /// Extracts a mutable slice of the `ByteArray`'s entire buffer. - /// - /// # Safety - /// - /// Any other accesses of the `bytearray`'s buffer invalidate the slice. If it is used - /// afterwards, the behavior is undefined. The safety requirements of [`PyByteArray::as_bytes`] - /// apply to this function as well. - #[allow(clippy::mut_from_ref)] - pub unsafe fn as_bytes_mut(&self) -> &mut [u8] { - self.as_borrowed().as_bytes_mut() - } - - /// Copies the contents of the bytearray to a Rust vector. - /// - /// # Examples - /// - /// ``` - /// # use pyo3::prelude::*; - /// # use pyo3::types::PyByteArray; - /// # Python::with_gil(|py| { - /// let bytearray = PyByteArray::new_bound(py, b"Hello World."); - /// let mut copied_message = bytearray.to_vec(); - /// assert_eq!(b"Hello World.", copied_message.as_slice()); - /// - /// copied_message[11] = b'!'; - /// assert_eq!(b"Hello World!", copied_message.as_slice()); - /// - /// pyo3::py_run!(py, bytearray, "assert bytearray == b'Hello World.'"); - /// # }); - /// ``` - pub fn to_vec(&self) -> Vec { - self.as_borrowed().to_vec() - } - - /// Resizes the bytearray object to the new length `len`. - /// - /// Note that this will invalidate any pointers obtained by [PyByteArray::data], as well as - /// any (unsafe) slices obtained from [PyByteArray::as_bytes] and [PyByteArray::as_bytes_mut]. - pub fn resize(&self, len: usize) -> PyResult<()> { - self.as_borrowed().resize(len) - } -} - /// Implementation of functionality for [`PyByteArray`]. /// /// These methods are defined for the `Bound<'py, PyByteArray>` smart pointer, so to use method call diff --git a/src/types/bytes.rs b/src/types/bytes.rs index 512c835f87a..0a8b4860d25 100644 --- a/src/types/bytes.rs +++ b/src/types/bytes.rs @@ -1,8 +1,6 @@ use crate::ffi_ptr_ext::FfiPtrExt; use crate::instance::{Borrowed, Bound}; use crate::types::any::PyAnyMethods; -#[cfg(feature = "gil-refs")] -use crate::PyNativeType; use crate::{ffi, Py, PyAny, PyResult, Python}; use std::ops::Index; use std::slice::SliceIndex; @@ -125,48 +123,6 @@ impl PyBytes { } } -#[cfg(feature = "gil-refs")] -impl PyBytes { - /// Deprecated form of [`PyBytes::new_bound`]. - #[deprecated( - since = "0.21.0", - note = "`PyBytes::new` will be replaced by `PyBytes::new_bound` in a future PyO3 version" - )] - pub fn new<'p>(py: Python<'p>, s: &[u8]) -> &'p PyBytes { - Self::new_bound(py, s).into_gil_ref() - } - - /// Deprecated form of [`PyBytes::new_bound_with`]. - #[deprecated( - since = "0.21.0", - note = "`PyBytes::new_with` will be replaced by `PyBytes::new_bound_with` in a future PyO3 version" - )] - pub fn new_with(py: Python<'_>, len: usize, init: F) -> PyResult<&PyBytes> - where - F: FnOnce(&mut [u8]) -> PyResult<()>, - { - Self::new_bound_with(py, len, init).map(Bound::into_gil_ref) - } - - /// Deprecated form of [`PyBytes::bound_from_ptr`]. - /// - /// # Safety - /// See [`PyBytes::bound_from_ptr`]. - #[deprecated( - since = "0.21.0", - note = "`PyBytes::from_ptr` will be replaced by `PyBytes::bound_from_ptr` in a future PyO3 version" - )] - pub unsafe fn from_ptr(py: Python<'_>, ptr: *const u8, len: usize) -> &PyBytes { - Self::bound_from_ptr(py, ptr, len).into_gil_ref() - } - - /// Gets the Python string as a byte slice. - #[inline] - pub fn as_bytes(&self) -> &[u8] { - self.as_borrowed().as_bytes() - } -} - /// Implementation of functionality for [`PyBytes`]. /// /// These methods are defined for the `Bound<'py, PyBytes>` smart pointer, so to use method call @@ -207,16 +163,6 @@ impl Py { } } -/// This is the same way [Vec] is indexed. -#[cfg(feature = "gil-refs")] -impl> Index for PyBytes { - type Output = I::Output; - - fn index(&self, index: I) -> &Self::Output { - &self.as_bytes()[index] - } -} - /// This is the same way [Vec] is indexed. impl> Index for Bound<'_, PyBytes> { type Output = I::Output; diff --git a/src/types/capsule.rs b/src/types/capsule.rs index 815b70ebc41..63cef1bed17 100644 --- a/src/types/capsule.rs +++ b/src/types/capsule.rs @@ -1,7 +1,5 @@ use crate::ffi_ptr_ext::FfiPtrExt; use crate::py_result_ext::PyResultExt; -#[cfg(feature = "gil-refs")] -use crate::PyNativeType; use crate::{ffi, PyAny}; use crate::{Bound, Python}; use crate::{PyErr, PyResult}; @@ -148,118 +146,6 @@ impl PyCapsule { } } -#[cfg(feature = "gil-refs")] -impl PyCapsule { - /// Deprecated form of [`PyCapsule::new_bound`]. - #[deprecated( - since = "0.21.0", - note = "`PyCapsule::new` will be replaced by `PyCapsule::new_bound` in a future PyO3 version" - )] - pub fn new( - py: Python<'_>, - value: T, - name: Option, - ) -> PyResult<&Self> { - Self::new_bound(py, value, name).map(Bound::into_gil_ref) - } - - /// Deprecated form of [`PyCapsule::new_bound_with_destructor`]. - #[deprecated( - since = "0.21.0", - note = "`PyCapsule::new_with_destructor` will be replaced by `PyCapsule::new_bound_with_destructor` in a future PyO3 version" - )] - pub fn new_with_destructor< - T: 'static + Send + AssertNotZeroSized, - F: FnOnce(T, *mut c_void) + Send, - >( - py: Python<'_>, - value: T, - name: Option, - destructor: F, - ) -> PyResult<&'_ Self> { - Self::new_bound_with_destructor(py, value, name, destructor).map(Bound::into_gil_ref) - } - - /// Sets the context pointer in the capsule. - /// - /// Returns an error if this capsule is not valid. - /// - /// # Notes - /// - /// The context is treated much like the value of the capsule, but should likely act as - /// a place to store any state management when using the capsule. - /// - /// If you want to store a Rust value as the context, and drop it from the destructor, use - /// `Box::into_raw` to convert it into a pointer, see the example. - /// - /// # Example - /// - /// ``` - /// use std::sync::mpsc::{channel, Sender}; - /// use libc::c_void; - /// use pyo3::{prelude::*, types::PyCapsule}; - /// - /// let (tx, rx) = channel::(); - /// - /// fn destructor(val: u32, context: *mut c_void) { - /// let ctx = unsafe { *Box::from_raw(context.cast::>()) }; - /// ctx.send("Destructor called!".to_string()).unwrap(); - /// } - /// - /// Python::with_gil(|py| { - /// let capsule = - /// PyCapsule::new_bound_with_destructor(py, 123, None, destructor as fn(u32, *mut c_void)) - /// .unwrap(); - /// let context = Box::new(tx); // `Sender` is our context, box it up and ship it! - /// capsule.set_context(Box::into_raw(context).cast()).unwrap(); - /// // This scope will end, causing our destructor to be called... - /// }); - /// - /// assert_eq!(rx.recv(), Ok("Destructor called!".to_string())); - /// ``` - pub fn set_context(&self, context: *mut c_void) -> PyResult<()> { - self.as_borrowed().set_context(context) - } - - /// Gets the current context stored in the capsule. If there is no context, the pointer - /// will be null. - /// - /// Returns an error if this capsule is not valid. - pub fn context(&self) -> PyResult<*mut c_void> { - self.as_borrowed().context() - } - - /// Obtains a reference to the value of this capsule. - /// - /// # Safety - /// - /// It must be known that this capsule is valid and its pointer is to an item of type `T`. - pub unsafe fn reference(&self) -> &T { - self.as_borrowed().reference() - } - - /// Gets the raw `c_void` pointer to the value in this capsule. - /// - /// Returns null if this capsule is not valid. - pub fn pointer(&self) -> *mut c_void { - self.as_borrowed().pointer() - } - - /// Checks if this is a valid capsule. - /// - /// Returns true if the stored `pointer()` is non-null. - pub fn is_valid(&self) -> bool { - self.as_borrowed().is_valid() - } - - /// Retrieves the name of this capsule, if set. - /// - /// Returns an error if this capsule is not valid. - pub fn name(&self) -> PyResult> { - self.as_borrowed().name() - } -} - /// Implementation of functionality for [`PyCapsule`]. /// /// These methods are defined for the `Bound<'py, PyCapsule>` smart pointer, so to use method call diff --git a/src/types/complex.rs b/src/types/complex.rs index 887bc12e438..0f16cac73ad 100644 --- a/src/types/complex.rs +++ b/src/types/complex.rs @@ -1,6 +1,7 @@ #[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))] use crate::py_result_ext::PyResultExt; #[cfg(feature = "gil-refs")] +#[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))] use crate::PyNativeType; use crate::{ffi, types::any::PyAnyMethods, Bound, PyAny, Python}; use std::os::raw::c_double; @@ -44,27 +45,6 @@ impl PyComplex { } } -#[cfg(feature = "gil-refs")] -impl PyComplex { - /// Deprecated form of [`PyComplex::from_doubles_bound`] - #[deprecated( - since = "0.21.0", - note = "`PyComplex::from_doubles` will be replaced by `PyComplex::from_doubles_bound` in a future PyO3 version" - )] - pub fn from_doubles(py: Python<'_>, real: c_double, imag: c_double) -> &PyComplex { - Self::from_doubles_bound(py, real, imag).into_gil_ref() - } - - /// Returns the real part of the complex number. - pub fn real(&self) -> c_double { - self.as_borrowed().real() - } - /// Returns the imaginary part of the complex number. - pub fn imag(&self) -> c_double { - self.as_borrowed().imag() - } -} - #[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))] mod not_limited_impls { use crate::Borrowed; @@ -72,18 +52,6 @@ mod not_limited_impls { use super::*; use std::ops::{Add, Div, Mul, Neg, Sub}; - #[cfg(feature = "gil-refs")] - impl PyComplex { - /// Returns `|self|`. - pub fn abs(&self) -> c_double { - self.as_borrowed().abs() - } - /// Returns `self` raised to the power of `other`. - pub fn pow<'py>(&'py self, other: &'py PyComplex) -> &'py PyComplex { - self.as_borrowed().pow(&other.as_borrowed()).into_gil_ref() - } - } - macro_rules! bin_ops { ($trait:ident, $fn:ident, $op:tt) => { impl<'py> $trait for Borrowed<'_, 'py, PyComplex> { diff --git a/src/types/datetime.rs b/src/types/datetime.rs index c07b6be9b2b..8743bd7a680 100644 --- a/src/types/datetime.rs +++ b/src/types/datetime.rs @@ -206,16 +206,6 @@ pyobject_native_type!( ); impl PyDate { - /// Deprecated form of [`PyDate::new_bound`]. - #[cfg(feature = "gil-refs")] - #[deprecated( - since = "0.21.0", - note = "`PyDate::new` will be replaced by `PyDate::new_bound` in a future PyO3 version" - )] - pub fn new(py: Python<'_>, year: i32, month: u8, day: u8) -> PyResult<&PyDate> { - Self::new_bound(py, year, month, day).map(Bound::into_gil_ref) - } - /// Creates a new `datetime.date`. pub fn new_bound(py: Python<'_>, year: i32, month: u8, day: u8) -> PyResult> { let api = ensure_datetime_api(py)?; @@ -226,16 +216,6 @@ impl PyDate { } } - /// Deprecated form of [`PyDate::from_timestamp_bound`]. - #[cfg(feature = "gil-refs")] - #[deprecated( - since = "0.21.0", - note = "`PyDate::from_timestamp` will be replaced by `PyDate::from_timestamp_bound` in a future PyO3 version" - )] - pub fn from_timestamp(py: Python<'_>, timestamp: i64) -> PyResult<&PyDate> { - Self::from_timestamp_bound(py, timestamp).map(Bound::into_gil_ref) - } - /// Construct a `datetime.date` from a POSIX timestamp /// /// This is equivalent to `datetime.date.fromtimestamp` @@ -297,38 +277,6 @@ pyobject_native_type!( ); impl PyDateTime { - /// Deprecated form of [`PyDateTime::new_bound`]. - #[cfg(feature = "gil-refs")] - #[deprecated( - since = "0.21.0", - note = "`PyDateTime::new` will be replaced by `PyDateTime::new_bound` in a future PyO3 version" - )] - #[allow(clippy::too_many_arguments)] - pub fn new<'py>( - py: Python<'py>, - year: i32, - month: u8, - day: u8, - hour: u8, - minute: u8, - second: u8, - microsecond: u32, - tzinfo: Option<&'py PyTzInfo>, - ) -> PyResult<&'py PyDateTime> { - Self::new_bound( - py, - year, - month, - day, - hour, - minute, - second, - microsecond, - tzinfo.map(PyTzInfo::as_borrowed).as_deref(), - ) - .map(Bound::into_gil_ref) - } - /// Creates a new `datetime.datetime` object. #[allow(clippy::too_many_arguments)] pub fn new_bound<'py>( @@ -360,40 +308,6 @@ impl PyDateTime { } } - /// Deprecated form of [`PyDateTime::new_bound_with_fold`]. - #[cfg(feature = "gil-refs")] - #[deprecated( - since = "0.21.0", - note = "`PyDateTime::new_with_fold` will be replaced by `PyDateTime::new_bound_with_fold` in a future PyO3 version" - )] - #[allow(clippy::too_many_arguments)] - pub fn new_with_fold<'py>( - py: Python<'py>, - year: i32, - month: u8, - day: u8, - hour: u8, - minute: u8, - second: u8, - microsecond: u32, - tzinfo: Option<&'py PyTzInfo>, - fold: bool, - ) -> PyResult<&'py PyDateTime> { - Self::new_bound_with_fold( - py, - year, - month, - day, - hour, - minute, - second, - microsecond, - tzinfo.map(PyTzInfo::as_borrowed).as_deref(), - fold, - ) - .map(Bound::into_gil_ref) - } - /// Alternate constructor that takes a `fold` parameter. A `true` value for this parameter /// signifies this this datetime is the later of two moments with the same representation, /// during a repeated interval. @@ -433,21 +347,6 @@ impl PyDateTime { } } - /// Deprecated form of [`PyDateTime::from_timestamp_bound`]. - #[cfg(feature = "gil-refs")] - #[deprecated( - since = "0.21.0", - note = "`PyDateTime::from_timestamp` will be replaced by `PyDateTime::from_timestamp_bound` in a future PyO3 version" - )] - pub fn from_timestamp<'py>( - py: Python<'py>, - timestamp: f64, - tzinfo: Option<&'py PyTzInfo>, - ) -> PyResult<&'py PyDateTime> { - Self::from_timestamp_bound(py, timestamp, tzinfo.map(PyTzInfo::as_borrowed).as_deref()) - .map(Bound::into_gil_ref) - } - /// Construct a `datetime` object from a POSIX timestamp /// /// This is equivalent to `datetime.datetime.fromtimestamp` @@ -599,31 +498,6 @@ pyobject_native_type!( ); impl PyTime { - /// Deprecated form of [`PyTime::new_bound`]. - #[cfg(feature = "gil-refs")] - #[deprecated( - since = "0.21.0", - note = "`PyTime::new` will be replaced by `PyTime::new_bound` in a future PyO3 version" - )] - pub fn new<'py>( - py: Python<'py>, - hour: u8, - minute: u8, - second: u8, - microsecond: u32, - tzinfo: Option<&'py PyTzInfo>, - ) -> PyResult<&'py PyTime> { - Self::new_bound( - py, - hour, - minute, - second, - microsecond, - tzinfo.map(PyTzInfo::as_borrowed).as_deref(), - ) - .map(Bound::into_gil_ref) - } - /// Creates a new `datetime.time` object. pub fn new_bound<'py>( py: Python<'py>, @@ -648,33 +522,6 @@ impl PyTime { } } - /// Deprecated form of [`PyTime::new_bound_with_fold`]. - #[cfg(feature = "gil-refs")] - #[deprecated( - since = "0.21.0", - note = "`PyTime::new_with_fold` will be replaced by `PyTime::new_bound_with_fold` in a future PyO3 version" - )] - pub fn new_with_fold<'py>( - py: Python<'py>, - hour: u8, - minute: u8, - second: u8, - microsecond: u32, - tzinfo: Option<&'py PyTzInfo>, - fold: bool, - ) -> PyResult<&'py PyTime> { - Self::new_bound_with_fold( - py, - hour, - minute, - second, - microsecond, - tzinfo.map(PyTzInfo::as_borrowed).as_deref(), - fold, - ) - .map(Bound::into_gil_ref) - } - /// Alternate constructor that takes a `fold` argument. See [`PyDateTime::new_bound_with_fold`]. pub fn new_bound_with_fold<'py>( py: Python<'py>, @@ -806,16 +653,6 @@ pyobject_native_type!( #checkfunction=PyTZInfo_Check ); -/// Deprecated form of [`timezone_utc_bound`]. -#[cfg(feature = "gil-refs")] -#[deprecated( - since = "0.21.0", - note = "`timezone_utc` will be replaced by `timezone_utc_bound` in a future PyO3 version" -)] -pub fn timezone_utc(py: Python<'_>) -> &PyTzInfo { - timezone_utc_bound(py).into_gil_ref() -} - /// Equivalent to `datetime.timezone.utc` pub fn timezone_utc_bound(py: Python<'_>) -> Bound<'_, PyTzInfo> { // TODO: this _could_ have a borrowed form `timezone_utc_borrowed`, but that seems @@ -861,22 +698,6 @@ pyobject_native_type!( ); impl PyDelta { - /// Deprecated form of [`PyDelta::new_bound`]. - #[cfg(feature = "gil-refs")] - #[deprecated( - since = "0.21.0", - note = "`PyDelta::new` will be replaced by `PyDelta::new_bound` in a future PyO3 version" - )] - pub fn new( - py: Python<'_>, - days: i32, - seconds: i32, - microseconds: i32, - normalize: bool, - ) -> PyResult<&PyDelta> { - Self::new_bound(py, days, seconds, microseconds, normalize).map(Bound::into_gil_ref) - } - /// Creates a new `timedelta`. pub fn new_bound( py: Python<'_>, diff --git a/src/types/dict.rs b/src/types/dict.rs index 001e8584028..3c731d909e0 100644 --- a/src/types/dict.rs +++ b/src/types/dict.rs @@ -6,8 +6,6 @@ use crate::instance::{Borrowed, Bound}; use crate::py_result_ext::PyResultExt; use crate::types::any::PyAnyMethods; use crate::types::{PyAny, PyList}; -#[cfg(feature = "gil-refs")] -use crate::PyNativeType; use crate::{ffi, Python, ToPyObject}; /// Represents a Python `dict`. @@ -87,195 +85,6 @@ impl PyDict { } } -#[cfg(feature = "gil-refs")] -impl PyDict { - /// Deprecated form of [`new_bound`][PyDict::new_bound]. - #[deprecated( - since = "0.21.0", - note = "`PyDict::new` will be replaced by `PyDict::new_bound` in a future PyO3 version" - )] - #[inline] - pub fn new(py: Python<'_>) -> &PyDict { - Self::new_bound(py).into_gil_ref() - } - - /// Deprecated form of [`from_sequence_bound`][PyDict::from_sequence_bound]. - #[deprecated( - since = "0.21.0", - note = "`PyDict::from_sequence` will be replaced by `PyDict::from_sequence_bound` in a future PyO3 version" - )] - #[inline] - #[cfg(not(any(PyPy, GraalPy)))] - pub fn from_sequence(seq: &PyAny) -> PyResult<&PyDict> { - Self::from_sequence_bound(&seq.as_borrowed()).map(Bound::into_gil_ref) - } - - /// Returns a new dictionary that contains the same key-value pairs as self. - /// - /// This is equivalent to the Python expression `self.copy()`. - pub fn copy(&self) -> PyResult<&PyDict> { - self.as_borrowed().copy().map(Bound::into_gil_ref) - } - - /// Empties an existing dictionary of all key-value pairs. - pub fn clear(&self) { - self.as_borrowed().clear() - } - - /// Return the number of items in the dictionary. - /// - /// This is equivalent to the Python expression `len(self)`. - pub fn len(&self) -> usize { - self.as_borrowed().len() - } - - /// Checks if the dict is empty, i.e. `len(self) == 0`. - pub fn is_empty(&self) -> bool { - self.as_borrowed().is_empty() - } - - /// Determines if the dictionary contains the specified key. - /// - /// This is equivalent to the Python expression `key in self`. - pub fn contains(&self, key: K) -> PyResult - where - K: ToPyObject, - { - self.as_borrowed().contains(key) - } - - /// Gets an item from the dictionary. - /// - /// Returns `Ok(None)` if the item is not present. To get a `KeyError` for - /// non-existing keys, use [`PyAny::get_item`]. - /// - /// Returns `Err(PyErr)` if Python magic methods `__hash__` or `__eq__` used in dictionary - /// lookup raise an exception, for example if the key `K` is not hashable. Usually it is - /// best to bubble this error up to the caller using the `?` operator. - /// - /// # Examples - /// - /// The following example calls `get_item` for the dictionary `{"a": 1}` with various - /// keys. - /// - `get_item("a")` returns `Ok(Some(...))`, with the `PyAny` being a reference to the Python - /// int `1`. - /// - `get_item("b")` returns `Ok(None)`, because "b" is not in the dictionary. - /// - `get_item(dict)` returns an `Err(PyErr)`. The error will be a `TypeError` because a dict is not - /// hashable. - /// - /// ```rust - /// use pyo3::prelude::*; - /// use pyo3::types::{IntoPyDict}; - /// use pyo3::exceptions::{PyTypeError, PyKeyError}; - /// - /// # fn main() { - /// # let _ = - /// Python::with_gil(|py| -> PyResult<()> { - /// let dict = &[("a", 1)].into_py_dict_bound(py); - /// // `a` is in the dictionary, with value 1 - /// assert!(dict.get_item("a")?.map_or(Ok(false), |x| x.eq(1))?); - /// // `b` is not in the dictionary - /// assert!(dict.get_item("b")?.is_none()); - /// // `dict` is not hashable, so this returns an error - /// assert!(dict.get_item(dict).unwrap_err().is_instance_of::(py)); - /// - /// // `PyAny::get_item("b")` will raise a `KeyError` instead of returning `None` - /// let any = dict.as_any(); - /// assert!(any.get_item("b").unwrap_err().is_instance_of::(py)); - /// Ok(()) - /// }); - /// # } - /// ``` - pub fn get_item(&self, key: K) -> PyResult> - where - K: ToPyObject, - { - match self.as_borrowed().get_item(key) { - Ok(Some(item)) => Ok(Some(item.into_gil_ref())), - Ok(None) => Ok(None), - Err(e) => Err(e), - } - } - - /// Sets an item value. - /// - /// This is equivalent to the Python statement `self[key] = value`. - pub fn set_item(&self, key: K, value: V) -> PyResult<()> - where - K: ToPyObject, - V: ToPyObject, - { - self.as_borrowed().set_item(key, value) - } - - /// Deletes an item. - /// - /// This is equivalent to the Python statement `del self[key]`. - pub fn del_item(&self, key: K) -> PyResult<()> - where - K: ToPyObject, - { - self.as_borrowed().del_item(key) - } - - /// Returns a list of dict keys. - /// - /// This is equivalent to the Python expression `list(dict.keys())`. - pub fn keys(&self) -> &PyList { - self.as_borrowed().keys().into_gil_ref() - } - - /// Returns a list of dict values. - /// - /// This is equivalent to the Python expression `list(dict.values())`. - pub fn values(&self) -> &PyList { - self.as_borrowed().values().into_gil_ref() - } - - /// Returns a list of dict items. - /// - /// This is equivalent to the Python expression `list(dict.items())`. - pub fn items(&self) -> &PyList { - self.as_borrowed().items().into_gil_ref() - } - - /// Returns an iterator of `(key, value)` pairs in this dictionary. - /// - /// # Panics - /// - /// If PyO3 detects that the dictionary is mutated during iteration, it will panic. - /// It is allowed to modify values as you iterate over the dictionary, but only - /// so long as the set of keys does not change. - pub fn iter(&self) -> PyDictIterator<'_> { - PyDictIterator(self.as_borrowed().iter()) - } - - /// Returns `self` cast as a `PyMapping`. - pub fn as_mapping(&self) -> &PyMapping { - unsafe { self.downcast_unchecked() } - } - - /// Update this dictionary with the key/value pairs from another. - /// - /// This is equivalent to the Python expression `self.update(other)`. If `other` is a `PyDict`, you may want - /// to use `self.update(other.as_mapping())`, note: `PyDict::as_mapping` is a zero-cost conversion. - pub fn update(&self, other: &PyMapping) -> PyResult<()> { - self.as_borrowed().update(&other.as_borrowed()) - } - - /// Add key/value pairs from another dictionary to this one only when they do not exist in this. - /// - /// This is equivalent to the Python expression `self.update({k: v for k, v in other.items() if k not in self})`. - /// If `other` is a `PyDict`, you may want to use `self.update_if_missing(other.as_mapping())`, - /// note: `PyDict::as_mapping` is a zero-cost conversion. - /// - /// This method uses [`PyDict_Merge`](https://docs.python.org/3/c-api/dict.html#c.PyDict_Merge) internally, - /// so should have the same performance as `update`. - pub fn update_if_missing(&self, other: &PyMapping) -> PyResult<()> { - self.as_borrowed().update_if_missing(&other.as_borrowed()) - } -} - /// Implementation of functionality for [`PyDict`]. /// /// These methods are defined for the `Bound<'py, PyDict>` smart pointer, so to use method call @@ -545,43 +354,6 @@ fn dict_len(dict: &Bound<'_, PyDict>) -> Py_ssize_t { } } -/// PyO3 implementation of an iterator for a Python `dict` object. -#[cfg(feature = "gil-refs")] -pub struct PyDictIterator<'py>(BoundDictIterator<'py>); - -#[cfg(feature = "gil-refs")] -impl<'py> Iterator for PyDictIterator<'py> { - type Item = (&'py PyAny, &'py PyAny); - - #[inline] - fn next(&mut self) -> Option { - let (key, value) = self.0.next()?; - Some((key.into_gil_ref(), value.into_gil_ref())) - } - - #[inline] - fn size_hint(&self) -> (usize, Option) { - self.0.size_hint() - } -} - -#[cfg(feature = "gil-refs")] -impl<'py> ExactSizeIterator for PyDictIterator<'py> { - fn len(&self) -> usize { - self.0.len() - } -} - -#[cfg(feature = "gil-refs")] -impl<'a> IntoIterator for &'a PyDict { - type Item = (&'a PyAny, &'a PyAny); - type IntoIter = PyDictIterator<'a>; - - fn into_iter(self) -> Self::IntoIter { - self.iter() - } -} - /// PyO3 implementation of an iterator for a Python `dict` object. pub struct BoundDictIterator<'py> { dict: Bound<'py, PyDict>, diff --git a/src/types/ellipsis.rs b/src/types/ellipsis.rs index 1dc7da08b55..887016a0ae8 100644 --- a/src/types/ellipsis.rs +++ b/src/types/ellipsis.rs @@ -14,17 +14,6 @@ pyobject_native_type_named!(PyEllipsis); pyobject_native_type_extract!(PyEllipsis); impl PyEllipsis { - /// Returns the `Ellipsis` object. - #[cfg(feature = "gil-refs")] - #[deprecated( - since = "0.21.0", - note = "`PyEllipsis::get` will be replaced by `PyEllipsis::get_bound` in a future PyO3 version" - )] - #[inline] - pub fn get(py: Python<'_>) -> &PyEllipsis { - Self::get_bound(py).into_gil_ref() - } - /// Returns the `Ellipsis` object. #[inline] pub fn get_bound(py: Python<'_>) -> Borrowed<'_, '_, PyEllipsis> { diff --git a/src/types/float.rs b/src/types/float.rs index 1e6cbe51eaf..0a981d60cec 100644 --- a/src/types/float.rs +++ b/src/types/float.rs @@ -1,8 +1,6 @@ use super::any::PyAnyMethods; #[cfg(feature = "experimental-inspect")] use crate::inspect::types::TypeInfo; -#[cfg(feature = "gil-refs")] -use crate::PyNativeType; use crate::{ ffi, ffi_ptr_ext::FfiPtrExt, instance::Bound, FromPyObject, IntoPy, PyAny, PyErr, PyObject, PyResult, Python, ToPyObject, @@ -41,24 +39,6 @@ impl PyFloat { } } -#[cfg(feature = "gil-refs")] -impl PyFloat { - /// Deprecated form of [`PyFloat::new_bound`]. - #[inline] - #[deprecated( - since = "0.21.0", - note = "`PyFloat::new` will be replaced by `PyFloat::new_bound` in a future PyO3 version" - )] - pub fn new(py: Python<'_>, val: f64) -> &'_ Self { - Self::new_bound(py, val).into_gil_ref() - } - - /// Gets the value of this float. - pub fn value(&self) -> c_double { - self.as_borrowed().value() - } -} - /// Implementation of functionality for [`PyFloat`]. /// /// These methods are defined for the `Bound<'py, PyFloat>` smart pointer, so to use method call diff --git a/src/types/frozenset.rs b/src/types/frozenset.rs index 4ec83915c70..845309fe2be 100644 --- a/src/types/frozenset.rs +++ b/src/types/frozenset.rs @@ -40,16 +40,6 @@ impl<'py> PyFrozenSetBuilder<'py> { inner(&self.py_frozen_set, key.to_object(self.py_frozen_set.py())) } - /// Deprecated form of [`PyFrozenSetBuilder::finalize_bound`] - #[cfg(feature = "gil-refs")] - #[deprecated( - since = "0.21.0", - note = "`PyFrozenSetBuilder::finalize` will be replaced by `PyFrozenSetBuilder::finalize_bound` in a future PyO3 version" - )] - pub fn finalize(self) -> &'py PyFrozenSet { - self.finalize_bound().into_gil_ref() - } - /// Finish building the set and take ownership of its current value pub fn finalize_bound(self) -> Bound<'py, PyFrozenSet> { self.py_frozen_set @@ -103,57 +93,6 @@ impl PyFrozenSet { } } -#[cfg(feature = "gil-refs")] -impl PyFrozenSet { - /// Deprecated form of [`PyFrozenSet::new_bound`]. - #[inline] - #[deprecated( - since = "0.21.0", - note = "`PyFrozenSet::new` will be replaced by `PyFrozenSet::new_bound` in a future PyO3 version" - )] - pub fn new<'a, 'p, T: ToPyObject + 'a>( - py: Python<'p>, - elements: impl IntoIterator, - ) -> PyResult<&'p PyFrozenSet> { - Self::new_bound(py, elements).map(Bound::into_gil_ref) - } - - /// Deprecated form of [`PyFrozenSet::empty_bound`]. - #[deprecated( - since = "0.21.0", - note = "`PyFrozenSet::empty` will be replaced by `PyFrozenSet::empty_bound` in a future PyO3 version" - )] - pub fn empty(py: Python<'_>) -> PyResult<&'_ PyFrozenSet> { - Self::empty_bound(py).map(Bound::into_gil_ref) - } - - /// Return the number of items in the set. - /// This is equivalent to len(p) on a set. - #[inline] - pub fn len(&self) -> usize { - self.as_borrowed().len() - } - - /// Check if set is empty. - pub fn is_empty(&self) -> bool { - self.as_borrowed().is_empty() - } - - /// Determine if the set contains the specified key. - /// This is equivalent to the Python expression `key in self`. - pub fn contains(&self, key: K) -> PyResult - where - K: ToPyObject, - { - self.as_borrowed().contains(key) - } - - /// Returns an iterator of values in this frozen set. - pub fn iter(&self) -> PyFrozenSetIterator<'_> { - PyFrozenSetIterator(BoundFrozenSetIterator::new(self.as_borrowed().to_owned())) - } -} - /// Implementation of functionality for [`PyFrozenSet`]. /// /// These methods are defined for the `Bound<'py, PyFrozenSet>` smart pointer, so to use method call diff --git a/src/types/function.rs b/src/types/function.rs index 62a2b30263b..65c17984ad2 100644 --- a/src/types/function.rs +++ b/src/types/function.rs @@ -1,11 +1,7 @@ -#[cfg(feature = "gil-refs")] -use crate::derive_utils::PyFunctionArguments; use crate::ffi_ptr_ext::FfiPtrExt; use crate::py_result_ext::PyResultExt; use crate::types::capsule::PyCapsuleMethods; use crate::types::module::PyModuleMethods; -#[cfg(feature = "gil-refs")] -use crate::PyNativeType; use crate::{ ffi, impl_::pymethods::{self, PyMethodDef}, @@ -25,27 +21,6 @@ pub struct PyCFunction(PyAny); pyobject_native_type_core!(PyCFunction, pyobject_native_static_type_object!(ffi::PyCFunction_Type), #checkfunction=ffi::PyCFunction_Check); impl PyCFunction { - /// Deprecated form of [`PyCFunction::new_with_keywords_bound`] - #[cfg(feature = "gil-refs")] - #[deprecated( - since = "0.21.0", - note = "`PyCFunction::new_with_keywords` will be replaced by `PyCFunction::new_with_keywords_bound` in a future PyO3 version" - )] - pub fn new_with_keywords<'a>( - fun: ffi::PyCFunctionWithKeywords, - name: &'static CStr, - doc: &'static CStr, - py_or_module: PyFunctionArguments<'a>, - ) -> PyResult<&'a Self> { - let (py, module) = py_or_module.into_py_and_maybe_module(); - Self::internal_new( - py, - &PyMethodDef::cfunction_with_keywords(name, fun, doc), - module.map(PyNativeType::as_borrowed).as_deref(), - ) - .map(Bound::into_gil_ref) - } - /// Create a new built-in function with keywords (*args and/or **kwargs). /// /// To create `name` and `doc` static strings on Rust versions older than 1.77 (which added c"" literals), @@ -64,27 +39,6 @@ impl PyCFunction { ) } - /// Deprecated form of [`PyCFunction::new`] - #[cfg(feature = "gil-refs")] - #[deprecated( - since = "0.21.0", - note = "`PyCFunction::new` will be replaced by `PyCFunction::new_bound` in a future PyO3 version" - )] - pub fn new<'a>( - fun: ffi::PyCFunction, - name: &'static CStr, - doc: &'static CStr, - py_or_module: PyFunctionArguments<'a>, - ) -> PyResult<&'a Self> { - let (py, module) = py_or_module.into_py_and_maybe_module(); - Self::internal_new( - py, - &PyMethodDef::noargs(name, fun, doc), - module.map(PyNativeType::as_borrowed).as_deref(), - ) - .map(Bound::into_gil_ref) - } - /// Create a new built-in function which takes no arguments. /// /// To create `name` and `doc` static strings on Rust versions older than 1.77 (which added c"" literals), @@ -99,28 +53,6 @@ impl PyCFunction { Self::internal_new(py, &PyMethodDef::noargs(name, fun, doc), module) } - /// Deprecated form of [`PyCFunction::new_closure`] - #[cfg(feature = "gil-refs")] - #[deprecated( - since = "0.21.0", - note = "`PyCFunction::new_closure` will be replaced by `PyCFunction::new_closure_bound` in a future PyO3 version" - )] - pub fn new_closure<'a, F, R>( - py: Python<'a>, - name: Option<&'static CStr>, - doc: Option<&'static CStr>, - closure: F, - ) -> PyResult<&'a PyCFunction> - where - F: Fn(&PyTuple, Option<&PyDict>) -> R + Send + 'static, - R: crate::callback::IntoPyCallbackOutput<*mut ffi::PyObject>, - { - Self::new_closure_bound(py, name, doc, move |args, kwargs| { - closure(args.as_gil_ref(), kwargs.map(Bound::as_gil_ref)) - }) - .map(Bound::into_gil_ref) - } - /// Create a new function from a closure. /// /// # Examples diff --git a/src/types/iterator.rs b/src/types/iterator.rs index 1ae9f9f471a..5602e34f40e 100644 --- a/src/types/iterator.rs +++ b/src/types/iterator.rs @@ -34,16 +34,6 @@ pyobject_native_type_named!(PyIterator); pyobject_native_type_extract!(PyIterator); impl PyIterator { - /// Deprecated form of `PyIterator::from_bound_object`. - #[cfg(feature = "gil-refs")] - #[deprecated( - since = "0.21.0", - note = "`PyIterator::from_object` will be replaced by `PyIterator::from_bound_object` in a future PyO3 version" - )] - pub fn from_object(obj: &PyAny) -> PyResult<&PyIterator> { - Self::from_bound_object(&obj.as_borrowed()).map(Bound::into_gil_ref) - } - /// Builds an iterator for an iterable Python object; the equivalent of calling `iter(obj)` in Python. /// /// Usually it is more convenient to write [`obj.iter()`][crate::types::any::PyAnyMethods::iter], diff --git a/src/types/list.rs b/src/types/list.rs index 9cfd574cd0f..fa1eb876353 100644 --- a/src/types/list.rs +++ b/src/types/list.rs @@ -6,8 +6,6 @@ use crate::ffi_ptr_ext::FfiPtrExt; use crate::instance::Borrowed; use crate::internal_tricks::get_ssize_index; use crate::types::{PySequence, PyTuple}; -#[cfg(feature = "gil-refs")] -use crate::PyNativeType; use crate::{Bound, PyAny, PyObject, Python, ToPyObject}; use crate::types::any::PyAnyMethods; @@ -111,180 +109,6 @@ impl PyList { } } -#[cfg(feature = "gil-refs")] -impl PyList { - /// Deprecated form of [`PyList::new_bound`]. - #[inline] - #[track_caller] - #[deprecated( - since = "0.21.0", - note = "`PyList::new` will be replaced by `PyList::new_bound` in a future PyO3 version" - )] - pub fn new(py: Python<'_>, elements: impl IntoIterator) -> &PyList - where - T: ToPyObject, - U: ExactSizeIterator, - { - Self::new_bound(py, elements).into_gil_ref() - } - - /// Deprecated form of [`PyList::empty_bound`]. - #[inline] - #[deprecated( - since = "0.21.0", - note = "`PyList::empty` will be replaced by `PyList::empty_bound` in a future PyO3 version" - )] - pub fn empty(py: Python<'_>) -> &PyList { - Self::empty_bound(py).into_gil_ref() - } - - /// Returns the length of the list. - pub fn len(&self) -> usize { - self.as_borrowed().len() - } - - /// Checks if the list is empty. - pub fn is_empty(&self) -> bool { - self.as_borrowed().is_empty() - } - - /// Returns `self` cast as a `PySequence`. - pub fn as_sequence(&self) -> &PySequence { - unsafe { self.downcast_unchecked() } - } - - /// Gets the list item at the specified index. - /// # Example - /// ``` - /// use pyo3::{prelude::*, types::PyList}; - /// Python::with_gil(|py| { - /// let list = PyList::new_bound(py, [2, 3, 5, 7]); - /// let obj = list.get_item(0); - /// assert_eq!(obj.unwrap().extract::().unwrap(), 2); - /// }); - /// ``` - pub fn get_item(&self, index: usize) -> PyResult<&PyAny> { - self.as_borrowed().get_item(index).map(Bound::into_gil_ref) - } - - /// Gets the list item at the specified index. Undefined behavior on bad index. Use with caution. - /// - /// # Safety - /// - /// Caller must verify that the index is within the bounds of the list. - #[cfg(not(Py_LIMITED_API))] - pub unsafe fn get_item_unchecked(&self, index: usize) -> &PyAny { - self.as_borrowed().get_item_unchecked(index).into_gil_ref() - } - - /// Takes the slice `self[low:high]` and returns it as a new list. - /// - /// Indices must be nonnegative, and out-of-range indices are clipped to - /// `self.len()`. - pub fn get_slice(&self, low: usize, high: usize) -> &PyList { - self.as_borrowed().get_slice(low, high).into_gil_ref() - } - - /// Sets the item at the specified index. - /// - /// Raises `IndexError` if the index is out of range. - pub fn set_item(&self, index: usize, item: I) -> PyResult<()> - where - I: ToPyObject, - { - self.as_borrowed().set_item(index, item) - } - - /// Deletes the `index`th element of self. - /// - /// This is equivalent to the Python statement `del self[i]`. - #[inline] - pub fn del_item(&self, index: usize) -> PyResult<()> { - self.as_borrowed().del_item(index) - } - - /// Assigns the sequence `seq` to the slice of `self` from `low` to `high`. - /// - /// This is equivalent to the Python statement `self[low:high] = v`. - #[inline] - pub fn set_slice(&self, low: usize, high: usize, seq: &PyAny) -> PyResult<()> { - self.as_borrowed().set_slice(low, high, &seq.as_borrowed()) - } - - /// Deletes the slice from `low` to `high` from `self`. - /// - /// This is equivalent to the Python statement `del self[low:high]`. - #[inline] - pub fn del_slice(&self, low: usize, high: usize) -> PyResult<()> { - self.as_borrowed().del_slice(low, high) - } - - /// Appends an item to the list. - pub fn append(&self, item: I) -> PyResult<()> - where - I: ToPyObject, - { - self.as_borrowed().append(item) - } - - /// Inserts an item at the specified index. - /// - /// If `index >= self.len()`, inserts at the end. - pub fn insert(&self, index: usize, item: I) -> PyResult<()> - where - I: ToPyObject, - { - self.as_borrowed().insert(index, item) - } - - /// Determines if self contains `value`. - /// - /// This is equivalent to the Python expression `value in self`. - #[inline] - pub fn contains(&self, value: V) -> PyResult - where - V: ToPyObject, - { - self.as_borrowed().contains(value) - } - - /// Returns the first index `i` for which `self[i] == value`. - /// - /// This is equivalent to the Python expression `self.index(value)`. - #[inline] - pub fn index(&self, value: V) -> PyResult - where - V: ToPyObject, - { - self.as_borrowed().index(value) - } - - /// Returns an iterator over this list's items. - pub fn iter(&self) -> PyListIterator<'_> { - PyListIterator(self.as_borrowed().iter()) - } - - /// Sorts the list in-place. Equivalent to the Python expression `l.sort()`. - pub fn sort(&self) -> PyResult<()> { - self.as_borrowed().sort() - } - - /// Reverses the list in-place. Equivalent to the Python expression `l.reverse()`. - pub fn reverse(&self) -> PyResult<()> { - self.as_borrowed().reverse() - } - - /// Return a new tuple containing the contents of the list; equivalent to the Python expression `tuple(list)`. - /// - /// This method is equivalent to `self.as_sequence().to_tuple()` and faster than `PyTuple::new(py, this_list)`. - pub fn to_tuple(&self) -> &PyTuple { - self.as_borrowed().to_tuple().into_gil_ref() - } -} - -#[cfg(feature = "gil-refs")] -index_impls!(PyList, "list", PyList::len, PyList::get_slice); - /// Implementation of functionality for [`PyList`]. /// /// These methods are defined for the `Bound<'py, PyList>` smart pointer, so to use method call @@ -595,53 +419,6 @@ impl<'py> PyListMethods<'py> for Bound<'py, PyList> { } } -/// Used by `PyList::iter()`. -#[cfg(feature = "gil-refs")] -pub struct PyListIterator<'a>(BoundListIterator<'a>); - -#[cfg(feature = "gil-refs")] -impl<'a> Iterator for PyListIterator<'a> { - type Item = &'a PyAny; - - #[inline] - fn next(&mut self) -> Option { - self.0.next().map(Bound::into_gil_ref) - } - - #[inline] - fn size_hint(&self) -> (usize, Option) { - self.0.size_hint() - } -} - -#[cfg(feature = "gil-refs")] -impl<'a> DoubleEndedIterator for PyListIterator<'a> { - #[inline] - fn next_back(&mut self) -> Option { - self.0.next_back().map(Bound::into_gil_ref) - } -} - -#[cfg(feature = "gil-refs")] -impl<'a> ExactSizeIterator for PyListIterator<'a> { - fn len(&self) -> usize { - self.0.len() - } -} - -#[cfg(feature = "gil-refs")] -impl FusedIterator for PyListIterator<'_> {} - -#[cfg(feature = "gil-refs")] -impl<'a> IntoIterator for &'a PyList { - type Item = &'a PyAny; - type IntoIter = PyListIterator<'a>; - - fn into_iter(self) -> Self::IntoIter { - self.iter() - } -} - /// Used by `PyList::iter()`. pub struct BoundListIterator<'py> { list: Bound<'py, PyList>, @@ -1068,91 +845,6 @@ mod tests { }); } - #[test] - #[cfg(feature = "gil-refs")] - #[allow(deprecated)] - fn test_list_index_trait() { - Python::with_gil(|py| { - let list = PyList::new(py, [2, 3, 5]); - assert_eq!(2, list[0].extract::().unwrap()); - assert_eq!(3, list[1].extract::().unwrap()); - assert_eq!(5, list[2].extract::().unwrap()); - }); - } - - #[test] - #[should_panic] - #[cfg(feature = "gil-refs")] - #[allow(deprecated)] - fn test_list_index_trait_panic() { - Python::with_gil(|py| { - let list = PyList::new(py, [2, 3, 5]); - let _ = &list[7]; - }); - } - - #[test] - #[cfg(feature = "gil-refs")] - #[allow(deprecated)] - fn test_list_index_trait_ranges() { - Python::with_gil(|py| { - let list = PyList::new(py, [2, 3, 5]); - assert_eq!(vec![3, 5], list[1..3].extract::>().unwrap()); - assert_eq!(Vec::::new(), list[3..3].extract::>().unwrap()); - assert_eq!(vec![3, 5], list[1..].extract::>().unwrap()); - assert_eq!(Vec::::new(), list[3..].extract::>().unwrap()); - assert_eq!(vec![2, 3, 5], list[..].extract::>().unwrap()); - assert_eq!(vec![3, 5], list[1..=2].extract::>().unwrap()); - assert_eq!(vec![2, 3], list[..2].extract::>().unwrap()); - assert_eq!(vec![2, 3], list[..=1].extract::>().unwrap()); - }) - } - - #[test] - #[should_panic = "range start index 5 out of range for list of length 3"] - #[cfg(feature = "gil-refs")] - #[allow(deprecated)] - fn test_list_index_trait_range_panic_start() { - Python::with_gil(|py| { - let list = PyList::new(py, [2, 3, 5]); - list[5..10].extract::>().unwrap(); - }) - } - - #[test] - #[should_panic = "range end index 10 out of range for list of length 3"] - #[cfg(feature = "gil-refs")] - #[allow(deprecated)] - fn test_list_index_trait_range_panic_end() { - Python::with_gil(|py| { - let list = PyList::new(py, [2, 3, 5]); - list[1..10].extract::>().unwrap(); - }) - } - - #[test] - #[should_panic = "slice index starts at 2 but ends at 1"] - #[cfg(feature = "gil-refs")] - #[allow(deprecated)] - fn test_list_index_trait_range_panic_wrong_order() { - Python::with_gil(|py| { - let list = PyList::new(py, [2, 3, 5]); - #[allow(clippy::reversed_empty_ranges)] - list[2..1].extract::>().unwrap(); - }) - } - - #[test] - #[should_panic = "range start index 8 out of range for list of length 3"] - #[cfg(feature = "gil-refs")] - #[allow(deprecated)] - fn test_list_index_trait_range_from_panic() { - Python::with_gil(|py| { - let list = PyList::new(py, [2, 3, 5]); - list[8..].extract::>().unwrap(); - }) - } - #[test] fn test_list_del_item() { Python::with_gil(|py| { diff --git a/src/types/mapping.rs b/src/types/mapping.rs index 9ff0ad85762..e3a19af069f 100644 --- a/src/types/mapping.rs +++ b/src/types/mapping.rs @@ -6,8 +6,6 @@ use crate::sync::GILOnceCell; use crate::type_object::PyTypeInfo; use crate::types::any::PyAnyMethods; use crate::types::{PyAny, PyDict, PySequence, PyType}; -#[cfg(feature = "gil-refs")] -use crate::PyNativeType; use crate::{ffi, Py, PyTypeCheck, Python, ToPyObject}; /// Represents a reference to a Python object supporting the mapping protocol. @@ -33,87 +31,6 @@ impl PyMapping { } } -#[cfg(feature = "gil-refs")] -impl PyMapping { - /// Returns the number of objects in the mapping. - /// - /// This is equivalent to the Python expression `len(self)`. - #[inline] - pub fn len(&self) -> PyResult { - self.as_borrowed().len() - } - - /// Returns whether the mapping is empty. - #[inline] - pub fn is_empty(&self) -> PyResult { - self.as_borrowed().is_empty() - } - - /// Determines if the mapping contains the specified key. - /// - /// This is equivalent to the Python expression `key in self`. - pub fn contains(&self, key: K) -> PyResult - where - K: ToPyObject, - { - self.as_borrowed().contains(key) - } - - /// Gets the item in self with key `key`. - /// - /// Returns an `Err` if the item with specified key is not found, usually `KeyError`. - /// - /// This is equivalent to the Python expression `self[key]`. - #[inline] - pub fn get_item(&self, key: K) -> PyResult<&PyAny> - where - K: ToPyObject, - { - self.as_borrowed().get_item(key).map(Bound::into_gil_ref) - } - - /// Sets the item in self with key `key`. - /// - /// This is equivalent to the Python expression `self[key] = value`. - #[inline] - pub fn set_item(&self, key: K, value: V) -> PyResult<()> - where - K: ToPyObject, - V: ToPyObject, - { - self.as_borrowed().set_item(key, value) - } - - /// Deletes the item with key `key`. - /// - /// This is equivalent to the Python statement `del self[key]`. - #[inline] - pub fn del_item(&self, key: K) -> PyResult<()> - where - K: ToPyObject, - { - self.as_borrowed().del_item(key) - } - - /// Returns a sequence containing all keys in the mapping. - #[inline] - pub fn keys(&self) -> PyResult<&PySequence> { - self.as_borrowed().keys().map(Bound::into_gil_ref) - } - - /// Returns a sequence containing all values in the mapping. - #[inline] - pub fn values(&self) -> PyResult<&PySequence> { - self.as_borrowed().values().map(Bound::into_gil_ref) - } - - /// Returns a sequence of tuples of all (key, value) pairs in the mapping. - #[inline] - pub fn items(&self) -> PyResult<&PySequence> { - self.as_borrowed().items().map(Bound::into_gil_ref) - } -} - /// Implementation of functionality for [`PyMapping`]. /// /// These methods are defined for the `Bound<'py, PyMapping>` smart pointer, so to use method call diff --git a/src/types/memoryview.rs b/src/types/memoryview.rs index bff1fdcd4c9..a79f66d548a 100644 --- a/src/types/memoryview.rs +++ b/src/types/memoryview.rs @@ -15,16 +15,6 @@ pub struct PyMemoryView(PyAny); pyobject_native_type_core!(PyMemoryView, pyobject_native_static_type_object!(ffi::PyMemoryView_Type), #checkfunction=ffi::PyMemoryView_Check); impl PyMemoryView { - /// Deprecated form of [`PyMemoryView::from_bound`] - #[cfg(feature = "gil-refs")] - #[deprecated( - since = "0.21.0", - note = "`PyMemoryView::from` will be replaced by `PyMemoryView::from_bound` in a future PyO3 version" - )] - pub fn from(src: &PyAny) -> PyResult<&PyMemoryView> { - PyMemoryView::from_bound(&src.as_borrowed()).map(Bound::into_gil_ref) - } - /// Creates a new Python `memoryview` object from another Python object that /// implements the buffer protocol. pub fn from_bound<'py>(src: &Bound<'py, PyAny>) -> PyResult> { diff --git a/src/types/mod.rs b/src/types/mod.rs index d74c7bc234c..a0e0cc77f25 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -8,9 +8,6 @@ pub use self::capsule::{PyCapsule, PyCapsuleMethods}; #[cfg(all(not(Py_LIMITED_API), not(PyPy), not(GraalPy)))] pub use self::code::PyCode; pub use self::complex::{PyComplex, PyComplexMethods}; -#[allow(deprecated)] -#[cfg(all(not(Py_LIMITED_API), feature = "gil-refs"))] -pub use self::datetime::timezone_utc; #[cfg(not(Py_LIMITED_API))] pub use self::datetime::{ timezone_utc_bound, PyDate, PyDateAccess, PyDateTime, PyDelta, PyDeltaAccess, PyTime, @@ -85,12 +82,6 @@ pub mod iter { pub use super::list::BoundListIterator; pub use super::set::BoundSetIterator; pub use super::tuple::{BorrowedTupleIterator, BoundTupleIterator}; - - #[cfg(feature = "gil-refs")] - pub use super::{ - dict::PyDictIterator, frozenset::PyFrozenSetIterator, list::PyListIterator, - set::PySetIterator, tuple::PyTupleIterator, - }; } /// Python objects that have a base type. diff --git a/src/types/module.rs b/src/types/module.rs index c805c09a239..6b90e5de197 100644 --- a/src/types/module.rs +++ b/src/types/module.rs @@ -10,9 +10,6 @@ use crate::{exceptions, ffi, Bound, IntoPy, Py, PyObject, Python}; use std::ffi::CString; use std::str; -#[cfg(feature = "gil-refs")] -use {super::PyStringMethods, crate::PyNativeType}; - /// Represents a Python [`module`][1] object. /// /// Values of this type are accessed via PyO3's smart pointers, e.g. as @@ -163,253 +160,6 @@ impl PyModule { } } -#[cfg(feature = "gil-refs")] -impl PyModule { - /// Deprecated form of [`PyModule::new_bound`]. - #[inline] - #[deprecated( - since = "0.21.0", - note = "`PyModule::new` will be replaced by `PyModule::new_bound` in a future PyO3 version" - )] - pub fn new<'py>(py: Python<'py>, name: &str) -> PyResult<&'py PyModule> { - Self::new_bound(py, name).map(Bound::into_gil_ref) - } - - /// Deprecated form of [`PyModule::import_bound`]. - #[inline] - #[deprecated( - since = "0.21.0", - note = "`PyModule::import` will be replaced by `PyModule::import_bound` in a future PyO3 version" - )] - pub fn import(py: Python<'_>, name: N) -> PyResult<&PyModule> - where - N: IntoPy>, - { - Self::import_bound(py, name).map(Bound::into_gil_ref) - } - - /// Deprecated form of [`PyModule::from_code_bound`]. - #[inline] - #[deprecated( - since = "0.21.0", - note = "`PyModule::from_code` will be replaced by `PyModule::from_code_bound` in a future PyO3 version" - )] - pub fn from_code<'py>( - py: Python<'py>, - code: &str, - file_name: &str, - module_name: &str, - ) -> PyResult<&'py PyModule> { - Self::from_code_bound(py, code, file_name, module_name).map(Bound::into_gil_ref) - } - - /// Returns the module's `__dict__` attribute, which contains the module's symbol table. - pub fn dict(&self) -> &PyDict { - self.as_borrowed().dict().into_gil_ref() - } - - /// Returns the index (the `__all__` attribute) of the module, - /// creating one if needed. - /// - /// `__all__` declares the items that will be imported with `from my_module import *`. - pub fn index(&self) -> PyResult<&PyList> { - self.as_borrowed().index().map(Bound::into_gil_ref) - } - - /// Returns the name (the `__name__` attribute) of the module. - /// - /// May fail if the module does not have a `__name__` attribute. - pub fn name(&self) -> PyResult<&str> { - self.as_borrowed().name()?.into_gil_ref().to_str() - } - - /// Returns the filename (the `__file__` attribute) of the module. - /// - /// May fail if the module does not have a `__file__` attribute. - pub fn filename(&self) -> PyResult<&str> { - self.as_borrowed().filename()?.into_gil_ref().to_str() - } - - /// Adds an attribute to the module. - /// - /// For adding classes, functions or modules, prefer to use [`PyModule::add_class`], - /// [`PyModule::add_function`] or [`PyModule::add_submodule`] instead, respectively. - /// - /// # Examples - /// - /// ```rust - /// use pyo3::prelude::*; - /// - /// #[pymodule] - /// fn my_module(module: &Bound<'_, PyModule>) -> PyResult<()> { - /// module.add("c", 299_792_458)?; - /// Ok(()) - /// } - /// ``` - /// - /// Python code can then do the following: - /// - /// ```python - /// from my_module import c - /// - /// print("c is", c) - /// ``` - /// - /// This will result in the following output: - /// - /// ```text - /// c is 299792458 - /// ``` - pub fn add(&self, name: &str, value: V) -> PyResult<()> - where - V: IntoPy, - { - self.as_borrowed().add(name, value) - } - - /// Adds a new class to the module. - /// - /// Notice that this method does not take an argument. - /// Instead, this method is *generic*, and requires us to use the - /// "turbofish" syntax to specify the class we want to add. - /// - /// # Examples - /// - /// ```rust - /// use pyo3::prelude::*; - /// - /// #[pyclass] - /// struct Foo { /* fields omitted */ } - /// - /// #[pymodule] - /// fn my_module(module: &Bound<'_, PyModule>) -> PyResult<()> { - /// module.add_class::()?; - /// Ok(()) - /// } - /// ``` - /// - /// Python code can see this class as such: - /// ```python - /// from my_module import Foo - /// - /// print("Foo is", Foo) - /// ``` - /// - /// This will result in the following output: - /// ```text - /// Foo is - /// ``` - /// - /// Note that as we haven't defined a [constructor][1], Python code can't actually - /// make an *instance* of `Foo` (or *get* one for that matter, as we haven't exported - /// anything that can return instances of `Foo`). - /// - #[doc = concat!("[1]: https://pyo3.rs/v", env!("CARGO_PKG_VERSION"), "/class.html#constructor")] - pub fn add_class(&self) -> PyResult<()> - where - T: PyClass, - { - self.as_borrowed().add_class::() - } - - /// Adds a function or a (sub)module to a module, using the functions name as name. - /// - /// Prefer to use [`PyModule::add_function`] and/or [`PyModule::add_submodule`] instead. - pub fn add_wrapped<'a, T>(&'a self, wrapper: &impl Fn(Python<'a>) -> T) -> PyResult<()> - where - T: IntoPyCallbackOutput, - { - self.as_borrowed().add_wrapped(wrapper) - } - - /// Adds a submodule to a module. - /// - /// This is especially useful for creating module hierarchies. - /// - /// Note that this doesn't define a *package*, so this won't allow Python code - /// to directly import submodules by using - /// `from my_module import submodule`. - /// For more information, see [#759][1] and [#1517][2]. - /// - /// # Examples - /// - /// ```rust - /// use pyo3::prelude::*; - /// - /// #[pymodule] - /// fn my_module(py: Python<'_>, module: &Bound<'_, PyModule>) -> PyResult<()> { - /// let submodule = PyModule::new_bound(py, "submodule")?; - /// submodule.add("super_useful_constant", "important")?; - /// - /// module.add_submodule(&submodule)?; - /// Ok(()) - /// } - /// ``` - /// - /// Python code can then do the following: - /// - /// ```python - /// import my_module - /// - /// print("super_useful_constant is", my_module.submodule.super_useful_constant) - /// ``` - /// - /// This will result in the following output: - /// - /// ```text - /// super_useful_constant is important - /// ``` - /// - /// [1]: https://github.com/PyO3/pyo3/issues/759 - /// [2]: https://github.com/PyO3/pyo3/issues/1517#issuecomment-808664021 - pub fn add_submodule(&self, module: &PyModule) -> PyResult<()> { - self.as_borrowed().add_submodule(&module.as_borrowed()) - } - - /// Add a function to a module. - /// - /// Note that this also requires the [`wrap_pyfunction!`][2] macro - /// to wrap a function annotated with [`#[pyfunction]`][1]. - /// - /// ```rust - /// use pyo3::prelude::*; - /// - /// #[pyfunction] - /// fn say_hello() { - /// println!("Hello world!") - /// } - /// #[pymodule] - /// fn my_module(module: &Bound<'_, PyModule>) -> PyResult<()> { - /// module.add_function(wrap_pyfunction!(say_hello, module)?) - /// } - /// ``` - /// - /// Python code can then do the following: - /// - /// ```python - /// from my_module import say_hello - /// - /// say_hello() - /// ``` - /// - /// This will result in the following output: - /// - /// ```text - /// Hello world! - /// ``` - /// - /// [1]: crate::prelude::pyfunction - /// [2]: crate::wrap_pyfunction - pub fn add_function<'a>(&'a self, fun: &'a PyCFunction) -> PyResult<()> { - let name = fun - .as_borrowed() - .getattr(__name__(self.py()))? - .downcast_into::()?; - let name = name.to_cow()?; - self.add(&name, fun) - } -} - /// Implementation of functionality for [`PyModule`]. /// /// These methods are defined for the `Bound<'py, PyModule>` smart pointer, so to use method call diff --git a/src/types/none.rs b/src/types/none.rs index 78f14be2b25..63e0b70dbf3 100644 --- a/src/types/none.rs +++ b/src/types/none.rs @@ -15,18 +15,6 @@ pyobject_native_type_named!(PyNone); pyobject_native_type_extract!(PyNone); impl PyNone { - /// Returns the `None` object. - /// Deprecated form of [`PyNone::get_bound`] - #[cfg(feature = "gil-refs")] - #[deprecated( - since = "0.21.0", - note = "`PyNone::get` will be replaced by `PyNone::get_bound` in a future PyO3 version" - )] - #[inline] - pub fn get(py: Python<'_>) -> &PyNone { - Self::get_bound(py).into_gil_ref() - } - /// Returns the `None` object. #[inline] pub fn get_bound(py: Python<'_>) -> Borrowed<'_, '_, PyNone> { diff --git a/src/types/notimplemented.rs b/src/types/notimplemented.rs index 6d1808070a6..2b4fe04421c 100644 --- a/src/types/notimplemented.rs +++ b/src/types/notimplemented.rs @@ -14,17 +14,6 @@ pyobject_native_type_named!(PyNotImplemented); pyobject_native_type_extract!(PyNotImplemented); impl PyNotImplemented { - /// Returns the `NotImplemented` object. - #[cfg(feature = "gil-refs")] - #[deprecated( - since = "0.21.0", - note = "`PyNotImplemented::get` will be replaced by `PyNotImplemented::get_bound` in a future PyO3 version" - )] - #[inline] - pub fn get(py: Python<'_>) -> &PyNotImplemented { - Self::get_bound(py).into_gil_ref() - } - /// Returns the `NotImplemented` object. #[inline] pub fn get_bound(py: Python<'_>) -> Borrowed<'_, '_, PyNotImplemented> { diff --git a/src/types/pysuper.rs b/src/types/pysuper.rs index bd9d042a1f0..456a3d83e8c 100644 --- a/src/types/pysuper.rs +++ b/src/types/pysuper.rs @@ -17,17 +17,6 @@ pyobject_native_type_core!( ); impl PySuper { - /// Deprecated form of `PySuper::new_bound`. - #[cfg(feature = "gil-refs")] - #[deprecated( - since = "0.21.0", - note = "`PySuper::new` will be replaced by `PySuper::new_bound` in a future PyO3 version" - )] - pub fn new<'py>(ty: &'py PyType, obj: &'py PyAny) -> PyResult<&'py PySuper> { - use crate::PyNativeType; - Self::new_bound(&ty.as_borrowed(), &obj.as_borrowed()).map(Bound::into_gil_ref) - } - /// Constructs a new super object. More read about super object: [docs](https://docs.python.org/3/library/functions.html#super) /// /// # Examples diff --git a/src/types/sequence.rs b/src/types/sequence.rs index faf371160dd..c618b15dd48 100644 --- a/src/types/sequence.rs +++ b/src/types/sequence.rs @@ -9,8 +9,6 @@ use crate::py_result_ext::PyResultExt; use crate::sync::GILOnceCell; use crate::type_object::PyTypeInfo; use crate::types::{any::PyAnyMethods, PyAny, PyList, PyString, PyTuple, PyType}; -#[cfg(feature = "gil-refs")] -use crate::PyNativeType; use crate::{ffi, FromPyObject, Py, PyTypeCheck, Python, ToPyObject}; /// Represents a reference to a Python object supporting the sequence protocol. @@ -36,167 +34,6 @@ impl PySequence { } } -#[cfg(feature = "gil-refs")] -impl PySequence { - /// Returns the number of objects in sequence. - /// - /// This is equivalent to the Python expression `len(self)`. - #[inline] - pub fn len(&self) -> PyResult { - self.as_borrowed().len() - } - - /// Returns whether the sequence is empty. - #[inline] - pub fn is_empty(&self) -> PyResult { - self.as_borrowed().is_empty() - } - - /// Returns the concatenation of `self` and `other`. - /// - /// This is equivalent to the Python expression `self + other`. - #[inline] - pub fn concat(&self, other: &PySequence) -> PyResult<&PySequence> { - self.as_borrowed() - .concat(&other.as_borrowed()) - .map(Bound::into_gil_ref) - } - - /// Returns the result of repeating a sequence object `count` times. - /// - /// This is equivalent to the Python expression `self * count`. - #[inline] - pub fn repeat(&self, count: usize) -> PyResult<&PySequence> { - self.as_borrowed().repeat(count).map(Bound::into_gil_ref) - } - - /// Concatenates `self` and `other`, in place if possible. - /// - /// This is equivalent to the Python expression `self.__iadd__(other)`. - /// - /// The Python statement `self += other` is syntactic sugar for `self = - /// self.__iadd__(other)`. `__iadd__` should modify and return `self` if - /// possible, but create and return a new object if not. - #[inline] - pub fn in_place_concat(&self, other: &PySequence) -> PyResult<&PySequence> { - self.as_borrowed() - .in_place_concat(&other.as_borrowed()) - .map(Bound::into_gil_ref) - } - - /// Repeats the sequence object `count` times and updates `self`, if possible. - /// - /// This is equivalent to the Python expression `self.__imul__(other)`. - /// - /// The Python statement `self *= other` is syntactic sugar for `self = - /// self.__imul__(other)`. `__imul__` should modify and return `self` if - /// possible, but create and return a new object if not. - #[inline] - pub fn in_place_repeat(&self, count: usize) -> PyResult<&PySequence> { - self.as_borrowed() - .in_place_repeat(count) - .map(Bound::into_gil_ref) - } - - /// Returns the `index`th element of the Sequence. - /// - /// This is equivalent to the Python expression `self[index]` without support of negative indices. - #[inline] - pub fn get_item(&self, index: usize) -> PyResult<&PyAny> { - self.as_borrowed().get_item(index).map(Bound::into_gil_ref) - } - - /// Returns the slice of sequence object between `begin` and `end`. - /// - /// This is equivalent to the Python expression `self[begin:end]`. - #[inline] - pub fn get_slice(&self, begin: usize, end: usize) -> PyResult<&PySequence> { - self.as_borrowed() - .get_slice(begin, end) - .map(Bound::into_gil_ref) - } - - /// Assigns object `item` to the `i`th element of self. - /// - /// This is equivalent to the Python statement `self[i] = v`. - #[inline] - pub fn set_item(&self, i: usize, item: I) -> PyResult<()> - where - I: ToPyObject, - { - self.as_borrowed().set_item(i, item) - } - - /// Deletes the `i`th element of self. - /// - /// This is equivalent to the Python statement `del self[i]`. - #[inline] - pub fn del_item(&self, i: usize) -> PyResult<()> { - self.as_borrowed().del_item(i) - } - - /// Assigns the sequence `v` to the slice of `self` from `i1` to `i2`. - /// - /// This is equivalent to the Python statement `self[i1:i2] = v`. - #[inline] - pub fn set_slice(&self, i1: usize, i2: usize, v: &PyAny) -> PyResult<()> { - self.as_borrowed().set_slice(i1, i2, &v.as_borrowed()) - } - - /// Deletes the slice from `i1` to `i2` from `self`. - /// - /// This is equivalent to the Python statement `del self[i1:i2]`. - #[inline] - pub fn del_slice(&self, i1: usize, i2: usize) -> PyResult<()> { - self.as_borrowed().del_slice(i1, i2) - } - - /// Returns the number of occurrences of `value` in self, that is, return the - /// number of keys for which `self[key] == value`. - #[inline] - #[cfg(not(any(PyPy, GraalPy)))] - pub fn count(&self, value: V) -> PyResult - where - V: ToPyObject, - { - self.as_borrowed().count(value) - } - - /// Determines if self contains `value`. - /// - /// This is equivalent to the Python expression `value in self`. - #[inline] - pub fn contains(&self, value: V) -> PyResult - where - V: ToPyObject, - { - self.as_borrowed().contains(value) - } - - /// Returns the first index `i` for which `self[i] == value`. - /// - /// This is equivalent to the Python expression `self.index(value)`. - #[inline] - pub fn index(&self, value: V) -> PyResult - where - V: ToPyObject, - { - self.as_borrowed().index(value) - } - - /// Returns a fresh list based on the Sequence. - #[inline] - pub fn to_list(&self) -> PyResult<&PyList> { - self.as_borrowed().to_list().map(Bound::into_gil_ref) - } - - /// Returns a fresh tuple based on the Sequence. - #[inline] - pub fn to_tuple(&self) -> PyResult<&PyTuple> { - self.as_borrowed().to_tuple().map(Bound::into_gil_ref) - } -} - /// Implementation of functionality for [`PySequence`]. /// /// These methods are defined for the `Bound<'py, PySequence>` smart pointer, so to use method call @@ -475,22 +312,6 @@ impl<'py> PySequenceMethods<'py> for Bound<'py, PySequence> { } } -#[inline] -#[cfg(feature = "gil-refs")] -fn sequence_len(seq: &PySequence) -> usize { - seq.len().expect("failed to get sequence length") -} - -#[inline] -#[cfg(feature = "gil-refs")] -fn sequence_slice(seq: &PySequence, start: usize, end: usize) -> &PySequence { - seq.get_slice(start, end) - .expect("sequence slice operation failed") -} - -#[cfg(feature = "gil-refs")] -index_impls!(PySequence, "sequence", sequence_len, sequence_slice); - impl<'py, T> FromPyObject<'py> for Vec where T: FromPyObject<'py>, @@ -655,105 +476,6 @@ mod tests { }); } - #[test] - #[cfg(feature = "gil-refs")] - #[allow(deprecated)] - fn test_seq_index_trait() { - Python::with_gil(|py| { - let v: Vec = vec![1, 1, 2]; - let ob = v.to_object(py); - let seq = ob.downcast::(py).unwrap(); - assert_eq!(1, seq[0].extract::().unwrap()); - assert_eq!(1, seq[1].extract::().unwrap()); - assert_eq!(2, seq[2].extract::().unwrap()); - }); - } - - #[test] - #[should_panic = "index 7 out of range for sequence"] - #[cfg(feature = "gil-refs")] - #[allow(deprecated)] - fn test_seq_index_trait_panic() { - Python::with_gil(|py| { - let v: Vec = vec![1, 1, 2]; - let ob = v.to_object(py); - let seq = ob.downcast::(py).unwrap(); - let _ = &seq[7]; - }); - } - - #[test] - #[cfg(feature = "gil-refs")] - #[allow(deprecated)] - fn test_seq_index_trait_ranges() { - Python::with_gil(|py| { - let v: Vec = vec![1, 1, 2]; - let ob = v.to_object(py); - let seq = ob.downcast::(py).unwrap(); - assert_eq!(vec![1, 2], seq[1..3].extract::>().unwrap()); - assert_eq!(Vec::::new(), seq[3..3].extract::>().unwrap()); - assert_eq!(vec![1, 2], seq[1..].extract::>().unwrap()); - assert_eq!(Vec::::new(), seq[3..].extract::>().unwrap()); - assert_eq!(vec![1, 1, 2], seq[..].extract::>().unwrap()); - assert_eq!(vec![1, 2], seq[1..=2].extract::>().unwrap()); - assert_eq!(vec![1, 1], seq[..2].extract::>().unwrap()); - assert_eq!(vec![1, 1], seq[..=1].extract::>().unwrap()); - }) - } - - #[test] - #[should_panic = "range start index 5 out of range for sequence of length 3"] - #[cfg(feature = "gil-refs")] - #[allow(deprecated)] - fn test_seq_index_trait_range_panic_start() { - Python::with_gil(|py| { - let v: Vec = vec![1, 1, 2]; - let ob = v.to_object(py); - let seq = ob.downcast::(py).unwrap(); - seq[5..10].extract::>().unwrap(); - }) - } - - #[test] - #[should_panic = "range end index 10 out of range for sequence of length 3"] - #[cfg(feature = "gil-refs")] - #[allow(deprecated)] - fn test_seq_index_trait_range_panic_end() { - Python::with_gil(|py| { - let v: Vec = vec![1, 1, 2]; - let ob = v.to_object(py); - let seq = ob.downcast::(py).unwrap(); - seq[1..10].extract::>().unwrap(); - }) - } - - #[test] - #[should_panic = "slice index starts at 2 but ends at 1"] - #[cfg(feature = "gil-refs")] - #[allow(deprecated)] - fn test_seq_index_trait_range_panic_wrong_order() { - Python::with_gil(|py| { - let v: Vec = vec![1, 1, 2]; - let ob = v.to_object(py); - let seq = ob.downcast::(py).unwrap(); - #[allow(clippy::reversed_empty_ranges)] - seq[2..1].extract::>().unwrap(); - }) - } - - #[test] - #[should_panic = "range start index 8 out of range for sequence of length 3"] - #[cfg(feature = "gil-refs")] - #[allow(deprecated)] - fn test_seq_index_trait_range_from_panic() { - Python::with_gil(|py| { - let v: Vec = vec![1, 1, 2]; - let ob = v.to_object(py); - let seq = ob.downcast::(py).unwrap(); - seq[8..].extract::>().unwrap(); - }) - } - #[test] fn test_seq_del_item() { Python::with_gil(|py| { diff --git a/src/types/set.rs b/src/types/set.rs index 9dc44745df2..975f4bd00aa 100644 --- a/src/types/set.rs +++ b/src/types/set.rs @@ -58,92 +58,6 @@ impl PySet { } } -#[cfg(feature = "gil-refs")] -impl PySet { - /// Deprecated form of [`PySet::new_bound`]. - #[deprecated( - since = "0.21.0", - note = "`PySet::new` will be replaced by `PySet::new_bound` in a future PyO3 version" - )] - #[inline] - pub fn new<'a, 'p, T: ToPyObject + 'a>( - py: Python<'p>, - elements: impl IntoIterator, - ) -> PyResult<&'p PySet> { - Self::new_bound(py, elements).map(Bound::into_gil_ref) - } - - /// Deprecated form of [`PySet::empty_bound`]. - #[deprecated( - since = "0.21.2", - note = "`PySet::empty` will be replaced by `PySet::empty_bound` in a future PyO3 version" - )] - pub fn empty(py: Python<'_>) -> PyResult<&PySet> { - Self::empty_bound(py).map(Bound::into_gil_ref) - } - - /// Removes all elements from the set. - #[inline] - pub fn clear(&self) { - self.as_borrowed().clear() - } - - /// Returns the number of items in the set. - /// - /// This is equivalent to the Python expression `len(self)`. - #[inline] - pub fn len(&self) -> usize { - self.as_borrowed().len() - } - - /// Checks if set is empty. - pub fn is_empty(&self) -> bool { - self.as_borrowed().is_empty() - } - - /// Determines if the set contains the specified key. - /// - /// This is equivalent to the Python expression `key in self`. - pub fn contains(&self, key: K) -> PyResult - where - K: ToPyObject, - { - self.as_borrowed().contains(key) - } - - /// Removes the element from the set if it is present. - /// - /// Returns `true` if the element was present in the set. - pub fn discard(&self, key: K) -> PyResult - where - K: ToPyObject, - { - self.as_borrowed().discard(key) - } - - /// Adds an element to the set. - pub fn add(&self, key: K) -> PyResult<()> - where - K: ToPyObject, - { - self.as_borrowed().add(key) - } - - /// Removes and returns an arbitrary element from the set. - pub fn pop(&self) -> Option { - self.as_borrowed().pop().map(Bound::unbind) - } - - /// Returns an iterator of values in this set. - /// - /// # Panics - /// - /// If PyO3 detects that the set is mutated during iteration, it will panic. - pub fn iter(&self) -> PySetIterator<'_> { - PySetIterator(BoundSetIterator::new(self.as_borrowed().to_owned())) - } -} - /// Implementation of functionality for [`PySet`]. /// /// These methods are defined for the `Bound<'py, PySet>` smart pointer, so to use method call diff --git a/src/types/slice.rs b/src/types/slice.rs index dafe5053059..1e6e0584eae 100644 --- a/src/types/slice.rs +++ b/src/types/slice.rs @@ -2,8 +2,6 @@ use crate::err::{PyErr, PyResult}; use crate::ffi; use crate::ffi_ptr_ext::FfiPtrExt; use crate::types::any::PyAnyMethods; -#[cfg(feature = "gil-refs")] -use crate::PyNativeType; use crate::{Bound, PyAny, PyObject, Python, ToPyObject}; /// Represents a Python `slice`. @@ -78,35 +76,6 @@ impl PySlice { } } -#[cfg(feature = "gil-refs")] -impl PySlice { - /// Deprecated form of `PySlice::new_bound`. - #[deprecated( - since = "0.21.0", - note = "`PySlice::new` will be replaced by `PySlice::new_bound` in a future PyO3 version" - )] - pub fn new(py: Python<'_>, start: isize, stop: isize, step: isize) -> &PySlice { - Self::new_bound(py, start, stop, step).into_gil_ref() - } - - /// Deprecated form of `PySlice::full_bound`. - #[deprecated( - since = "0.21.0", - note = "`PySlice::full` will be replaced by `PySlice::full_bound` in a future PyO3 version" - )] - pub fn full(py: Python<'_>) -> &PySlice { - PySlice::full_bound(py).into_gil_ref() - } - - /// Retrieves the start, stop, and step indices from the slice object, - /// assuming a sequence of length `length`, and stores the length of the - /// slice in its `slicelength` member. - #[inline] - pub fn indices(&self, length: isize) -> PyResult { - self.as_borrowed().indices(length) - } -} - /// Implementation of functionality for [`PySlice`]. /// /// These methods are defined for the `Bound<'py, PyTuple>` smart pointer, so to use method call diff --git a/src/types/string.rs b/src/types/string.rs index fafeac83091..aec11ecfde2 100644 --- a/src/types/string.rs +++ b/src/types/string.rs @@ -6,8 +6,6 @@ use crate::py_result_ext::PyResultExt; use crate::types::any::PyAnyMethods; use crate::types::bytes::PyBytesMethods; use crate::types::PyBytes; -#[cfg(feature = "gil-refs")] -use crate::PyNativeType; use crate::{ffi, Bound, IntoPy, Py, PyAny, PyResult, Python}; use std::borrow::Cow; use std::str; @@ -210,88 +208,6 @@ impl PyString { } } -#[cfg(feature = "gil-refs")] -impl PyString { - /// Deprecated form of [`PyString::new_bound`]. - #[deprecated( - since = "0.21.0", - note = "`PyString::new` will be replaced by `PyString::new_bound` in a future PyO3 version" - )] - pub fn new<'py>(py: Python<'py>, s: &str) -> &'py Self { - Self::new_bound(py, s).into_gil_ref() - } - - /// Deprecated form of [`PyString::intern_bound`]. - #[deprecated( - since = "0.21.0", - note = "`PyString::intern` will be replaced by `PyString::intern_bound` in a future PyO3 version" - )] - pub fn intern<'py>(py: Python<'py>, s: &str) -> &'py Self { - Self::intern_bound(py, s).into_gil_ref() - } - - /// Deprecated form of [`PyString::from_object_bound`]. - #[deprecated( - since = "0.21.0", - note = "`PyString::from_object` will be replaced by `PyString::from_object_bound` in a future PyO3 version" - )] - pub fn from_object<'py>(src: &'py PyAny, encoding: &str, errors: &str) -> PyResult<&'py Self> { - Self::from_object_bound(&src.as_borrowed(), encoding, errors).map(Bound::into_gil_ref) - } - - /// Gets the Python string as a Rust UTF-8 string slice. - /// - /// Returns a `UnicodeEncodeError` if the input is not valid unicode - /// (containing unpaired surrogates). - pub fn to_str(&self) -> PyResult<&str> { - #[cfg(any(Py_3_10, not(Py_LIMITED_API)))] - { - self.as_borrowed().to_str() - } - - #[cfg(not(any(Py_3_10, not(Py_LIMITED_API))))] - { - let bytes = self.as_borrowed().encode_utf8()?.into_gil_ref(); - Ok(unsafe { std::str::from_utf8_unchecked(bytes.as_bytes()) }) - } - } - - /// Converts the `PyString` into a Rust string, avoiding copying when possible. - /// - /// Returns a `UnicodeEncodeError` if the input is not valid unicode - /// (containing unpaired surrogates). - pub fn to_cow(&self) -> PyResult> { - self.as_borrowed().to_cow() - } - - /// Converts the `PyString` into a Rust string. - /// - /// Unpaired surrogates invalid UTF-8 sequences are - /// replaced with `U+FFFD REPLACEMENT CHARACTER`. - pub fn to_string_lossy(&self) -> Cow<'_, str> { - self.as_borrowed().to_string_lossy() - } - - /// Obtains the raw data backing the Python string. - /// - /// If the Python string object was created through legacy APIs, its internal storage format - /// will be canonicalized before data is returned. - /// - /// # Safety - /// - /// This function implementation relies on manually decoding a C bitfield. In practice, this - /// works well on common little-endian architectures such as x86_64, where the bitfield has a - /// common representation (even if it is not part of the C spec). The PyO3 CI tests this API on - /// x86_64 platforms. - /// - /// By using this API, you accept responsibility for testing that PyStringData behaves as - /// expected on the targets where you plan to distribute your software. - #[cfg(not(any(Py_LIMITED_API, GraalPy, PyPy)))] - pub unsafe fn data(&self) -> PyResult> { - self.as_borrowed().data() - } -} - /// Implementation of functionality for [`PyString`]. /// /// These methods are defined for the `Bound<'py, PyString>` smart pointer, so to use method call diff --git a/src/types/traceback.rs b/src/types/traceback.rs index 5e3496145e2..7f716c6f24a 100644 --- a/src/types/traceback.rs +++ b/src/types/traceback.rs @@ -1,7 +1,5 @@ use crate::err::{error_on_minusone, PyResult}; use crate::types::{any::PyAnyMethods, string::PyStringMethods, PyString}; -#[cfg(feature = "gil-refs")] -use crate::PyNativeType; use crate::{ffi, Bound, PyAny}; /// Represents a Python traceback. @@ -20,44 +18,6 @@ pyobject_native_type_core!( #checkfunction=ffi::PyTraceBack_Check ); -#[cfg(feature = "gil-refs")] -impl PyTraceback { - /// Formats the traceback as a string. - /// - /// This does not include the exception type and value. The exception type and value can be - /// formatted using the `Display` implementation for `PyErr`. - /// - /// # Example - /// - /// The following code formats a Python traceback and exception pair from Rust: - /// - /// ```rust - /// # use pyo3::{Python, PyResult, prelude::PyTracebackMethods}; - /// # let result: PyResult<()> = - /// Python::with_gil(|py| { - /// let err = py - /// .run_bound("raise Exception('banana')", None, None) - /// .expect_err("raise will create a Python error"); - /// - /// let traceback = err.traceback_bound(py).expect("raised exception will have a traceback"); - /// assert_eq!( - /// format!("{}{}", traceback.format()?, err), - /// "\ - /// Traceback (most recent call last): - /// File \"\", line 1, in - /// Exception: banana\ - /// " - /// ); - /// Ok(()) - /// }) - /// # ; - /// # result.expect("example failed"); - /// ``` - pub fn format(&self) -> PyResult { - self.as_borrowed().format() - } -} - /// Implementation of functionality for [`PyTraceback`]. /// /// These methods are defined for the `Bound<'py, PyTraceback>` smart pointer, so to use method call diff --git a/src/types/tuple.rs b/src/types/tuple.rs index aacc1efe431..eb17c6320fb 100644 --- a/src/types/tuple.rs +++ b/src/types/tuple.rs @@ -112,140 +112,6 @@ impl PyTuple { } } -#[cfg(feature = "gil-refs")] -impl PyTuple { - /// Deprecated form of `PyTuple::new_bound`. - #[track_caller] - #[deprecated( - since = "0.21.0", - note = "`PyTuple::new` will be replaced by `PyTuple::new_bound` in a future PyO3 version" - )] - pub fn new( - py: Python<'_>, - elements: impl IntoIterator, - ) -> &PyTuple - where - T: ToPyObject, - U: ExactSizeIterator, - { - Self::new_bound(py, elements).into_gil_ref() - } - - /// Deprecated form of `PyTuple::empty_bound`. - #[deprecated( - since = "0.21.0", - note = "`PyTuple::empty` will be replaced by `PyTuple::empty_bound` in a future PyO3 version" - )] - pub fn empty(py: Python<'_>) -> &PyTuple { - Self::empty_bound(py).into_gil_ref() - } - - /// Gets the length of the tuple. - pub fn len(&self) -> usize { - self.as_borrowed().len() - } - - /// Checks if the tuple is empty. - pub fn is_empty(&self) -> bool { - self.as_borrowed().is_empty() - } - - /// Returns `self` cast as a `PySequence`. - pub fn as_sequence(&self) -> &PySequence { - unsafe { self.downcast_unchecked() } - } - - /// Takes the slice `self[low:high]` and returns it as a new tuple. - /// - /// Indices must be nonnegative, and out-of-range indices are clipped to - /// `self.len()`. - pub fn get_slice(&self, low: usize, high: usize) -> &PyTuple { - self.as_borrowed().get_slice(low, high).into_gil_ref() - } - - /// Gets the tuple item at the specified index. - /// # Example - /// ``` - /// use pyo3::{prelude::*, types::PyTuple}; - /// - /// # fn main() -> PyResult<()> { - /// Python::with_gil(|py| -> PyResult<()> { - /// let ob = (1, 2, 3).to_object(py); - /// let tuple = ob.downcast_bound::(py).unwrap(); - /// let obj = tuple.get_item(0); - /// assert_eq!(obj.unwrap().extract::().unwrap(), 1); - /// Ok(()) - /// }) - /// # } - /// ``` - pub fn get_item(&self, index: usize) -> PyResult<&PyAny> { - self.as_borrowed() - .get_borrowed_item(index) - .map(Borrowed::into_gil_ref) - } - - /// Gets the tuple item at the specified index. Undefined behavior on bad index. Use with caution. - /// - /// # Safety - /// - /// Caller must verify that the index is within the bounds of the tuple. - #[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))] - pub unsafe fn get_item_unchecked(&self, index: usize) -> &PyAny { - self.as_borrowed() - .get_borrowed_item_unchecked(index) - .into_gil_ref() - } - - /// Returns `self` as a slice of objects. - #[cfg(not(any(Py_LIMITED_API, GraalPy)))] - pub fn as_slice(&self) -> &[&PyAny] { - // This is safe because &PyAny has the same memory layout as *mut ffi::PyObject, - // and because tuples are immutable. - unsafe { - let ptr = self.as_ptr() as *mut ffi::PyTupleObject; - let slice = std::slice::from_raw_parts((*ptr).ob_item.as_ptr(), self.len()); - &*(slice as *const [*mut ffi::PyObject] as *const [&PyAny]) - } - } - - /// Determines if self contains `value`. - /// - /// This is equivalent to the Python expression `value in self`. - #[inline] - pub fn contains(&self, value: V) -> PyResult - where - V: ToPyObject, - { - self.as_borrowed().contains(value) - } - - /// Returns the first index `i` for which `self[i] == value`. - /// - /// This is equivalent to the Python expression `self.index(value)`. - #[inline] - pub fn index(&self, value: V) -> PyResult - where - V: ToPyObject, - { - self.as_borrowed().index(value) - } - - /// Returns an iterator over the tuple items. - pub fn iter(&self) -> PyTupleIterator<'_> { - PyTupleIterator(BorrowedTupleIterator::new(self.as_borrowed())) - } - - /// Return a new list containing the contents of this tuple; equivalent to the Python expression `list(tuple)`. - /// - /// This method is equivalent to `self.as_sequence().to_list()` and faster than `PyList::new(py, self)`. - pub fn to_list(&self) -> &PyList { - self.as_borrowed().to_list().into_gil_ref() - } -} - -#[cfg(feature = "gil-refs")] -index_impls!(PyTuple, "tuple", PyTuple::len, PyTuple::get_slice); - /// Implementation of functionality for [`PyTuple`]. /// /// These methods are defined for the `Bound<'py, PyTuple>` smart pointer, so to use method call @@ -1129,101 +995,6 @@ mod tests { }); } - #[test] - #[cfg(feature = "gil-refs")] - #[allow(deprecated)] - fn test_tuple_index_trait() { - Python::with_gil(|py| { - let ob = (1, 2, 3).to_object(py); - let tuple: &PyTuple = ob.downcast(py).unwrap(); - assert_eq!(1, tuple[0].extract::().unwrap()); - assert_eq!(2, tuple[1].extract::().unwrap()); - assert_eq!(3, tuple[2].extract::().unwrap()); - }); - } - - #[test] - #[should_panic] - #[cfg(feature = "gil-refs")] - #[allow(deprecated)] - fn test_tuple_index_trait_panic() { - Python::with_gil(|py| { - let ob = (1, 2, 3).to_object(py); - let tuple: &PyTuple = ob.downcast(py).unwrap(); - let _ = &tuple[7]; - }); - } - - #[test] - #[cfg(feature = "gil-refs")] - #[allow(deprecated)] - fn test_tuple_index_trait_ranges() { - Python::with_gil(|py| { - let ob = (1, 2, 3).to_object(py); - let tuple: &PyTuple = ob.downcast(py).unwrap(); - assert_eq!(vec![2, 3], tuple[1..3].extract::>().unwrap()); - assert_eq!( - Vec::::new(), - tuple[3..3].extract::>().unwrap() - ); - assert_eq!(vec![2, 3], tuple[1..].extract::>().unwrap()); - assert_eq!(Vec::::new(), tuple[3..].extract::>().unwrap()); - assert_eq!(vec![1, 2, 3], tuple[..].extract::>().unwrap()); - assert_eq!(vec![2, 3], tuple[1..=2].extract::>().unwrap()); - assert_eq!(vec![1, 2], tuple[..2].extract::>().unwrap()); - assert_eq!(vec![1, 2], tuple[..=1].extract::>().unwrap()); - }) - } - - #[test] - #[should_panic = "range start index 5 out of range for tuple of length 3"] - #[cfg(feature = "gil-refs")] - #[allow(deprecated)] - fn test_tuple_index_trait_range_panic_start() { - Python::with_gil(|py| { - let ob = (1, 2, 3).to_object(py); - let tuple: &PyTuple = ob.downcast(py).unwrap(); - tuple[5..10].extract::>().unwrap(); - }) - } - - #[test] - #[should_panic = "range end index 10 out of range for tuple of length 3"] - #[cfg(feature = "gil-refs")] - #[allow(deprecated)] - fn test_tuple_index_trait_range_panic_end() { - Python::with_gil(|py| { - let ob = (1, 2, 3).to_object(py); - let tuple: &PyTuple = ob.downcast(py).unwrap(); - tuple[1..10].extract::>().unwrap(); - }) - } - - #[test] - #[should_panic = "slice index starts at 2 but ends at 1"] - #[cfg(feature = "gil-refs")] - #[allow(deprecated)] - fn test_tuple_index_trait_range_panic_wrong_order() { - Python::with_gil(|py| { - let ob = (1, 2, 3).to_object(py); - let tuple: &PyTuple = ob.downcast(py).unwrap(); - #[allow(clippy::reversed_empty_ranges)] - tuple[2..1].extract::>().unwrap(); - }) - } - - #[test] - #[should_panic = "range start index 8 out of range for tuple of length 3"] - #[cfg(feature = "gil-refs")] - #[allow(deprecated)] - fn test_tuple_index_trait_range_from_panic() { - Python::with_gil(|py| { - let ob = (1, 2, 3).to_object(py); - let tuple: &PyTuple = ob.downcast(py).unwrap(); - tuple[8..].extract::>().unwrap(); - }) - } - #[test] fn test_tuple_contains() { Python::with_gil(|py| { diff --git a/src/types/typeobject.rs b/src/types/typeobject.rs index dbe4c3efd10..f4b7a6fbc43 100644 --- a/src/types/typeobject.rs +++ b/src/types/typeobject.rs @@ -4,8 +4,6 @@ use crate::instance::Borrowed; use crate::pybacked::PyBackedStr; use crate::types::any::PyAnyMethods; use crate::types::PyTuple; -#[cfg(feature = "gil-refs")] -use crate::PyNativeType; use crate::{ffi, Bound, PyAny, PyTypeInfo, Python}; use super::PyString; @@ -47,70 +45,6 @@ impl PyType { } } -#[cfg(feature = "gil-refs")] -impl PyType { - /// Deprecated form of [`PyType::new_bound`]. - #[inline] - #[deprecated( - since = "0.21.0", - note = "`PyType::new` will be replaced by `PyType::new_bound` in a future PyO3 version" - )] - pub fn new(py: Python<'_>) -> &PyType { - T::type_object_bound(py).into_gil_ref() - } - - /// Retrieves the underlying FFI pointer associated with this Python object. - #[inline] - pub fn as_type_ptr(&self) -> *mut ffi::PyTypeObject { - self.as_borrowed().as_type_ptr() - } - - /// Deprecated form of [`PyType::from_borrowed_type_ptr`]. - /// - /// # Safety - /// - /// - The pointer must a valid non-null reference to a `PyTypeObject`. - #[inline] - #[deprecated( - since = "0.21.0", - note = "Use `PyType::from_borrowed_type_ptr` instead" - )] - pub unsafe fn from_type_ptr(py: Python<'_>, p: *mut ffi::PyTypeObject) -> &PyType { - Self::from_borrowed_type_ptr(py, p).into_gil_ref() - } - - /// Gets the name of the `PyType`. Equivalent to `self.__name__` in Python. - pub fn name(&self) -> PyResult<&PyString> { - self.as_borrowed().name().map(Bound::into_gil_ref) - } - - /// Gets the [qualified name](https://docs.python.org/3/glossary.html#term-qualified-name) of the `PyType`. - /// Equivalent to `self.__qualname__` in Python. - pub fn qualname(&self) -> PyResult<&PyString> { - self.as_borrowed().qualname().map(Bound::into_gil_ref) - } - - // `module` and `fully_qualified_name` intentionally omitted - - /// Checks whether `self` is a subclass of `other`. - /// - /// Equivalent to the Python expression `issubclass(self, other)`. - pub fn is_subclass(&self, other: &PyAny) -> PyResult { - self.as_borrowed().is_subclass(&other.as_borrowed()) - } - - /// Checks whether `self` is a subclass of type `T`. - /// - /// Equivalent to the Python expression `issubclass(self, T)`, if the type - /// `T` is known at compile time. - pub fn is_subclass_of(&self) -> PyResult - where - T: PyTypeInfo, - { - self.as_borrowed().is_subclass_of::() - } -} - /// Implementation of functionality for [`PyType`]. /// /// These methods are defined for the `Bound<'py, PyType>` smart pointer, so to use method call diff --git a/src/types/weakref/anyref.rs b/src/types/weakref/anyref.rs index 82e16293e62..75643fceb6a 100644 --- a/src/types/weakref/anyref.rs +++ b/src/types/weakref/anyref.rs @@ -4,9 +4,6 @@ use crate::type_object::{PyTypeCheck, PyTypeInfo}; use crate::types::any::{PyAny, PyAnyMethods}; use crate::{ffi, Borrowed, Bound}; -#[cfg(feature = "gil-refs")] -use crate::PyNativeType; - /// Represents any Python `weakref` reference. /// /// In Python this is created by calling `weakref.ref` or `weakref.proxy`. @@ -28,361 +25,6 @@ impl PyTypeCheck for PyWeakref { } } -#[cfg(feature = "gil-refs")] -impl PyWeakref { - // TODO: MAYBE ADD CREATION METHODS OR EASY CASTING?; - - /// Upgrade the weakref to a direct object reference. - /// - /// It is named `upgrade` to be inline with [rust's `Weak::upgrade`](std::rc::Weak::upgrade). - /// In Python it would be equivalent to [`PyWeakref_GetObject`] or retrieving the Object from Python. - /// - /// # Example - #[cfg_attr( - not(all(feature = "macros", not(all(Py_LIMITED_API, not(Py_3_9))))), - doc = "```rust,ignore" - )] - #[cfg_attr( - all(feature = "macros", not(all(Py_LIMITED_API, not(Py_3_9)))), - doc = "```rust" - )] - /// use pyo3::prelude::*; - /// use pyo3::types::{PyWeakref, PyWeakrefProxy}; - /// - /// #[pyclass(weakref)] - /// struct Foo { /* fields omitted */ } - /// - /// #[pymethods] - /// impl Foo { - /// fn get_data(&self) -> (&str, u32) { - /// ("Dave", 10) - /// } - /// } - /// - /// fn parse_data(reference: Borrowed<'_, '_, PyWeakref>) -> PyResult { - /// if let Some(data_src) = reference.upgrade_as::()? { - /// let data = data_src.borrow(); - /// let (name, score) = data.get_data(); - /// Ok(format!("Processing '{}': score = {}", name, score)) - /// } else { - /// Ok("The supplied data reference is nolonger relavent.".to_owned()) - /// } - /// } - /// - /// # fn main() -> PyResult<()> { - /// Python::with_gil(|py| { - /// let data = Bound::new(py, Foo{})?; - /// let proxy = PyWeakrefProxy::new_bound(&data)?; // Retrieve this as an PyMethods argument. - /// let reference = proxy.downcast::()?; - /// - /// assert_eq!( - /// parse_data(reference.as_borrowed())?, - /// "Processing 'Dave': score = 10" - /// ); - /// - /// drop(data); - /// - /// assert_eq!( - /// parse_data(reference.as_borrowed())?, - /// "The supplied data reference is nolonger relavent." - /// ); - /// - /// Ok(()) - /// }) - /// # } - /// ``` - /// - /// # Panics - /// This function panics is the current object is invalid. - /// If used propperly this is never the case. (NonNull and actually a weakref type) - /// - /// [`PyWeakref_GetObject`]: https://docs.python.org/3/c-api/weakref.html#c.PyWeakref_GetObject - pub fn upgrade_as(&self) -> PyResult> - where - T: PyTypeCheck, - { - Ok(self - .as_borrowed() - .upgrade_as::()? - .map(Bound::into_gil_ref)) - } - - /// Upgrade the weakref to a direct object reference unchecked. The type of the recovered object is not checked before downcasting, this could lead to unexpected behavior. Use only when absolutely certain the type can be guaranteed. The `weakref` may still return `None`. - /// - /// It is named `upgrade` to be inline with [rust's `Weak::upgrade`](std::rc::Weak::upgrade). - /// In Python it would be equivalent to [`PyWeakref_GetObject`] or retrieving the Object from Python. - /// - /// # Safety - /// Callers must ensure that the type is valid or risk type confusion. - /// The `weakref` is still allowed to be `None`, if the referenced object has been cleaned up. - /// - /// # Example - #[cfg_attr( - not(all(feature = "macros", not(all(Py_LIMITED_API, not(Py_3_9))))), - doc = "```rust,ignore" - )] - #[cfg_attr( - all(feature = "macros", not(all(Py_LIMITED_API, not(Py_3_9)))), - doc = "```rust" - )] - /// use pyo3::prelude::*; - /// use pyo3::types::{PyWeakref, PyWeakrefProxy}; - /// - /// #[pyclass(weakref)] - /// struct Foo { /* fields omitted */ } - /// - /// #[pymethods] - /// impl Foo { - /// fn get_data(&self) -> (&str, u32) { - /// ("Dave", 10) - /// } - /// } - /// - /// fn parse_data(reference: Borrowed<'_, '_, PyWeakref>) -> String { - /// if let Some(data_src) = unsafe { reference.upgrade_as_unchecked::() } { - /// let data = data_src.borrow(); - /// let (name, score) = data.get_data(); - /// format!("Processing '{}': score = {}", name, score) - /// } else { - /// "The supplied data reference is nolonger relavent.".to_owned() - /// } - /// } - /// - /// # fn main() -> PyResult<()> { - /// Python::with_gil(|py| { - /// let data = Bound::new(py, Foo{})?; - /// let proxy = PyWeakrefProxy::new_bound(&data)?; // Retrieve this as an PyMethods argument. - /// let reference = proxy.downcast::()?; - /// - /// assert_eq!( - /// parse_data(reference.as_borrowed()), - /// "Processing 'Dave': score = 10" - /// ); - /// - /// drop(data); - /// - /// assert_eq!( - /// parse_data(reference.as_borrowed()), - /// "The supplied data reference is nolonger relavent." - /// ); - /// - /// Ok(()) - /// }) - /// # } - /// ``` - /// - /// # Panics - /// This function panics is the current object is invalid. - /// If used propperly this is never the case. (NonNull and actually a weakref type) - /// - /// [`PyWeakref_GetObject`]: https://docs.python.org/3/c-api/weakref.html#c.PyWeakref_GetObject - pub unsafe fn upgrade_as_unchecked(&self) -> Option<&T::AsRefTarget> - where - T: PyTypeCheck, - { - self.as_borrowed() - .upgrade_as_unchecked::() - .map(Bound::into_gil_ref) - } - - /// Upgrade the weakref to an exact direct object reference. - /// - /// It is named `upgrade` to be inline with [rust's `Weak::upgrade`](std::rc::Weak::upgrade). - /// In Python it would be equivalent to [`PyWeakref_GetObject`] or retrieving the Object from Python. - /// - /// # Example - #[cfg_attr( - not(all(feature = "macros", not(all(Py_LIMITED_API, not(Py_3_9))))), - doc = "```rust,ignore" - )] - #[cfg_attr( - all(feature = "macros", not(all(Py_LIMITED_API, not(Py_3_9)))), - doc = "```rust" - )] - /// use pyo3::prelude::*; - /// use pyo3::types::{PyWeakref, PyWeakrefProxy}; - /// - /// #[pyclass(weakref)] - /// struct Foo { /* fields omitted */ } - /// - /// #[pymethods] - /// impl Foo { - /// fn get_data(&self) -> (&str, u32) { - /// ("Dave", 10) - /// } - /// } - /// - /// fn parse_data(reference: Borrowed<'_, '_, PyWeakref>) -> PyResult { - /// if let Some(data_src) = reference.upgrade_as_exact::()? { - /// let data = data_src.borrow(); - /// let (name, score) = data.get_data(); - /// Ok(format!("Processing '{}': score = {}", name, score)) - /// } else { - /// Ok("The supplied data reference is nolonger relavent.".to_owned()) - /// } - /// } - /// - /// # fn main() -> PyResult<()> { - /// Python::with_gil(|py| { - /// let data = Bound::new(py, Foo{})?; - /// let proxy = PyWeakrefProxy::new_bound(&data)?; // Retrieve this as an PyMethods argument. - /// let reference = proxy.downcast::()?; - /// - /// assert_eq!( - /// parse_data(reference.as_borrowed())?, - /// "Processing 'Dave': score = 10" - /// ); - /// - /// drop(data); - /// - /// assert_eq!( - /// parse_data(reference.as_borrowed())?, - /// "The supplied data reference is nolonger relavent." - /// ); - /// - /// Ok(()) - /// }) - /// # } - /// ``` - /// - /// # Panics - /// This function panics is the current object is invalid. - /// If used propperly this is never the case. (NonNull and actually a weakref type) - /// - /// [`PyWeakref_GetObject`]: https://docs.python.org/3/c-api/weakref.html#c.PyWeakref_GetObject - pub fn upgrade_as_exact(&self) -> PyResult> - where - T: PyTypeInfo, - { - Ok(self - .as_borrowed() - .upgrade_as_exact::()? - .map(Bound::into_gil_ref)) - } - - /// Upgrade the weakref to a [`PyAny`] reference to the target if possible. - /// - /// It is named `upgrade` to be inline with [rust's `Weak::upgrade`](std::rc::Weak::upgrade). - /// This function returns `Some(&'py PyAny)` if the reference still exists, otherwise `None` will be returned. - /// - /// This function gets the optional target of this [`PyWeakref`] (Any Python `weakref` weakreference). - /// It produces similair results as using [`PyWeakref_GetObject`] in the C api or retrieving the Object from Python. - /// - /// # Example - #[cfg_attr( - not(all(feature = "macros", not(all(Py_LIMITED_API, not(Py_3_9))))), - doc = "```rust,ignore" - )] - #[cfg_attr( - all(feature = "macros", not(all(Py_LIMITED_API, not(Py_3_9)))), - doc = "```rust" - )] - /// use pyo3::prelude::*; - /// use pyo3::types::{PyWeakref, PyWeakrefProxy}; - /// - /// #[pyclass(weakref)] - /// struct Foo { /* fields omitted */ } - /// - /// fn parse_data(reference: Borrowed<'_, '_, PyWeakref>) -> PyResult { - /// if let Some(object) = reference.upgrade() { - /// Ok(format!("The object '{}' refered by this reference still exists.", object.getattr("__class__")?.getattr("__qualname__")?)) - /// } else { - /// Ok("The object, which this reference refered to, no longer exists".to_owned()) - /// } - /// } - /// - /// # fn main() -> PyResult<()> { - /// Python::with_gil(|py| { - /// let data = Bound::new(py, Foo{})?; - /// let proxy = PyWeakrefProxy::new_bound(&data)?; // Retrieve this as an PyMethods argument. - /// let reference = proxy.downcast::()?; - /// - /// assert_eq!( - /// parse_data(reference.as_borrowed())?, - /// "The object 'Foo' refered by this reference still exists." - /// ); - /// - /// drop(data); - /// - /// assert_eq!( - /// parse_data(reference.as_borrowed())?, - /// "The object, which this reference refered to, no longer exists" - /// ); - /// - /// Ok(()) - /// }) - /// # } - /// ``` - /// - /// # Panics - /// This function panics is the current object is invalid. - /// If used propperly this is never the case. (NonNull and actually a weakref type) - /// - /// [`PyWeakref_GetObject`]: https://docs.python.org/3/c-api/weakref.html#c.PyWeakref_GetObject - pub fn upgrade(&self) -> Option<&'_ PyAny> { - self.as_borrowed().upgrade().map(Bound::into_gil_ref) - } - - /// Retrieve to a object pointed to by the weakref. - /// - /// This function returns `&'py PyAny`, which is either the object if it still exists, otherwise it will refer to [`PyNone`](crate::types::PyNone). - /// - /// This function gets the optional target of this [`PyWeakref`] (Any Python `weakref` weakreference). - /// It produces similair results as using [`PyWeakref_GetObject`] in the C api or retrieving the Object from Python. - /// - /// # Example - #[cfg_attr( - not(all(feature = "macros", not(all(Py_LIMITED_API, not(Py_3_9))))), - doc = "```rust,ignore" - )] - #[cfg_attr( - all(feature = "macros", not(all(Py_LIMITED_API, not(Py_3_9)))), - doc = "```rust" - )] - /// use pyo3::prelude::*; - /// use pyo3::types::{PyWeakref, PyWeakrefProxy}; - /// - /// #[pyclass(weakref)] - /// struct Foo { /* fields omitted */ } - /// - /// fn get_class(reference: Borrowed<'_, '_, PyWeakref>) -> PyResult { - /// reference - /// .get_object() - /// .getattr("__class__")? - /// .repr() - /// .map(|repr| repr.to_string()) - /// } - /// - /// # fn main() -> PyResult<()> { - /// Python::with_gil(|py| { - /// let object = Bound::new(py, Foo{})?; - /// let proxy = PyWeakrefProxy::new_bound(&object)?; // Retrieve this as an PyMethods argument. - /// let reference = proxy.downcast::()?; - /// - /// assert_eq!( - /// get_class(reference.as_borrowed())?, - /// "" - /// ); - /// - /// drop(object); - /// - /// assert_eq!(get_class(reference.as_borrowed())?, ""); - /// - /// Ok(()) - /// }) - /// # } - /// ``` - /// - /// # Panics - /// This function panics is the current object is invalid. - /// If used propperly this is never the case. (NonNull and actually a weakref type) - /// - /// [`PyWeakref_GetObject`]: https://docs.python.org/3/c-api/weakref.html#c.PyWeakref_GetObject - pub fn get_object(&self) -> &'_ PyAny { - self.as_borrowed().get_object().into_gil_ref() - } -} - /// Implementation of functionality for [`PyWeakref`]. /// /// These methods are defined for the `Bound<'py, PyWeakref>` smart pointer, so to use method call diff --git a/src/types/weakref/proxy.rs b/src/types/weakref/proxy.rs index 71334488b54..497b86a08bc 100644 --- a/src/types/weakref/proxy.rs +++ b/src/types/weakref/proxy.rs @@ -5,9 +5,6 @@ use crate::type_object::PyTypeCheck; use crate::types::any::PyAny; use crate::{ffi, Borrowed, Bound, ToPyObject}; -#[cfg(feature = "gil-refs")] -use crate::{type_object::PyTypeInfo, PyNativeType}; - use super::PyWeakrefMethods; /// Represents any Python `weakref` Proxy type. @@ -169,394 +166,6 @@ impl PyWeakrefProxy { } } -/// TODO: UPDATE DOCS -#[cfg(feature = "gil-refs")] -impl PyWeakrefProxy { - /// Deprecated form of [`PyWeakrefProxy::new_bound`]. - #[inline] - #[deprecated( - since = "0.21.0", - note = "`PyWeakrefProxy::new` will be replaced by `PyWeakrefProxy::new_bound` in a future PyO3 version" - )] - pub fn new(object: &T) -> PyResult<&PyWeakrefProxy> - where - T: PyNativeType, - { - Self::new_bound(object.as_borrowed().as_any()).map(Bound::into_gil_ref) - } - - /// Deprecated form of [`PyWeakrefProxy::new_bound_with`]. - #[inline] - #[deprecated( - since = "0.21.0", - note = "`PyWeakrefProxy::new_with` will be replaced by `PyWeakrefProxy::new_bound_with` in a future PyO3 version" - )] - pub fn new_with(object: &T, callback: C) -> PyResult<&PyWeakrefProxy> - where - T: PyNativeType, - C: ToPyObject, - { - Self::new_bound_with(object.as_borrowed().as_any(), callback).map(Bound::into_gil_ref) - } - - /// Upgrade the weakref to a direct object reference. - /// - /// It is named `upgrade` to be inline with [rust's `Weak::upgrade`](std::rc::Weak::upgrade). - /// In Python it would be equivalent to [`PyWeakref_GetObject`]. - /// - /// # Example - #[cfg_attr( - not(all(feature = "macros", not(all(Py_LIMITED_API, not(Py_3_9))))), - doc = "```rust,ignore" - )] - #[cfg_attr( - all(feature = "macros", not(all(Py_LIMITED_API, not(Py_3_9)))), - doc = "```rust" - )] - /// use pyo3::prelude::*; - /// use pyo3::types::PyWeakrefProxy; - /// - /// #[pyclass(weakref)] - /// struct Foo { /* fields omitted */ } - /// - /// #[pymethods] - /// impl Foo { - /// fn get_data(&self) -> (&str, u32) { - /// ("Dave", 10) - /// } - /// } - /// - /// fn parse_data(reference: Borrowed<'_, '_, PyWeakrefProxy>) -> PyResult { - /// if let Some(data_src) = reference.upgrade_as::()? { - /// let data = data_src.borrow(); - /// let (name, score) = data.get_data(); - /// Ok(format!("Processing '{}': score = {}", name, score)) - /// } else { - /// Ok("The supplied data reference is nolonger relavent.".to_owned()) - /// } - /// } - /// - /// # fn main() -> PyResult<()> { - /// Python::with_gil(|py| { - /// let data = Bound::new(py, Foo{})?; - /// let reference = PyWeakrefProxy::new_bound(&data)?; - /// - /// assert_eq!( - /// parse_data(reference.as_borrowed())?, - /// "Processing 'Dave': score = 10" - /// ); - /// - /// drop(data); - /// - /// assert_eq!( - /// parse_data(reference.as_borrowed())?, - /// "The supplied data reference is nolonger relavent." - /// ); - /// - /// Ok(()) - /// }) - /// # } - /// ``` - /// - /// # Panics - /// This function panics is the current object is invalid. - /// If used propperly this is never the case. (NonNull and actually a weakref type) - /// - /// [`PyWeakref_GetObject`]: https://docs.python.org/3/c-api/weakref.html#c.PyWeakref_GetObject - /// [`weakref.ProxyType`]: https://docs.python.org/3/library/weakref.html#weakref.ProxyType - /// [`weakref.proxy`]: https://docs.python.org/3/library/weakref.html#weakref.proxy - pub fn upgrade_as(&self) -> PyResult> - where - T: PyTypeCheck, - { - Ok(self - .as_borrowed() - .upgrade_as::()? - .map(Bound::into_gil_ref)) - } - - /// Upgrade the weakref to a direct object reference unchecked. The type of the recovered object is not checked before downcasting, this could lead to unexpected behavior. Use only when absolutely certain the type can be guaranteed. The `weakref` may still return `None`. - /// - /// It is named `upgrade` to be inline with [rust's `Weak::upgrade`](std::rc::Weak::upgrade). - /// In Python it would be equivalent to [`PyWeakref_GetObject`]. - /// - /// # Safety - /// Callers must ensure that the type is valid or risk type confusion. - /// The `weakref` is still allowed to be `None`, if the referenced object has been cleaned up. - /// - /// # Example - #[cfg_attr( - not(all(feature = "macros", not(all(Py_LIMITED_API, not(Py_3_9))))), - doc = "```rust,ignore" - )] - #[cfg_attr( - all(feature = "macros", not(all(Py_LIMITED_API, not(Py_3_9)))), - doc = "```rust" - )] - /// use pyo3::prelude::*; - /// use pyo3::types::PyWeakrefProxy; - /// - /// #[pyclass(weakref)] - /// struct Foo { /* fields omitted */ } - /// - /// #[pymethods] - /// impl Foo { - /// fn get_data(&self) -> (&str, u32) { - /// ("Dave", 10) - /// } - /// } - /// - /// fn parse_data(reference: Borrowed<'_, '_, PyWeakrefProxy>) -> String { - /// if let Some(data_src) = unsafe { reference.upgrade_as_unchecked::() } { - /// let data = data_src.borrow(); - /// let (name, score) = data.get_data(); - /// format!("Processing '{}': score = {}", name, score) - /// } else { - /// "The supplied data reference is nolonger relavent.".to_owned() - /// } - /// } - /// - /// # fn main() -> PyResult<()> { - /// Python::with_gil(|py| { - /// let data = Bound::new(py, Foo{})?; - /// let reference = PyWeakrefProxy::new_bound(&data)?; - /// - /// assert_eq!( - /// parse_data(reference.as_borrowed()), - /// "Processing 'Dave': score = 10" - /// ); - /// - /// drop(data); - /// - /// assert_eq!( - /// parse_data(reference.as_borrowed()), - /// "The supplied data reference is nolonger relavent." - /// ); - /// - /// Ok(()) - /// }) - /// # } - /// ``` - /// - /// # Panics - /// This function panics is the current object is invalid. - /// If used propperly this is never the case. (NonNull and actually a weakref type) - /// - /// [`PyWeakref_GetObject`]: https://docs.python.org/3/c-api/weakref.html#c.PyWeakref_GetObject - /// [`weakref.ProxyType`]: https://docs.python.org/3/library/weakref.html#weakref.ProxyType - /// [`weakref.proxy`]: https://docs.python.org/3/library/weakref.html#weakref.proxy - pub unsafe fn upgrade_as_unchecked(&self) -> Option<&T::AsRefTarget> - where - T: PyTypeCheck, - { - self.as_borrowed() - .upgrade_as_unchecked::() - .map(Bound::into_gil_ref) - } - - /// Upgrade the weakref to an exact direct object reference. - /// - /// It is named `upgrade` to be inline with [rust's `Weak::upgrade`](std::rc::Weak::upgrade). - /// In Python it would be equivalent to [`PyWeakref_GetObject`]. - /// - /// # Example - #[cfg_attr( - not(all(feature = "macros", not(all(Py_LIMITED_API, not(Py_3_9))))), - doc = "```rust,ignore" - )] - #[cfg_attr( - all(feature = "macros", not(all(Py_LIMITED_API, not(Py_3_9)))), - doc = "```rust" - )] - /// use pyo3::prelude::*; - /// use pyo3::types::PyWeakrefProxy; - /// - /// #[pyclass(weakref)] - /// struct Foo { /* fields omitted */ } - /// - /// #[pymethods] - /// impl Foo { - /// fn get_data(&self) -> (&str, u32) { - /// ("Dave", 10) - /// } - /// } - /// - /// fn parse_data(reference: Borrowed<'_, '_, PyWeakrefProxy>) -> PyResult { - /// if let Some(data_src) = reference.upgrade_as_exact::()? { - /// let data = data_src.borrow(); - /// let (name, score) = data.get_data(); - /// Ok(format!("Processing '{}': score = {}", name, score)) - /// } else { - /// Ok("The supplied data reference is nolonger relavent.".to_owned()) - /// } - /// } - /// - /// # fn main() -> PyResult<()> { - /// Python::with_gil(|py| { - /// let data = Bound::new(py, Foo{})?; - /// let reference = PyWeakrefProxy::new_bound(&data)?; - /// - /// assert_eq!( - /// parse_data(reference.as_borrowed())?, - /// "Processing 'Dave': score = 10" - /// ); - /// - /// drop(data); - /// - /// assert_eq!( - /// parse_data(reference.as_borrowed())?, - /// "The supplied data reference is nolonger relavent." - /// ); - /// - /// Ok(()) - /// }) - /// # } - /// ``` - /// - /// # Panics - /// This function panics is the current object is invalid. - /// If used propperly this is never the case. (NonNull and actually a weakref type) - /// - /// [`PyWeakref_GetObject`]: https://docs.python.org/3/c-api/weakref.html#c.PyWeakref_GetObject - /// [`weakref.ProxyType`]: https://docs.python.org/3/library/weakref.html#weakref.ProxyType - /// [`weakref.proxy`]: https://docs.python.org/3/library/weakref.html#weakref.proxy - pub fn upgrade_as_exact(&self) -> PyResult> - where - T: PyTypeInfo, - { - Ok(self - .as_borrowed() - .upgrade_as_exact::()? - .map(Bound::into_gil_ref)) - } - - /// Upgrade the weakref to a [`PyAny`] reference to the target if possible. - /// - /// It is named `upgrade` to be inline with [rust's `Weak::upgrade`](std::rc::Weak::upgrade). - /// This function returns `Some(&'py PyAny)` if the reference still exists, otherwise `None` will be returned. - /// - /// This function gets the optional target of this [`weakref.ProxyType`] (or [`weakref.CallableProxyType`], result of calling [`weakref.proxy`]). - /// It produces similair results using [`PyWeakref_GetObject`] in the C api. - /// - /// # Example - #[cfg_attr( - not(all(feature = "macros", not(all(Py_LIMITED_API, not(Py_3_9))))), - doc = "```rust,ignore" - )] - #[cfg_attr( - all(feature = "macros", not(all(Py_LIMITED_API, not(Py_3_9)))), - doc = "```rust" - )] - /// use pyo3::prelude::*; - /// use pyo3::types::PyWeakrefProxy; - /// - /// #[pyclass(weakref)] - /// struct Foo { /* fields omitted */ } - /// - /// fn parse_data(reference: Borrowed<'_, '_, PyWeakrefProxy>) -> PyResult { - /// if let Some(object) = reference.upgrade() { - /// Ok(format!("The object '{}' refered by this reference still exists.", object.getattr("__class__")?.getattr("__qualname__")?)) - /// } else { - /// Ok("The object, which this reference refered to, no longer exists".to_owned()) - /// } - /// } - /// - /// # fn main() -> PyResult<()> { - /// Python::with_gil(|py| { - /// let data = Bound::new(py, Foo{})?; - /// let reference = PyWeakrefProxy::new_bound(&data)?; - /// - /// assert_eq!( - /// parse_data(reference.as_borrowed())?, - /// "The object 'Foo' refered by this reference still exists." - /// ); - /// - /// drop(data); - /// - /// assert_eq!( - /// parse_data(reference.as_borrowed())?, - /// "The object, which this reference refered to, no longer exists" - /// ); - /// - /// Ok(()) - /// }) - /// # } - /// ``` - /// - /// # Panics - /// This function panics is the current object is invalid. - /// If used propperly this is never the case. (NonNull and actually a weakref type) - /// - /// [`PyWeakref_GetObject`]: https://docs.python.org/3/c-api/weakref.html#c.PyWeakref_GetObject - /// [`weakref.ProxyType`]: https://docs.python.org/3/library/weakref.html#weakref.ProxyType - /// [`weakref.CallableProxyType`]: https://docs.python.org/3/library/weakref.html#weakref.CallableProxyType - /// [`weakref.proxy`]: https://docs.python.org/3/library/weakref.html#weakref.proxy - pub fn upgrade(&self) -> Option<&'_ PyAny> { - self.as_borrowed().upgrade().map(Bound::into_gil_ref) - } - - /// Retrieve to a object pointed to by the weakref. - /// - /// This function returns `&'py PyAny`, which is either the object if it still exists, otherwise it will refer to [`PyNone`](crate::types::PyNone). - /// - /// This function gets the optional target of this [`weakref.ProxyType`] (or [`weakref.CallableProxyType`], result of calling [`weakref.proxy`]). - /// It produces similair results using [`PyWeakref_GetObject`] in the C api. - /// - /// # Example - #[cfg_attr( - not(all(feature = "macros", not(all(Py_LIMITED_API, not(Py_3_9))))), - doc = "```rust,ignore" - )] - #[cfg_attr( - all(feature = "macros", not(all(Py_LIMITED_API, not(Py_3_9)))), - doc = "```rust" - )] - /// use pyo3::prelude::*; - /// use pyo3::types::PyWeakrefProxy; - /// - /// #[pyclass(weakref)] - /// struct Foo { /* fields omitted */ } - /// - /// fn get_class(reference: Borrowed<'_, '_, PyWeakrefProxy>) -> PyResult { - /// reference - /// .get_object() - /// .getattr("__class__")? - /// .repr() - /// .map(|repr| repr.to_string()) - /// } - /// - /// # fn main() -> PyResult<()> { - /// Python::with_gil(|py| { - /// let object = Bound::new(py, Foo{})?; - /// let reference = PyWeakrefProxy::new_bound(&object)?; - /// - /// assert_eq!( - /// get_class(reference.as_borrowed())?, - /// "" - /// ); - /// - /// drop(object); - /// - /// assert_eq!(get_class(reference.as_borrowed())?, ""); - /// - /// Ok(()) - /// }) - /// # } - /// ``` - /// - /// # Panics - /// This function panics is the current object is invalid. - /// If used propperly this is never the case. (NonNull and actually a weakref type) - /// - /// [`PyWeakref_GetObject`]: https://docs.python.org/3/c-api/weakref.html#c.PyWeakref_GetObject - /// [`weakref.ProxyType`]: https://docs.python.org/3/library/weakref.html#weakref.ProxyType - /// [`weakref.CallableProxyType`]: https://docs.python.org/3/library/weakref.html#weakref.CallableProxyType - /// [`weakref.proxy`]: https://docs.python.org/3/library/weakref.html#weakref.proxy - pub fn get_object(&self) -> &'_ PyAny { - self.as_borrowed().get_object().into_gil_ref() - } -} - impl<'py> PyWeakrefMethods<'py> for Bound<'py, PyWeakrefProxy> { fn get_object_borrowed(&self) -> Borrowed<'_, 'py, PyAny> { // PyWeakref_GetObject does some error checking, however we ensure the passed object is Non-Null and a Weakref type. diff --git a/src/types/weakref/reference.rs b/src/types/weakref/reference.rs index 6cdcde3a7f7..634257d4a3e 100644 --- a/src/types/weakref/reference.rs +++ b/src/types/weakref/reference.rs @@ -4,10 +4,8 @@ use crate::py_result_ext::PyResultExt; use crate::types::any::PyAny; use crate::{ffi, Borrowed, Bound, ToPyObject}; -#[cfg(any(any(PyPy, GraalPy, Py_LIMITED_API), feature = "gil-refs"))] +#[cfg(any(PyPy, GraalPy, Py_LIMITED_API))] use crate::type_object::PyTypeCheck; -#[cfg(feature = "gil-refs")] -use crate::{type_object::PyTypeInfo, PyNativeType}; use super::PyWeakrefMethods; @@ -175,391 +173,6 @@ impl PyWeakrefReference { } } -#[cfg(feature = "gil-refs")] -impl PyWeakrefReference { - /// Deprecated form of [`PyWeakrefReference::new_bound`]. - #[inline] - #[deprecated( - since = "0.21.0", - note = "`PyWeakrefReference::new` will be replaced by `PyWeakrefReference::new_bound` in a future PyO3 version" - )] - pub fn new(object: &T) -> PyResult<&PyWeakrefReference> - where - T: PyNativeType, - { - Self::new_bound(object.as_borrowed().as_any()).map(Bound::into_gil_ref) - } - - /// Deprecated form of [`PyWeakrefReference::new_bound_with`]. - #[inline] - #[deprecated( - since = "0.21.0", - note = "`PyWeakrefReference::new_with` will be replaced by `PyWeakrefReference::new_bound_with` in a future PyO3 version" - )] - pub fn new_with(object: &T, callback: C) -> PyResult<&PyWeakrefReference> - where - T: PyNativeType, - C: ToPyObject, - { - Self::new_bound_with(object.as_borrowed().as_any(), callback).map(Bound::into_gil_ref) - } - - /// Upgrade the weakref to a direct object reference. - /// - /// It is named `upgrade` to be inline with [rust's `Weak::upgrade`](std::rc::Weak::upgrade). - /// In Python it would be equivalent to [`PyWeakref_GetObject`] or calling the [`weakref.ReferenceType`] (result of calling [`weakref.ref`]). - /// - /// # Example - #[cfg_attr( - not(all(feature = "macros", not(all(Py_LIMITED_API, not(Py_3_9))))), - doc = "```rust,ignore" - )] - #[cfg_attr( - all(feature = "macros", not(all(Py_LIMITED_API, not(Py_3_9)))), - doc = "```rust" - )] - /// use pyo3::prelude::*; - /// use pyo3::types::PyWeakrefReference; - /// - /// #[pyclass(weakref)] - /// struct Foo { /* fields omitted */ } - /// - /// #[pymethods] - /// impl Foo { - /// fn get_data(&self) -> (&str, u32) { - /// ("Dave", 10) - /// } - /// } - /// - /// fn parse_data(reference: Borrowed<'_, '_, PyWeakrefReference>) -> PyResult { - /// if let Some(data_src) = reference.upgrade_as::()? { - /// let data = data_src.borrow(); - /// let (name, score) = data.get_data(); - /// Ok(format!("Processing '{}': score = {}", name, score)) - /// } else { - /// Ok("The supplied data reference is nolonger relavent.".to_owned()) - /// } - /// } - /// - /// # fn main() -> PyResult<()> { - /// Python::with_gil(|py| { - /// let data = Bound::new(py, Foo{})?; - /// let reference = PyWeakrefReference::new_bound(&data)?; - /// - /// assert_eq!( - /// parse_data(reference.as_borrowed())?, - /// "Processing 'Dave': score = 10" - /// ); - /// - /// drop(data); - /// - /// assert_eq!( - /// parse_data(reference.as_borrowed())?, - /// "The supplied data reference is nolonger relavent." - /// ); - /// - /// Ok(()) - /// }) - /// # } - /// ``` - /// - /// # Panics - /// This function panics is the current object is invalid. - /// If used propperly this is never the case. (NonNull and actually a weakref type) - /// - /// [`PyWeakref_GetObject`]: https://docs.python.org/3/c-api/weakref.html#c.PyWeakref_GetObject - /// [`weakref.ReferenceType`]: https://docs.python.org/3/library/weakref.html#weakref.ReferenceType - /// [`weakref.ref`]: https://docs.python.org/3/library/weakref.html#weakref.ref - pub fn upgrade_as(&self) -> PyResult> - where - T: PyTypeCheck, - { - Ok(self - .as_borrowed() - .upgrade_as::()? - .map(Bound::into_gil_ref)) - } - - /// Upgrade the weakref to a direct object reference unchecked. The type of the recovered object is not checked before downcasting, this could lead to unexpected behavior. Use only when absolutely certain the type can be guaranteed. The `weakref` may still return `None`. - /// - /// It is named `upgrade` to be inline with [rust's `Weak::upgrade`](std::rc::Weak::upgrade). - /// In Python it would be equivalent to [`PyWeakref_GetObject`] or calling the [`weakref.ReferenceType`] (result of calling [`weakref.ref`]). - /// - /// # Safety - /// Callers must ensure that the type is valid or risk type confusion. - /// The `weakref` is still allowed to be `None`, if the referenced object has been cleaned up. - /// - /// # Example - #[cfg_attr( - not(all(feature = "macros", not(all(Py_LIMITED_API, not(Py_3_9))))), - doc = "```rust,ignore" - )] - #[cfg_attr( - all(feature = "macros", not(all(Py_LIMITED_API, not(Py_3_9)))), - doc = "```rust" - )] - /// use pyo3::prelude::*; - /// use pyo3::types::PyWeakrefReference; - /// - /// #[pyclass(weakref)] - /// struct Foo { /* fields omitted */ } - /// - /// #[pymethods] - /// impl Foo { - /// fn get_data(&self) -> (&str, u32) { - /// ("Dave", 10) - /// } - /// } - /// - /// fn parse_data(reference: Borrowed<'_, '_, PyWeakrefReference>) -> String { - /// if let Some(data_src) = unsafe { reference.upgrade_as_unchecked::() } { - /// let data = data_src.borrow(); - /// let (name, score) = data.get_data(); - /// format!("Processing '{}': score = {}", name, score) - /// } else { - /// "The supplied data reference is nolonger relavent.".to_owned() - /// } - /// } - /// - /// # fn main() -> PyResult<()> { - /// Python::with_gil(|py| { - /// let data = Bound::new(py, Foo{})?; - /// let reference = PyWeakrefReference::new_bound(&data)?; - /// - /// assert_eq!( - /// parse_data(reference.as_borrowed()), - /// "Processing 'Dave': score = 10" - /// ); - /// - /// drop(data); - /// - /// assert_eq!( - /// parse_data(reference.as_borrowed()), - /// "The supplied data reference is nolonger relavent." - /// ); - /// - /// Ok(()) - /// }) - /// # } - /// ``` - /// - /// # Panics - /// This function panics is the current object is invalid. - /// If used propperly this is never the case. (NonNull and actually a weakref type) - /// - /// [`PyWeakref_GetObject`]: https://docs.python.org/3/c-api/weakref.html#c.PyWeakref_GetObject - /// [`weakref.ReferenceType`]: https://docs.python.org/3/library/weakref.html#weakref.ReferenceType - /// [`weakref.ref`]: https://docs.python.org/3/library/weakref.html#weakref.ref - pub unsafe fn upgrade_as_unchecked(&self) -> Option<&T::AsRefTarget> - where - T: PyTypeCheck, - { - self.as_borrowed() - .upgrade_as_unchecked::() - .map(Bound::into_gil_ref) - } - - /// Upgrade the weakref to an exact direct object reference. - /// - /// It is named `upgrade` to be inline with [rust's `Weak::upgrade`](std::rc::Weak::upgrade). - /// In Python it would be equivalent to [`PyWeakref_GetObject`] or calling the [`weakref.ReferenceType`] (result of calling [`weakref.ref`]). - /// - /// # Example - #[cfg_attr( - not(all(feature = "macros", not(all(Py_LIMITED_API, not(Py_3_9))))), - doc = "```rust,ignore" - )] - #[cfg_attr( - all(feature = "macros", not(all(Py_LIMITED_API, not(Py_3_9)))), - doc = "```rust" - )] - /// use pyo3::prelude::*; - /// use pyo3::types::PyWeakrefReference; - /// - /// #[pyclass(weakref)] - /// struct Foo { /* fields omitted */ } - /// - /// #[pymethods] - /// impl Foo { - /// fn get_data(&self) -> (&str, u32) { - /// ("Dave", 10) - /// } - /// } - /// - /// fn parse_data(reference: Borrowed<'_, '_, PyWeakrefReference>) -> PyResult { - /// if let Some(data_src) = reference.upgrade_as_exact::()? { - /// let data = data_src.borrow(); - /// let (name, score) = data.get_data(); - /// Ok(format!("Processing '{}': score = {}", name, score)) - /// } else { - /// Ok("The supplied data reference is nolonger relavent.".to_owned()) - /// } - /// } - /// - /// # fn main() -> PyResult<()> { - /// Python::with_gil(|py| { - /// let data = Bound::new(py, Foo{})?; - /// let reference = PyWeakrefReference::new_bound(&data)?; - /// - /// assert_eq!( - /// parse_data(reference.as_borrowed())?, - /// "Processing 'Dave': score = 10" - /// ); - /// - /// drop(data); - /// - /// assert_eq!( - /// parse_data(reference.as_borrowed())?, - /// "The supplied data reference is nolonger relavent." - /// ); - /// - /// Ok(()) - /// }) - /// # } - /// ``` - /// - /// # Panics - /// This function panics is the current object is invalid. - /// If used propperly this is never the case. (NonNull and actually a weakref type) - /// - /// [`PyWeakref_GetObject`]: https://docs.python.org/3/c-api/weakref.html#c.PyWeakref_GetObject - /// [`weakref.ReferenceType`]: https://docs.python.org/3/library/weakref.html#weakref.ReferenceType - /// [`weakref.ref`]: https://docs.python.org/3/library/weakref.html#weakref.ref - pub fn upgrade_as_exact(&self) -> PyResult> - where - T: PyTypeInfo, - { - Ok(self - .as_borrowed() - .upgrade_as_exact::()? - .map(Bound::into_gil_ref)) - } - - /// Upgrade the weakref to a [`PyAny`] reference to the target if possible. - /// - /// It is named `upgrade` to be inline with [rust's `Weak::upgrade`](std::rc::Weak::upgrade). - /// This function returns `Some(&'py PyAny)` if the reference still exists, otherwise `None` will be returned. - /// - /// This function gets the optional target of this [`weakref.ReferenceType`] (result of calling [`weakref.ref`]). - /// It produces similair results to calling the `weakref.ReferenceType` or using [`PyWeakref_GetObject`] in the C api. - /// - /// # Example - #[cfg_attr( - not(all(feature = "macros", not(all(Py_LIMITED_API, not(Py_3_9))))), - doc = "```rust,ignore" - )] - #[cfg_attr( - all(feature = "macros", not(all(Py_LIMITED_API, not(Py_3_9)))), - doc = "```rust" - )] - /// use pyo3::prelude::*; - /// use pyo3::types::PyWeakrefReference; - /// - /// #[pyclass(weakref)] - /// struct Foo { /* fields omitted */ } - /// - /// fn parse_data(reference: Borrowed<'_, '_, PyWeakrefReference>) -> PyResult { - /// if let Some(object) = reference.upgrade() { - /// Ok(format!("The object '{}' refered by this reference still exists.", object.getattr("__class__")?.getattr("__qualname__")?)) - /// } else { - /// Ok("The object, which this reference refered to, no longer exists".to_owned()) - /// } - /// } - /// - /// # fn main() -> PyResult<()> { - /// Python::with_gil(|py| { - /// let data = Bound::new(py, Foo{})?; - /// let reference = PyWeakrefReference::new_bound(&data)?; - /// - /// assert_eq!( - /// parse_data(reference.as_borrowed())?, - /// "The object 'Foo' refered by this reference still exists." - /// ); - /// - /// drop(data); - /// - /// assert_eq!( - /// parse_data(reference.as_borrowed())?, - /// "The object, which this reference refered to, no longer exists" - /// ); - /// - /// Ok(()) - /// }) - /// # } - /// ``` - /// - /// # Panics - /// This function panics is the current object is invalid. - /// If used propperly this is never the case. (NonNull and actually a weakref type) - /// - /// [`PyWeakref_GetObject`]: https://docs.python.org/3/c-api/weakref.html#c.PyWeakref_GetObject - /// [`weakref.ReferenceType`]: https://docs.python.org/3/library/weakref.html#weakref.ReferenceType - /// [`weakref.ref`]: https://docs.python.org/3/library/weakref.html#weakref.ref - pub fn upgrade(&self) -> Option<&'_ PyAny> { - self.as_borrowed().upgrade().map(Bound::into_gil_ref) - } - - /// Retrieve to a object pointed to by the weakref. - /// - /// This function returns `&'py PyAny`, which is either the object if it still exists, otherwise it will refer to [`PyNone`](crate::types::PyNone). - /// - /// This function gets the optional target of this [`weakref.ReferenceType`] (result of calling [`weakref.ref`]). - /// It produces similair results to calling the `weakref.ReferenceType` or using [`PyWeakref_GetObject`] in the C api. - /// - /// # Example - #[cfg_attr( - not(all(feature = "macros", not(all(Py_LIMITED_API, not(Py_3_9))))), - doc = "```rust,ignore" - )] - #[cfg_attr( - all(feature = "macros", not(all(Py_LIMITED_API, not(Py_3_9)))), - doc = "```rust" - )] - /// use pyo3::prelude::*; - /// use pyo3::types::PyWeakrefReference; - /// - /// #[pyclass(weakref)] - /// struct Foo { /* fields omitted */ } - /// - /// fn get_class(reference: Borrowed<'_, '_, PyWeakrefReference>) -> PyResult { - /// reference - /// .get_object() - /// .getattr("__class__")? - /// .repr() - /// .map(|repr| repr.to_string()) - /// } - /// - /// # fn main() -> PyResult<()> { - /// Python::with_gil(|py| { - /// let object = Bound::new(py, Foo{})?; - /// let reference = PyWeakrefReference::new_bound(&object)?; - /// - /// assert_eq!( - /// get_class(reference.as_borrowed())?, - /// "" - /// ); - /// - /// drop(object); - /// - /// assert_eq!(get_class(reference.as_borrowed())?, ""); - /// - /// Ok(()) - /// }) - /// # } - /// ``` - /// - /// # Panics - /// This function panics is the current object is invalid. - /// If used propperly this is never the case. (NonNull and actually a weakref type) - /// - /// [`PyWeakref_GetObject`]: https://docs.python.org/3/c-api/weakref.html#c.PyWeakref_GetObject - /// [`weakref.ReferenceType`]: https://docs.python.org/3/library/weakref.html#weakref.ReferenceType - /// [`weakref.ref`]: https://docs.python.org/3/library/weakref.html#weakref.ref - pub fn get_object(&self) -> &'_ PyAny { - self.as_borrowed().get_object().into_gil_ref() - } -} - impl<'py> PyWeakrefMethods<'py> for Bound<'py, PyWeakrefReference> { fn get_object_borrowed(&self) -> Borrowed<'_, 'py, PyAny> { // PyWeakref_GetObject does some error checking, however we ensure the passed object is Non-Null and a Weakref type. diff --git a/tests/test_no_imports.rs b/tests/test_no_imports.rs index 3509a11f4be..c30148b0d25 100644 --- a/tests/test_no_imports.rs +++ b/tests/test_no_imports.rs @@ -10,20 +10,6 @@ fn basic_function(py: pyo3::Python<'_>, x: Option) -> pyo3::PyOb x.unwrap_or_else(|| py.None()) } -#[cfg(feature = "gil-refs")] -#[allow(deprecated)] -#[pyo3::pymodule] -fn basic_module(_py: pyo3::Python<'_>, m: &pyo3::types::PyModule) -> pyo3::PyResult<()> { - #[pyfn(m)] - fn answer() -> usize { - 42 - } - - m.add_function(pyo3::wrap_pyfunction!(basic_function, m)?)?; - - Ok(()) -} - #[pyo3::pymodule] fn basic_module_bound(m: &pyo3::Bound<'_, pyo3::types::PyModule>) -> pyo3::PyResult<()> { #[pyfn(m)] From 2c7853fa773e1002c1f2c891b19839c4d4623c40 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Fri, 12 Jul 2024 13:14:39 +0100 Subject: [PATCH 158/495] docs: remove reference to `IterNextOutput` (#4339) --- guide/src/class/protocols.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/guide/src/class/protocols.md b/guide/src/class/protocols.md index 4e5f6010e6d..c5ad847834f 100644 --- a/guide/src/class/protocols.md +++ b/guide/src/class/protocols.md @@ -225,15 +225,15 @@ documentation](https://docs.python.org/library/stdtypes.html#iterator-types). #### Returning a value from iteration This guide has so far shown how to use `Option` to implement yielding values -during iteration. In Python a generator can also return a value. To express -this in Rust, PyO3 provides the [`IterNextOutput`] enum to both `Yield` values -and `Return` a final value - see its docs for further details and an example. +during iteration. In Python a generator can also return a value. This is done by +raising a `StopIteration` exception. To express this in Rust, return `PyResult::Err` +with a `PyStopIteration` as the error. ### Awaitable objects - `__await__() -> object` - `__aiter__() -> object` - - `__anext__() -> Option or IterANextOutput` + - `__anext__() -> Option` ### Mapping & Sequence types @@ -458,6 +458,5 @@ i.e. `Python::with_gil` will panic. > Note: these methods are part of the C API, PyPy does not necessarily honor them. If you are building for PyPy you should measure memory consumption to make sure you do not have runaway memory growth. See [this issue on the PyPy bug tracker](https://github.com/pypy/pypy/issues/3848). -[`IterNextOutput`]: {{#PYO3_DOCS_URL}}/pyo3/pyclass/enum.IterNextOutput.html [`PySequence`]: {{#PYO3_DOCS_URL}}/pyo3/types/struct.PySequence.html [`CompareOp::matches`]: {{#PYO3_DOCS_URL}}/pyo3/pyclass/enum.CompareOp.html#method.matches From 397e6d8d556ea29c281d398298c2b472dec96095 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Fri, 12 Jul 2024 17:13:00 +0100 Subject: [PATCH 159/495] use FFI calls for refcounting on all abi3 versions (#4324) * use FFI calls for refcounting on all abi3 versions * fix implementation on PyPy --- newsfragments/4324.changed.md | 1 + pyo3-ffi/src/object.rs | 91 ++++++++++++++--------------------- 2 files changed, 36 insertions(+), 56 deletions(-) create mode 100644 newsfragments/4324.changed.md diff --git a/newsfragments/4324.changed.md b/newsfragments/4324.changed.md new file mode 100644 index 00000000000..2818159dda3 --- /dev/null +++ b/newsfragments/4324.changed.md @@ -0,0 +1 @@ +Use FFI function calls for reference counting on all abi3 versions. diff --git a/pyo3-ffi/src/object.rs b/pyo3-ffi/src/object.rs index 7acd0897217..21161a34d3a 100644 --- a/pyo3-ffi/src/object.rs +++ b/pyo3-ffi/src/object.rs @@ -490,11 +490,9 @@ extern "C" { #[cfg_attr(GraalPy, link_name = "_Py_DecRef")] pub fn Py_DecRef(o: *mut PyObject); - #[cfg(Py_3_10)] - #[cfg_attr(PyPy, link_name = "_PyPy_IncRef")] + #[cfg(all(Py_3_10, not(PyPy)))] pub fn _Py_IncRef(o: *mut PyObject); - #[cfg(Py_3_10)] - #[cfg_attr(PyPy, link_name = "_PyPy_DecRef")] + #[cfg(all(Py_3_10, not(PyPy)))] pub fn _Py_DecRef(o: *mut PyObject); #[cfg(GraalPy)] @@ -509,35 +507,23 @@ extern "C" { #[inline(always)] pub unsafe fn Py_INCREF(op: *mut PyObject) { - #[cfg(any( - GraalPy, - all(Py_LIMITED_API, Py_3_12), - all( - py_sys_config = "Py_REF_DEBUG", - Py_3_10, - not(all(Py_3_12, not(Py_LIMITED_API))) - ) - ))] + // On limited API or with refcount debugging, let the interpreter do refcounting + #[cfg(any(Py_LIMITED_API, py_sys_config = "Py_REF_DEBUG", GraalPy))] { - _Py_IncRef(op); - } + // _Py_IncRef was added to the ABI in 3.10; skips null checks + #[cfg(all(Py_3_10, not(PyPy)))] + { + _Py_IncRef(op); + } - #[cfg(all(py_sys_config = "Py_REF_DEBUG", not(Py_3_10)))] - { - return Py_IncRef(op); + #[cfg(any(not(Py_3_10), PyPy))] + { + Py_IncRef(op); + } } - #[cfg(any( - all(Py_LIMITED_API, not(Py_3_12)), - all( - not(Py_LIMITED_API), - not(GraalPy), - any( - not(py_sys_config = "Py_REF_DEBUG"), - all(py_sys_config = "Py_REF_DEBUG", Py_3_12), - ) - ), - ))] + // version-specific builds are allowed to directly manipulate the reference count + #[cfg(not(any(any(Py_LIMITED_API, py_sys_config = "Py_REF_DEBUG", GraalPy))))] { #[cfg(all(Py_3_12, target_pointer_width = "64"))] { @@ -564,9 +550,6 @@ pub unsafe fn Py_INCREF(op: *mut PyObject) { // Skipped _Py_INCREF_STAT_INC - if anyone wants this, please file an issue // or submit a PR supporting Py_STATS build option and pystats.h - - #[cfg(all(py_sys_config = "Py_REF_DEBUG", Py_3_12))] - _Py_INCREF_IncRefTotal(); } } @@ -576,35 +559,31 @@ pub unsafe fn Py_INCREF(op: *mut PyObject) { track_caller )] pub unsafe fn Py_DECREF(op: *mut PyObject) { + // On limited API or with refcount debugging, let the interpreter do refcounting + // On 3.12+ we implement refcount debugging to get better assertion locations on negative refcounts #[cfg(any( - GraalPy, - all(Py_LIMITED_API, Py_3_12), - all( - py_sys_config = "Py_REF_DEBUG", - Py_3_10, - not(all(Py_3_12, not(Py_LIMITED_API))) - ) + Py_LIMITED_API, + all(py_sys_config = "Py_REF_DEBUG", not(Py_3_12)), + GraalPy ))] { - _Py_DecRef(op); - } + // _Py_DecRef was added to the ABI in 3.10; skips null checks + #[cfg(all(Py_3_10, not(PyPy)))] + { + _Py_DecRef(op); + } - #[cfg(all(py_sys_config = "Py_REF_DEBUG", not(Py_3_10)))] - { - return Py_DecRef(op); + #[cfg(any(not(Py_3_10), PyPy))] + { + Py_DecRef(op); + } } - #[cfg(any( - all(Py_LIMITED_API, not(Py_3_12)), - all( - not(Py_LIMITED_API), - not(GraalPy), - any( - not(py_sys_config = "Py_REF_DEBUG"), - all(py_sys_config = "Py_REF_DEBUG", Py_3_12), - ) - ), - ))] + #[cfg(not(any( + Py_LIMITED_API, + all(py_sys_config = "Py_REF_DEBUG", not(Py_3_12)), + GraalPy + )))] { #[cfg(Py_3_12)] if _Py_IsImmortal(op) != 0 { @@ -614,7 +593,7 @@ pub unsafe fn Py_DECREF(op: *mut PyObject) { // Skipped _Py_DECREF_STAT_INC - if anyone needs this, please file an issue // or submit a PR supporting Py_STATS build option and pystats.h - #[cfg(all(py_sys_config = "Py_REF_DEBUG", Py_3_12))] + #[cfg(py_sys_config = "Py_REF_DEBUG")] _Py_DECREF_DecRefTotal(); #[cfg(Py_3_12)] From e40e13bf3b9c91344cbf2d3fc74c6a0214245c9f Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Fri, 12 Jul 2024 20:56:51 +0100 Subject: [PATCH 160/495] clean up `test_no_imports` tests (#4341) --- tests/test_class_new.rs | 25 +++++++ tests/test_no_imports.rs | 155 --------------------------------------- 2 files changed, 25 insertions(+), 155 deletions(-) delete mode 100644 tests/test_no_imports.rs diff --git a/tests/test_class_new.rs b/tests/test_class_new.rs index 01081d7afb0..1ccc152317d 100644 --- a/tests/test_class_new.rs +++ b/tests/test_class_new.rs @@ -298,3 +298,28 @@ fn test_new_returns_bound() { assert!(obj.is_exact_instance_of::()); }) } + +#[pyo3::pyclass] +struct NewClassMethod { + #[pyo3(get)] + cls: pyo3::PyObject, +} + +#[pyo3::pymethods] +impl NewClassMethod { + #[new] + #[classmethod] + fn new(cls: &pyo3::Bound<'_, pyo3::types::PyType>) -> Self { + Self { + cls: cls.clone().into_any().unbind(), + } + } +} + +#[test] +fn test_new_class_method() { + pyo3::Python::with_gil(|py| { + let cls = py.get_type_bound::(); + pyo3::py_run!(py, cls, "assert cls().cls is cls"); + }); +} diff --git a/tests/test_no_imports.rs b/tests/test_no_imports.rs deleted file mode 100644 index c30148b0d25..00000000000 --- a/tests/test_no_imports.rs +++ /dev/null @@ -1,155 +0,0 @@ -//! Tests that various macros work correctly without any PyO3 imports. - -#![cfg(feature = "macros")] - -use pyo3::prelude::PyAnyMethods; - -#[pyo3::pyfunction] -#[pyo3(name = "identity", signature = (x = None))] -fn basic_function(py: pyo3::Python<'_>, x: Option) -> pyo3::PyObject { - x.unwrap_or_else(|| py.None()) -} - -#[pyo3::pymodule] -fn basic_module_bound(m: &pyo3::Bound<'_, pyo3::types::PyModule>) -> pyo3::PyResult<()> { - #[pyfn(m)] - fn answer() -> usize { - 42 - } - - pyo3::types::PyModuleMethods::add_function( - m, - pyo3::wrap_pyfunction_bound!(basic_function, m)?, - )?; - - Ok(()) -} - -#[pyo3::pyclass] -struct BasicClass { - #[pyo3(get)] - v: usize, - #[pyo3(get, set)] - s: String, -} - -#[pyo3::pymethods] -impl BasicClass { - #[classattr] - const OKAY: bool = true; - - #[new] - fn new(arg: &pyo3::Bound<'_, pyo3::PyAny>) -> pyo3::PyResult { - if let Ok(v) = arg.extract::() { - Ok(Self { - v, - s: "".to_string(), - }) - } else { - Ok(Self { - v: 0, - s: arg.extract()?, - }) - } - } - - #[getter] - fn get_property(&self) -> usize { - self.v * 100 - } - - #[setter] - fn set_property(&mut self, value: usize) { - self.v = value / 100 - } - - /// Some documentation here - #[classmethod] - fn classmethod<'a, 'py>( - cls: &'a pyo3::Bound<'py, pyo3::types::PyType>, - ) -> &'a pyo3::Bound<'py, pyo3::types::PyType> { - cls - } - - #[staticmethod] - fn staticmethod(py: pyo3::Python<'_>, v: usize) -> pyo3::Py { - use pyo3::IntoPy; - v.to_string().into_py(py) - } - - fn __add__(&self, other: usize) -> usize { - self.v + other - } - - fn __iadd__(&mut self, other: pyo3::PyRef<'_, Self>) { - self.v += other.v; - self.s.push_str(&other.s); - } - - fn mutate(mut slf: pyo3::PyRefMut<'_, Self>) { - slf.v += slf.v; - slf.s.push('!'); - } -} - -#[test] -fn test_basic() { - pyo3::Python::with_gil(|py| { - let module = pyo3::wrap_pymodule!(basic_module_bound)(py); - let cls = py.get_type_bound::(); - let d = pyo3::types::IntoPyDict::into_py_dict_bound( - [ - ("mod", module.bind(py).as_any()), - ("cls", &cls), - ("a", &cls.call1((8,)).unwrap()), - ("b", &cls.call1(("foo",)).unwrap()), - ], - py, - ); - - pyo3::py_run!(py, *d, "assert mod.answer() == 42"); - pyo3::py_run!(py, *d, "assert mod.identity() is None"); - pyo3::py_run!(py, *d, "v = object(); assert mod.identity(v) is v"); - pyo3::py_run!(py, *d, "assert cls.OKAY"); - pyo3::py_run!(py, *d, "assert (a.v, a.s) == (8, '')"); - pyo3::py_run!(py, *d, "assert (b.v, b.s) == (0, 'foo')"); - pyo3::py_run!(py, *d, "b.property = 314"); - pyo3::py_run!(py, *d, "assert b.property == 300"); - pyo3::py_run!( - py, - *d, - "assert cls.classmethod.__doc__ == 'Some documentation here'" - ); - pyo3::py_run!(py, *d, "assert cls.classmethod() is cls"); - pyo3::py_run!(py, *d, "assert cls.staticmethod(5) == '5'"); - pyo3::py_run!(py, *d, "a.s = 'bar'; assert a.s == 'bar'"); - pyo3::py_run!(py, *d, "a.mutate(); assert (a.v, a.s) == (16, 'bar!')"); - pyo3::py_run!(py, *d, "assert a + 9 == 25"); - pyo3::py_run!(py, *d, "b += a; assert (b.v, b.s) == (19, 'foobar!')"); - }); -} - -#[pyo3::pyclass] -struct NewClassMethod { - #[pyo3(get)] - cls: pyo3::PyObject, -} - -#[pyo3::pymethods] -impl NewClassMethod { - #[new] - #[classmethod] - fn new(cls: &pyo3::Bound<'_, pyo3::types::PyType>) -> Self { - Self { - cls: cls.clone().into_any().unbind(), - } - } -} - -#[test] -fn test_new_class_method() { - pyo3::Python::with_gil(|py| { - let cls = py.get_type_bound::(); - pyo3::py_run!(py, cls, "assert cls().cls is cls"); - }); -} From e51251be4772f12c6a89a48152aa59da500e7b88 Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Fri, 12 Jul 2024 22:00:16 +0200 Subject: [PATCH 161/495] remove more gil-ref stuff (#4342) --- guide/src/memory.md | 8 +- src/buffer.rs | 12 --- src/conversion.rs | 11 -- src/err/mod.rs | 186 --------------------------------- src/exceptions.rs | 95 ----------------- src/instance.rs | 32 ------ src/lib.rs | 2 - src/marshal.rs | 27 ----- src/types/any.rs | 127 +--------------------- src/types/bytearray.rs | 13 --- src/types/complex.rs | 19 ---- src/types/datetime.rs | 107 ------------------- src/types/ellipsis.rs | 1 - src/types/frozenset.rs | 39 ------- src/types/iterator.rs | 25 ----- src/types/mapping.rs | 1 - src/types/memoryview.rs | 13 --- src/types/mod.rs | 19 ---- src/types/none.rs | 1 - src/types/notimplemented.rs | 1 - src/types/sequence.rs | 1 - src/types/set.rs | 46 -------- src/types/tuple.rs | 49 --------- src/types/weakref/anyref.rs | 1 - src/types/weakref/proxy.rs | 1 - src/types/weakref/reference.rs | 2 - 26 files changed, 6 insertions(+), 833 deletions(-) diff --git a/guide/src/memory.md b/guide/src/memory.md index 38a31f4d0ef..41454b03500 100644 --- a/guide/src/memory.md +++ b/guide/src/memory.md @@ -33,7 +33,7 @@ held. (If PyO3 could not assume this, every PyO3 API would need to take a `Python` GIL token to prove that the GIL is held.) This allows us to write very simple and easy-to-understand programs like this: -```rust +```rust,ignore # #![allow(unused_imports)] # use pyo3::prelude::*; # use pyo3::types::PyString; @@ -58,7 +58,7 @@ the `GILPool` is also dropped and the Python reference counts of the variables it owns are decreased, releasing them to the Python garbage collector. Most of the time we don't have to think about this, but consider the following: -```rust +```rust,ignore # #![allow(unused_imports)] # use pyo3::prelude::*; # use pyo3::types::PyString; @@ -99,7 +99,7 @@ PyO3 0.21 has introduced a new API known as the Bound API, which doesn't have th In general we don't want unbounded memory growth during loops! One workaround is to acquire and release the GIL with each iteration of the loop. -```rust +```rust,ignore # #![allow(unused_imports)] # use pyo3::prelude::*; # use pyo3::types::PyString; @@ -123,7 +123,7 @@ It might not be practical or performant to acquire and release the GIL so many times. Another workaround is to work with the `GILPool` object directly, but this is unsafe. -```rust +```rust,ignore # #![allow(unused_imports)] # use pyo3::prelude::*; # use pyo3::types::PyString; diff --git a/src/buffer.rs b/src/buffer.rs index 85e0e4ce990..2a6d602d567 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -19,8 +19,6 @@ //! `PyBuffer` implementation use crate::Bound; -#[cfg(feature = "gil-refs")] -use crate::PyNativeType; use crate::{err, exceptions::PyBufferError, ffi, FromPyObject, PyAny, PyResult, Python}; use std::marker::PhantomData; use std::os::raw; @@ -191,16 +189,6 @@ impl<'py, T: Element> FromPyObject<'py> for PyBuffer { } impl PyBuffer { - /// Deprecated form of [`PyBuffer::get_bound`] - #[cfg(feature = "gil-refs")] - #[deprecated( - since = "0.21.0", - note = "`PyBuffer::get` will be replaced by `PyBuffer::get_bound` in a future PyO3 version" - )] - pub fn get(obj: &PyAny) -> PyResult> { - Self::get_bound(&obj.as_borrowed()) - } - /// Gets the underlying buffer from the specified python object. pub fn get_bound(obj: &Bound<'_, PyAny>) -> PyResult> { // TODO: use nightly API Box::new_uninit() once stable diff --git a/src/conversion.rs b/src/conversion.rs index 22ec199a035..79f5f7f13cf 100644 --- a/src/conversion.rs +++ b/src/conversion.rs @@ -349,17 +349,6 @@ where } } -#[allow(deprecated)] -#[cfg(feature = "gil-refs")] -impl<'py, T> FromPyObject<'py> for &'py crate::PyCell -where - T: PyClass, -{ - fn extract_bound(obj: &Bound<'py, PyAny>) -> PyResult { - obj.clone().into_gil_ref().downcast().map_err(Into::into) - } -} - impl FromPyObject<'_> for T where T: PyClass + Clone, diff --git a/src/err/mod.rs b/src/err/mod.rs index ad7d9b20dde..b8ede9c3e3e 100644 --- a/src/err/mod.rs +++ b/src/err/mod.rs @@ -3,8 +3,6 @@ use crate::panic::PanicException; use crate::type_object::PyTypeInfo; use crate::types::any::PyAnyMethods; use crate::types::{string::PyStringMethods, typeobject::PyTypeMethods, PyTraceback, PyType}; -#[cfg(feature = "gil-refs")] -use crate::PyNativeType; use crate::{ exceptions::{self, PyBaseException}, ffi, @@ -47,34 +45,6 @@ unsafe impl Sync for PyErr {} /// Represents the result of a Python call. pub type PyResult = Result; -/// Error that indicates a failure to convert a PyAny to a more specific Python type. -#[derive(Debug)] -#[cfg(feature = "gil-refs")] -pub struct PyDowncastError<'a> { - from: &'a PyAny, - to: Cow<'static, str>, -} - -#[cfg(feature = "gil-refs")] -impl<'a> PyDowncastError<'a> { - /// Create a new `PyDowncastError` representing a failure to convert the object - /// `from` into the type named in `to`. - pub fn new(from: &'a PyAny, to: impl Into>) -> Self { - PyDowncastError { - from, - to: to.into(), - } - } - - /// Compatibility API to convert the Bound variant `DowncastError` into the - /// gil-ref variant - pub(crate) fn from_downcast_err(DowncastError { from, to }: DowncastError<'a, 'a>) -> Self { - #[allow(deprecated)] - let from = unsafe { from.py().from_borrowed_ptr(from.as_ptr()) }; - Self { from, to } - } -} - /// Error that indicates a failure to convert a PyAny to a more specific Python type. #[derive(Debug)] pub struct DowncastError<'a, 'py> { @@ -194,19 +164,6 @@ impl PyErr { }))) } - /// Deprecated form of [`PyErr::from_type_bound`] - #[cfg(feature = "gil-refs")] - #[deprecated( - since = "0.21.0", - note = "`PyErr::from_type` will be replaced by `PyErr::from_type_bound` in a future PyO3 version" - )] - pub fn from_type(ty: &PyType, args: A) -> PyErr - where - A: PyErrArguments + Send + Sync + 'static, - { - PyErr::from_state(PyErrState::lazy(ty.into(), args)) - } - /// Constructs a new PyErr from the given Python type and arguments. /// /// `ty` is the exception type; usually one of the standard exceptions @@ -224,16 +181,6 @@ impl PyErr { PyErr::from_state(PyErrState::lazy(ty.unbind().into_any(), args)) } - /// Deprecated form of [`PyErr::from_value_bound`]. - #[cfg(feature = "gil-refs")] - #[deprecated( - since = "0.21.0", - note = "`PyErr::from_value` will be replaced by `PyErr::from_value_bound` in a future PyO3 version" - )] - pub fn from_value(obj: &PyAny) -> PyErr { - PyErr::from_value_bound(obj.as_borrowed().to_owned()) - } - /// Creates a new PyErr. /// /// If `obj` is a Python exception object, the PyErr will contain that object. @@ -282,16 +229,6 @@ impl PyErr { PyErr::from_state(state) } - /// Deprecated form of [`PyErr::get_type_bound`]. - #[cfg(feature = "gil-refs")] - #[deprecated( - since = "0.21.0", - note = "`PyErr::get_type` will be replaced by `PyErr::get_type_bound` in a future PyO3 version" - )] - pub fn get_type<'py>(&'py self, py: Python<'py>) -> &'py PyType { - self.get_type_bound(py).into_gil_ref() - } - /// Returns the type of this exception. /// /// # Examples @@ -307,16 +244,6 @@ impl PyErr { self.normalized(py).ptype(py) } - /// Deprecated form of [`PyErr::value_bound`]. - #[cfg(feature = "gil-refs")] - #[deprecated( - since = "0.21.0", - note = "`PyErr::value` will be replaced by `PyErr::value_bound` in a future PyO3 version" - )] - pub fn value<'py>(&'py self, py: Python<'py>) -> &'py PyBaseException { - self.value_bound(py).as_gil_ref() - } - /// Returns the value of this exception. /// /// # Examples @@ -349,16 +276,6 @@ impl PyErr { exc } - /// Deprecated form of [`PyErr::traceback_bound`]. - #[cfg(feature = "gil-refs")] - #[deprecated( - since = "0.21.0", - note = "`PyErr::traceback` will be replaced by `PyErr::traceback_bound` in a future PyO3 version" - )] - pub fn traceback<'py>(&'py self, py: Python<'py>) -> Option<&'py PyTraceback> { - self.normalized(py).ptraceback(py).map(|b| b.into_gil_ref()) - } - /// Returns the traceback of this exception object. /// /// # Examples @@ -500,28 +417,6 @@ impl PyErr { } } - /// Deprecated form of [`PyErr::new_type_bound`] - #[cfg(feature = "gil-refs")] - #[deprecated( - since = "0.21.0", - note = "`PyErr::new_type` will be replaced by `PyErr::new_type_bound` in a future PyO3 version" - )] - pub fn new_type( - py: Python<'_>, - name: &str, - doc: Option<&str>, - base: Option<&PyType>, - dict: Option, - ) -> PyResult> { - Self::new_type_bound( - py, - name, - doc, - base.map(PyNativeType::as_borrowed).as_deref(), - dict, - ) - } - /// Creates a new exception type with the given name and docstring. /// /// - `base` can be an existing exception type to subclass, or a tuple of classes. @@ -626,17 +521,6 @@ impl PyErr { self.is_instance_bound(py, exc.to_object(py).bind(py)) } - /// Deprecated form of `PyErr::is_instance_bound`. - #[cfg(feature = "gil-refs")] - #[deprecated( - since = "0.21.0", - note = "`PyErr::is_instance` will be replaced by `PyErr::is_instance_bound` in a future PyO3 version" - )] - #[inline] - pub fn is_instance(&self, py: Python<'_>, ty: &PyAny) -> bool { - self.is_instance_bound(py, &ty.as_borrowed()) - } - /// Returns true if the current exception is instance of `T`. #[inline] pub fn is_instance_bound(&self, py: Python<'_>, ty: &Bound<'_, PyAny>) -> bool { @@ -663,17 +547,6 @@ impl PyErr { .restore(py) } - /// Deprecated form of `PyErr::write_unraisable_bound`. - #[cfg(feature = "gil-refs")] - #[deprecated( - since = "0.21.0", - note = "`PyErr::write_unraisable` will be replaced by `PyErr::write_unraisable_bound` in a future PyO3 version" - )] - #[inline] - pub fn write_unraisable(self, py: Python<'_>, obj: Option<&PyAny>) { - self.write_unraisable_bound(py, obj.map(PyAny::as_borrowed).as_deref()) - } - /// Reports the error as unraisable. /// /// This calls `sys.unraisablehook()` using the current exception and obj argument. @@ -708,16 +581,6 @@ impl PyErr { unsafe { ffi::PyErr_WriteUnraisable(obj.map_or(std::ptr::null_mut(), Bound::as_ptr)) } } - /// Deprecated form of [`PyErr::warn_bound`]. - #[cfg(feature = "gil-refs")] - #[deprecated( - since = "0.21.0", - note = "`PyErr::warn` will be replaced by `PyErr::warn_bound` in a future PyO3 version" - )] - pub fn warn(py: Python<'_>, category: &PyAny, message: &str, stacklevel: i32) -> PyResult<()> { - Self::warn_bound(py, &category.as_borrowed(), message, stacklevel) - } - /// Issues a warning message. /// /// May return an `Err(PyErr)` if warnings-as-errors is enabled. @@ -755,32 +618,6 @@ impl PyErr { }) } - /// Deprecated form of [`PyErr::warn_explicit_bound`]. - #[cfg(feature = "gil-refs")] - #[deprecated( - since = "0.21.0", - note = "`PyErr::warn_explicit` will be replaced by `PyErr::warn_explicit_bound` in a future PyO3 version" - )] - pub fn warn_explicit( - py: Python<'_>, - category: &PyAny, - message: &str, - filename: &str, - lineno: i32, - module: Option<&str>, - registry: Option<&PyAny>, - ) -> PyResult<()> { - Self::warn_explicit_bound( - py, - &category.as_borrowed(), - message, - filename, - lineno, - module, - registry.map(PyNativeType::as_borrowed).as_deref(), - ) - } - /// Issues a warning message, with more control over the warning attributes. /// /// May return a `PyErr` if warnings-as-errors is enabled. @@ -998,29 +835,6 @@ where } } -/// Convert `PyDowncastError` to Python `TypeError`. -#[cfg(feature = "gil-refs")] -impl<'a> std::convert::From> for PyErr { - fn from(err: PyDowncastError<'_>) -> PyErr { - let args = PyDowncastErrorArguments { - from: err.from.get_type().into(), - to: err.to, - }; - - exceptions::PyTypeError::new_err(args) - } -} - -#[cfg(feature = "gil-refs")] -impl<'a> std::error::Error for PyDowncastError<'a> {} - -#[cfg(feature = "gil-refs")] -impl<'a> std::fmt::Display for PyDowncastError<'a> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { - display_downcast_error(f, &self.from.as_borrowed(), &self.to) - } -} - /// Convert `DowncastError` to Python `TypeError`. impl std::convert::From> for PyErr { fn from(err: DowncastError<'_, '_>) -> PyErr { diff --git a/src/exceptions.rs b/src/exceptions.rs index 496d614fc20..d5260729c1c 100644 --- a/src/exceptions.rs +++ b/src/exceptions.rs @@ -18,33 +18,8 @@ use std::ops; #[macro_export] macro_rules! impl_exception_boilerplate { ($name: ident) => { - // FIXME https://github.com/PyO3/pyo3/issues/3903 - #[allow(unknown_lints, non_local_definitions)] - #[cfg(feature = "gil-refs")] - impl ::std::convert::From<&$name> for $crate::PyErr { - #[inline] - fn from(err: &$name) -> $crate::PyErr { - #[allow(deprecated)] - $crate::PyErr::from_value(err) - } - } - $crate::impl_exception_boilerplate_bound!($name); - #[cfg(feature = "gil-refs")] - impl ::std::error::Error for $name { - fn source(&self) -> ::std::option::Option<&(dyn ::std::error::Error + 'static)> { - unsafe { - #[allow(deprecated)] - let cause: &$crate::exceptions::PyBaseException = self - .py() - .from_owned_ptr_or_opt($crate::ffi::PyException_GetCause(self.as_ptr()))?; - - ::std::option::Option::Some(cause) - } - } - } - impl $crate::ToPyErr for $name {} }; } @@ -653,22 +628,6 @@ impl_windows_native_exception!( ); impl PyUnicodeDecodeError { - /// Deprecated form of [`PyUnicodeDecodeError::new_bound`]. - #[cfg(feature = "gil-refs")] - #[deprecated( - since = "0.21.0", - note = "`PyUnicodeDecodeError::new` will be replaced by `PyUnicodeDecodeError::new_bound` in a future PyO3 version" - )] - pub fn new<'p>( - py: Python<'p>, - encoding: &CStr, - input: &[u8], - range: ops::Range, - reason: &CStr, - ) -> PyResult<&'p PyUnicodeDecodeError> { - Ok(PyUnicodeDecodeError::new_bound(py, encoding, input, range, reason)?.into_gil_ref()) - } - /// Creates a Python `UnicodeDecodeError`. pub fn new_bound<'p>( py: Python<'p>, @@ -693,20 +652,6 @@ impl PyUnicodeDecodeError { .downcast_into() } - /// Deprecated form of [`PyUnicodeDecodeError::new_utf8_bound`]. - #[cfg(feature = "gil-refs")] - #[deprecated( - since = "0.21.0", - note = "`PyUnicodeDecodeError::new_utf8` will be replaced by `PyUnicodeDecodeError::new_utf8_bound` in a future PyO3 version" - )] - pub fn new_utf8<'p>( - py: Python<'p>, - input: &[u8], - err: std::str::Utf8Error, - ) -> PyResult<&'p PyUnicodeDecodeError> { - Ok(PyUnicodeDecodeError::new_utf8_bound(py, input, err)?.into_gil_ref()) - } - /// Creates a Python `UnicodeDecodeError` from a Rust UTF-8 decoding error. /// /// # Examples @@ -821,16 +766,6 @@ macro_rules! test_exception { let value = err.value_bound(py).as_any().downcast::<$exc_ty>().unwrap(); - #[cfg(feature = "gil-refs")] - { - use std::error::Error; - let value = value.as_gil_ref(); - assert!(value.source().is_none()); - - err.set_cause(py, Some($crate::exceptions::PyValueError::new_err("a cause"))); - assert!(value.source().is_some()); - } - assert!($crate::PyErr::from(value.clone()).is_instance_of::<$exc_ty>(py)); }) } @@ -885,8 +820,6 @@ mod tests { use crate::types::any::PyAnyMethods; use crate::types::{IntoPyDict, PyDict}; use crate::PyErr; - #[cfg(feature = "gil-refs")] - use crate::PyNativeType; import_exception_bound!(socket, gaierror); import_exception_bound!(email.errors, MessageError); @@ -1075,34 +1008,6 @@ mod tests { }); } - #[test] - #[cfg(feature = "gil-refs")] - fn native_exception_chain() { - use std::error::Error; - - Python::with_gil(|py| { - #[allow(deprecated)] - let exc = py - .run_bound( - "raise Exception('banana') from TypeError('peach')", - None, - None, - ) - .expect_err("raising should have given us an error") - .into_value(py) - .into_ref(py); - - assert_eq!(format!("{:?}", exc), "Exception('banana')"); - - let source = exc.source().expect("cause should exist"); - - assert_eq!(format!("{:?}", source), "TypeError('peach')"); - - let source_source = source.source(); - assert!(source_source.is_none(), "source_source should be None"); - }); - } - #[test] fn unicode_decode_error() { let invalid_utf8 = b"fo\xd8o"; diff --git a/src/instance.rs b/src/instance.rs index 4bbdbbb184e..6967306a73f 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -680,20 +680,6 @@ impl<'a, 'py, T> From<&'a Bound<'py, T>> for Borrowed<'a, 'py, T> { } } -#[cfg(feature = "gil-refs")] -impl<'py, T> Borrowed<'py, 'py, T> -where - T: HasPyGilRef, -{ - pub(crate) fn into_gil_ref(self) -> &'py T::AsRefTarget { - // Safety: self is a borrow over `'py`. - #[allow(deprecated)] - unsafe { - self.py().from_borrowed_ptr(self.0.as_ptr()) - } - } -} - impl std::fmt::Debug for Borrowed<'_, '_, T> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { Bound::fmt(self, f) @@ -1898,24 +1884,6 @@ impl std::fmt::Debug for Py { pub type PyObject = Py; impl PyObject { - /// Deprecated form of [`PyObject::downcast_bound`] - #[cfg(feature = "gil-refs")] - #[deprecated( - since = "0.21.0", - note = "`PyObject::downcast` will be replaced by `PyObject::downcast_bound` in a future PyO3 version" - )] - #[inline] - pub fn downcast<'py, T>( - &'py self, - py: Python<'py>, - ) -> Result<&'py T, crate::err::PyDowncastError<'py>> - where - T: PyTypeCheck, - { - self.downcast_bound::(py) - .map(Bound::as_gil_ref) - .map_err(crate::err::PyDowncastError::from_downcast_err) - } /// Downcast this `PyObject` to a concrete Python type or pyclass. /// /// Note that you can often avoid downcasting yourself by just specifying diff --git a/src/lib.rs b/src/lib.rs index 5792a63e9b5..41525b16fa8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -317,8 +317,6 @@ //! [`Ungil`]: crate::marker::Ungil pub use crate::class::*; pub use crate::conversion::{AsPyPointer, FromPyObject, IntoPy, ToPyObject}; -#[cfg(feature = "gil-refs")] -pub use crate::err::PyDowncastError; pub use crate::err::{DowncastError, DowncastIntoError, PyErr, PyErrArguments, PyResult, ToPyErr}; #[cfg(feature = "gil-refs")] #[allow(deprecated)] diff --git a/src/marshal.rs b/src/marshal.rs index 3978f4873e1..dbd3c9a5a2d 100644 --- a/src/marshal.rs +++ b/src/marshal.rs @@ -12,20 +12,6 @@ use std::os::raw::c_int; /// The current version of the marshal binary format. pub const VERSION: i32 = 4; -/// Deprecated form of [`dumps_bound`] -#[cfg(feature = "gil-refs")] -#[deprecated( - since = "0.21.0", - note = "`dumps` will be replaced by `dumps_bound` in a future PyO3 version" -)] -pub fn dumps<'py>( - py: Python<'py>, - object: &impl AsPyPointer, - version: i32, -) -> PyResult<&'py PyBytes> { - dumps_bound(py, object, version).map(Bound::into_gil_ref) -} - /// Serialize an object to bytes using the Python built-in marshal module. /// /// The built-in marshalling only supports a limited range of objects. @@ -58,19 +44,6 @@ pub fn dumps_bound<'py>( } } -/// Deprecated form of [`loads_bound`] -#[cfg(feature = "gil-refs")] -#[deprecated( - since = "0.21.0", - note = "`loads` will be replaced by `loads_bound` in a future PyO3 version" -)] -pub fn loads<'py, B>(py: Python<'py>, data: &B) -> PyResult<&'py PyAny> -where - B: AsRef<[u8]> + ?Sized, -{ - loads_bound(py, data).map(Bound::into_gil_ref) -} - /// Deserialize an object from bytes using the Python built-in marshal module. pub fn loads_bound<'py, B>(py: Python<'py>, data: &B) -> PyResult> where diff --git a/src/types/any.rs b/src/types/any.rs index 0b45dab2c92..745847b86ee 100644 --- a/src/types/any.rs +++ b/src/types/any.rs @@ -10,9 +10,9 @@ use crate::type_object::{PyTypeCheck, PyTypeInfo}; #[cfg(not(any(PyPy, GraalPy)))] use crate::types::PySuper; use crate::types::{PyDict, PyIterator, PyList, PyString, PyTuple, PyType}; -use crate::{err, ffi, Py, Python}; #[cfg(feature = "gil-refs")] -use crate::{err::PyDowncastError, type_object::HasPyGilRef, PyNativeType}; +use crate::PyNativeType; +use crate::{err, ffi, Py, Python}; use std::cell::UnsafeCell; use std::cmp::Ordering; use std::os::raw::c_int; @@ -54,8 +54,6 @@ pyobject_native_type_info!( #checkfunction=PyObject_Check ); -pyobject_native_type_extract!(PyAny); - pyobject_native_type_sized!(PyAny, ffi::PyObject); #[cfg(feature = "gil-refs")] @@ -653,127 +651,6 @@ impl PyAny { self.as_borrowed().get_type_ptr() } - /// Downcast this `PyAny` to a concrete Python type or pyclass. - /// - /// Note that you can often avoid downcasting yourself by just specifying - /// the desired type in function or method signatures. - /// However, manual downcasting is sometimes necessary. - /// - /// For extracting a Rust-only type, see [`PyAny::extract`](struct.PyAny.html#method.extract). - /// - /// # Example: Downcasting to a specific Python object - /// - /// ```rust - /// use pyo3::prelude::*; - /// use pyo3::types::{PyDict, PyList}; - /// - /// Python::with_gil(|py| { - /// let dict = PyDict::new_bound(py); - /// assert!(dict.is_instance_of::()); - /// let any = dict.as_any(); - /// - /// assert!(any.downcast::().is_ok()); - /// assert!(any.downcast::().is_err()); - /// }); - /// ``` - /// - /// # Example: Getting a reference to a pyclass - /// - /// This is useful if you want to mutate a `PyObject` that - /// might actually be a pyclass. - /// - /// ```rust - /// # fn main() -> Result<(), pyo3::PyErr> { - /// use pyo3::prelude::*; - /// - /// #[pyclass] - /// struct Class { - /// i: i32, - /// } - /// - /// Python::with_gil(|py| { - /// let class = Py::new(py, Class { i: 0 }).unwrap().into_bound(py).into_any(); - /// - /// let class_bound: &Bound<'_, Class> = class.downcast()?; - /// - /// class_bound.borrow_mut().i += 1; - /// - /// // Alternatively you can get a `PyRefMut` directly - /// let class_ref: PyRefMut<'_, Class> = class.extract()?; - /// assert_eq!(class_ref.i, 1); - /// Ok(()) - /// }) - /// # } - /// ``` - #[inline] - pub fn downcast(&self) -> Result<&T, PyDowncastError<'_>> - where - T: PyTypeCheck, - { - if T::type_check(&self.as_borrowed()) { - // Safety: type_check is responsible for ensuring that the type is correct - Ok(unsafe { self.downcast_unchecked() }) - } else { - Err(PyDowncastError::new(self, T::NAME)) - } - } - - /// Downcast this `PyAny` to a concrete Python type or pyclass (but not a subclass of it). - /// - /// It is almost always better to use [`PyAny::downcast`] because it accounts for Python - /// subtyping. Use this method only when you do not want to allow subtypes. - /// - /// The advantage of this method over [`PyAny::downcast`] is that it is faster. The implementation - /// of `downcast_exact` uses the equivalent of the Python expression `type(self) is T`, whereas - /// `downcast` uses `isinstance(self, T)`. - /// - /// For extracting a Rust-only type, see [`PyAny::extract`](struct.PyAny.html#method.extract). - /// - /// # Example: Downcasting to a specific Python object but not a subtype - /// - /// ```rust - /// use pyo3::prelude::*; - /// use pyo3::types::{PyBool, PyLong}; - /// - /// Python::with_gil(|py| { - /// let b = PyBool::new_bound(py, true); - /// assert!(b.is_instance_of::()); - /// let any: &Bound<'_, PyAny> = b.as_any(); - /// - /// // `bool` is a subtype of `int`, so `downcast` will accept a `bool` as an `int` - /// // but `downcast_exact` will not. - /// assert!(any.downcast::().is_ok()); - /// assert!(any.downcast_exact::().is_err()); - /// - /// assert!(any.downcast_exact::().is_ok()); - /// }); - /// ``` - #[inline] - pub fn downcast_exact(&self) -> Result<&T, PyDowncastError<'_>> - where - T: PyTypeInfo, - { - if T::is_exact_type_of_bound(&self.as_borrowed()) { - // Safety: type_check is responsible for ensuring that the type is correct - Ok(unsafe { self.downcast_unchecked() }) - } else { - Err(PyDowncastError::new(self, T::NAME)) - } - } - - /// Converts this `PyAny` to a concrete Python type without checking validity. - /// - /// # Safety - /// - /// Callers must ensure that the type is valid or risk type confusion. - #[inline] - pub unsafe fn downcast_unchecked(&self) -> &T - where - T: HasPyGilRef, - { - &*(self.as_ptr() as *const T) - } - /// Extracts some type from the Python object. /// /// This is a wrapper function around diff --git a/src/types/bytearray.rs b/src/types/bytearray.rs index f93fc791865..9885f637cec 100644 --- a/src/types/bytearray.rs +++ b/src/types/bytearray.rs @@ -4,8 +4,6 @@ use crate::instance::{Borrowed, Bound}; use crate::py_result_ext::PyResultExt; use crate::types::any::PyAnyMethods; use crate::{ffi, PyAny, Python}; -#[cfg(feature = "gil-refs")] -use crate::{AsPyPointer, PyNativeType}; use std::slice; /// Represents a Python `bytearray`. @@ -304,17 +302,6 @@ impl<'a> Borrowed<'a, '_, PyByteArray> { } } -#[cfg(feature = "gil-refs")] -impl<'py> TryFrom<&'py PyAny> for &'py PyByteArray { - type Error = crate::PyErr; - - /// Creates a new Python `bytearray` object from another Python object that - /// implements the buffer protocol. - fn try_from(value: &'py PyAny) -> Result { - PyByteArray::from_bound(&value.as_borrowed()).map(Bound::into_gil_ref) - } -} - impl<'py> TryFrom<&Bound<'py, PyAny>> for Bound<'py, PyByteArray> { type Error = crate::PyErr; diff --git a/src/types/complex.rs b/src/types/complex.rs index 0f16cac73ad..4276f8424ad 100644 --- a/src/types/complex.rs +++ b/src/types/complex.rs @@ -1,8 +1,5 @@ #[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))] use crate::py_result_ext::PyResultExt; -#[cfg(feature = "gil-refs")] -#[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))] -use crate::PyNativeType; use crate::{ffi, types::any::PyAnyMethods, Bound, PyAny, Python}; use std::os::raw::c_double; @@ -66,14 +63,6 @@ mod not_limited_impls { } } - #[cfg(feature = "gil-refs")] - impl<'py> $trait for &'py PyComplex { - type Output = &'py PyComplex; - fn $fn(self, other: &'py PyComplex) -> &'py PyComplex { - (self.as_borrowed() $op other.as_borrowed()).into_gil_ref() - } - } - impl<'py> $trait for &Bound<'py, PyComplex> { type Output = Bound<'py, PyComplex>; fn $fn(self, other: &Bound<'py, PyComplex>) -> Bound<'py, PyComplex> { @@ -109,14 +98,6 @@ mod not_limited_impls { bin_ops!(Mul, mul, *); bin_ops!(Div, div, /); - #[cfg(feature = "gil-refs")] - impl<'py> Neg for &'py PyComplex { - type Output = &'py PyComplex; - fn neg(self) -> &'py PyComplex { - (-self.as_borrowed()).into_gil_ref() - } - } - impl<'py> Neg for Borrowed<'_, 'py, PyComplex> { type Output = Bound<'py, PyComplex>; fn neg(self) -> Self::Output { diff --git a/src/types/datetime.rs b/src/types/datetime.rs index 8743bd7a680..b8a344a60b1 100644 --- a/src/types/datetime.rs +++ b/src/types/datetime.rs @@ -22,8 +22,6 @@ use crate::ffi::{ PyDateTime_TIME_GET_MINUTE, PyDateTime_TIME_GET_SECOND, }; use crate::ffi_ptr_ext::FfiPtrExt; -#[cfg(feature = "gil-refs")] -use crate::instance::PyNativeType; use crate::py_result_ext::PyResultExt; use crate::types::any::PyAnyMethods; use crate::types::PyTuple; @@ -233,21 +231,6 @@ impl PyDate { } } -#[cfg(feature = "gil-refs")] -impl PyDateAccess for PyDate { - fn get_year(&self) -> i32 { - self.as_borrowed().get_year() - } - - fn get_month(&self) -> u8 { - self.as_borrowed().get_month() - } - - fn get_day(&self) -> u8 { - self.as_borrowed().get_day() - } -} - impl PyDateAccess for Bound<'_, PyDate> { fn get_year(&self) -> i32 { unsafe { PyDateTime_GET_YEAR(self.as_ptr()) } @@ -368,21 +351,6 @@ impl PyDateTime { } } -#[cfg(feature = "gil-refs")] -impl PyDateAccess for PyDateTime { - fn get_year(&self) -> i32 { - self.as_borrowed().get_year() - } - - fn get_month(&self) -> u8 { - self.as_borrowed().get_month() - } - - fn get_day(&self) -> u8 { - self.as_borrowed().get_day() - } -} - impl PyDateAccess for Bound<'_, PyDateTime> { fn get_year(&self) -> i32 { unsafe { PyDateTime_GET_YEAR(self.as_ptr()) } @@ -397,29 +365,6 @@ impl PyDateAccess for Bound<'_, PyDateTime> { } } -#[cfg(feature = "gil-refs")] -impl PyTimeAccess for PyDateTime { - fn get_hour(&self) -> u8 { - self.as_borrowed().get_hour() - } - - fn get_minute(&self) -> u8 { - self.as_borrowed().get_minute() - } - - fn get_second(&self) -> u8 { - self.as_borrowed().get_second() - } - - fn get_microsecond(&self) -> u32 { - self.as_borrowed().get_microsecond() - } - - fn get_fold(&self) -> bool { - self.as_borrowed().get_fold() - } -} - impl PyTimeAccess for Bound<'_, PyDateTime> { fn get_hour(&self) -> u8 { unsafe { PyDateTime_DATE_GET_HOUR(self.as_ptr()) as u8 } @@ -442,13 +387,6 @@ impl PyTimeAccess for Bound<'_, PyDateTime> { } } -#[cfg(feature = "gil-refs")] -impl<'py> PyTzInfoAccess<'py> for &'py PyDateTime { - fn get_tzinfo_bound(&self) -> Option> { - self.as_borrowed().get_tzinfo_bound() - } -} - impl<'py> PyTzInfoAccess<'py> for Bound<'py, PyDateTime> { fn get_tzinfo_bound(&self) -> Option> { let ptr = self.as_ptr() as *mut ffi::PyDateTime_DateTime; @@ -549,29 +487,6 @@ impl PyTime { } } -#[cfg(feature = "gil-refs")] -impl PyTimeAccess for PyTime { - fn get_hour(&self) -> u8 { - self.as_borrowed().get_hour() - } - - fn get_minute(&self) -> u8 { - self.as_borrowed().get_minute() - } - - fn get_second(&self) -> u8 { - self.as_borrowed().get_second() - } - - fn get_microsecond(&self) -> u32 { - self.as_borrowed().get_microsecond() - } - - fn get_fold(&self) -> bool { - self.as_borrowed().get_fold() - } -} - impl PyTimeAccess for Bound<'_, PyTime> { fn get_hour(&self) -> u8 { unsafe { PyDateTime_TIME_GET_HOUR(self.as_ptr()) as u8 } @@ -594,13 +509,6 @@ impl PyTimeAccess for Bound<'_, PyTime> { } } -#[cfg(feature = "gil-refs")] -impl<'py> PyTzInfoAccess<'py> for &'py PyTime { - fn get_tzinfo_bound(&self) -> Option> { - self.as_borrowed().get_tzinfo_bound() - } -} - impl<'py> PyTzInfoAccess<'py> for Bound<'py, PyTime> { fn get_tzinfo_bound(&self) -> Option> { let ptr = self.as_ptr() as *mut ffi::PyDateTime_Time; @@ -721,21 +629,6 @@ impl PyDelta { } } -#[cfg(feature = "gil-refs")] -impl PyDeltaAccess for PyDelta { - fn get_days(&self) -> i32 { - self.as_borrowed().get_days() - } - - fn get_seconds(&self) -> i32 { - self.as_borrowed().get_seconds() - } - - fn get_microseconds(&self) -> i32 { - self.as_borrowed().get_microseconds() - } -} - impl PyDeltaAccess for Bound<'_, PyDelta> { fn get_days(&self) -> i32 { unsafe { PyDateTime_DELTA_GET_DAYS(self.as_ptr()) } diff --git a/src/types/ellipsis.rs b/src/types/ellipsis.rs index 887016a0ae8..0f13c20d4e1 100644 --- a/src/types/ellipsis.rs +++ b/src/types/ellipsis.rs @@ -11,7 +11,6 @@ use crate::{ pub struct PyEllipsis(PyAny); pyobject_native_type_named!(PyEllipsis); -pyobject_native_type_extract!(PyEllipsis); impl PyEllipsis { /// Returns the `Ellipsis` object. diff --git a/src/types/frozenset.rs b/src/types/frozenset.rs index 845309fe2be..1e8cf947c39 100644 --- a/src/types/frozenset.rs +++ b/src/types/frozenset.rs @@ -1,6 +1,4 @@ use crate::types::PyIterator; -#[cfg(feature = "gil-refs")] -use crate::PyNativeType; use crate::{ err::{self, PyErr, PyResult}, ffi, @@ -148,43 +146,6 @@ impl<'py> PyFrozenSetMethods<'py> for Bound<'py, PyFrozenSet> { } } -/// PyO3 implementation of an iterator for a Python `frozenset` object. -#[cfg(feature = "gil-refs")] -pub struct PyFrozenSetIterator<'py>(BoundFrozenSetIterator<'py>); - -#[cfg(feature = "gil-refs")] -impl<'py> Iterator for PyFrozenSetIterator<'py> { - type Item = &'py super::PyAny; - - /// Advances the iterator and returns the next value. - #[inline] - fn next(&mut self) -> Option { - self.0.next().map(Bound::into_gil_ref) - } - - fn size_hint(&self) -> (usize, Option) { - self.0.size_hint() - } -} - -#[cfg(feature = "gil-refs")] -impl ExactSizeIterator for PyFrozenSetIterator<'_> { - #[inline] - fn len(&self) -> usize { - self.0.len() - } -} - -#[cfg(feature = "gil-refs")] -impl<'py> IntoIterator for &'py PyFrozenSet { - type Item = &'py PyAny; - type IntoIter = PyFrozenSetIterator<'py>; - /// Returns an iterator of values in this set. - fn into_iter(self) -> Self::IntoIter { - PyFrozenSetIterator(BoundFrozenSetIterator::new(self.as_borrowed().to_owned())) - } -} - impl<'py> IntoIterator for Bound<'py, PyFrozenSet> { type Item = Bound<'py, PyAny>; type IntoIter = BoundFrozenSetIterator<'py>; diff --git a/src/types/iterator.rs b/src/types/iterator.rs index 5602e34f40e..e89cb864cd6 100644 --- a/src/types/iterator.rs +++ b/src/types/iterator.rs @@ -2,8 +2,6 @@ use crate::ffi_ptr_ext::FfiPtrExt; use crate::instance::Borrowed; use crate::py_result_ext::PyResultExt; use crate::{ffi, Bound, PyAny, PyErr, PyResult, PyTypeCheck}; -#[cfg(feature = "gil-refs")] -use crate::{AsPyPointer, PyNativeType}; /// A Python iterator object. /// @@ -31,7 +29,6 @@ use crate::{AsPyPointer, PyNativeType}; #[repr(transparent)] pub struct PyIterator(PyAny); pyobject_native_type_named!(PyIterator); -pyobject_native_type_extract!(PyIterator); impl PyIterator { /// Builds an iterator for an iterable Python object; the equivalent of calling `iter(obj)` in Python. @@ -47,28 +44,6 @@ impl PyIterator { } } -#[cfg(feature = "gil-refs")] -impl<'p> Iterator for &'p PyIterator { - type Item = PyResult<&'p PyAny>; - - /// Retrieves the next item from an iterator. - /// - /// Returns `None` when the iterator is exhausted. - /// If an exception occurs, returns `Some(Err(..))`. - /// Further `next()` calls after an exception occurs are likely - /// to repeatedly result in the same exception. - fn next(&mut self) -> Option { - self.as_borrowed() - .next() - .map(|result| result.map(Bound::into_gil_ref)) - } - - #[cfg(not(Py_LIMITED_API))] - fn size_hint(&self) -> (usize, Option) { - self.as_borrowed().size_hint() - } -} - impl<'py> Iterator for Bound<'py, PyIterator> { type Item = PyResult>; diff --git a/src/types/mapping.rs b/src/types/mapping.rs index e3a19af069f..537959719e0 100644 --- a/src/types/mapping.rs +++ b/src/types/mapping.rs @@ -18,7 +18,6 @@ use crate::{ffi, Py, PyTypeCheck, Python, ToPyObject}; #[repr(transparent)] pub struct PyMapping(PyAny); pyobject_native_type_named!(PyMapping); -pyobject_native_type_extract!(PyMapping); impl PyMapping { /// Register a pyclass as a subclass of `collections.abc.Mapping` (from the Python standard diff --git a/src/types/memoryview.rs b/src/types/memoryview.rs index a79f66d548a..22fbb9ba4e2 100644 --- a/src/types/memoryview.rs +++ b/src/types/memoryview.rs @@ -2,8 +2,6 @@ use crate::err::PyResult; use crate::ffi_ptr_ext::FfiPtrExt; use crate::py_result_ext::PyResultExt; use crate::{ffi, Bound, PyAny}; -#[cfg(feature = "gil-refs")] -use crate::{AsPyPointer, PyNativeType}; /// Represents a Python `memoryview`. /// @@ -26,17 +24,6 @@ impl PyMemoryView { } } -#[cfg(feature = "gil-refs")] -impl<'py> TryFrom<&'py PyAny> for &'py PyMemoryView { - type Error = crate::PyErr; - - /// Creates a new Python `memoryview` object from another Python object that - /// implements the buffer protocol. - fn try_from(value: &'py PyAny) -> Result { - PyMemoryView::from_bound(&value.as_borrowed()).map(Bound::into_gil_ref) - } -} - impl<'py> TryFrom<&Bound<'py, PyAny>> for Bound<'py, PyMemoryView> { type Error = crate::PyErr; diff --git a/src/types/mod.rs b/src/types/mod.rs index a0e0cc77f25..2326fc5b8ef 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -263,24 +263,6 @@ macro_rules! pyobject_native_type_info( }; ); -// NOTE: This macro is not included in pyobject_native_type_base! -// because rust-numpy has a special implementation. -#[doc(hidden)] -#[macro_export] -macro_rules! pyobject_native_type_extract { - ($name:ty $(;$generics:ident)*) => { - // FIXME https://github.com/PyO3/pyo3/issues/3903 - #[allow(unknown_lints, non_local_definitions)] - #[cfg(feature = "gil-refs")] - impl<'py, $($generics,)*> $crate::FromPyObject<'py> for &'py $name { - #[inline] - fn extract_bound(obj: &$crate::Bound<'py, $crate::PyAny>) -> $crate::PyResult { - ::std::clone::Clone::clone(obj).into_gil_ref().downcast().map_err(::std::convert::Into::into) - } - } - } -} - /// Declares all of the boilerplate for Python types. #[doc(hidden)] #[macro_export] @@ -288,7 +270,6 @@ macro_rules! pyobject_native_type_core { ($name:ty, $typeobject:expr, #module=$module:expr $(, #checkfunction=$checkfunction:path)? $(;$generics:ident)*) => { $crate::pyobject_native_type_named!($name $(;$generics)*); $crate::pyobject_native_type_info!($name, $typeobject, $module $(, #checkfunction=$checkfunction)? $(;$generics)*); - $crate::pyobject_native_type_extract!($name $(;$generics)*); }; ($name:ty, $typeobject:expr $(, #checkfunction=$checkfunction:path)? $(;$generics:ident)*) => { $crate::pyobject_native_type_core!($name, $typeobject, #module=::std::option::Option::Some("builtins") $(, #checkfunction=$checkfunction)? $(;$generics)*); diff --git a/src/types/none.rs b/src/types/none.rs index 63e0b70dbf3..cfbba359113 100644 --- a/src/types/none.rs +++ b/src/types/none.rs @@ -12,7 +12,6 @@ use crate::{ pub struct PyNone(PyAny); pyobject_native_type_named!(PyNone); -pyobject_native_type_extract!(PyNone); impl PyNone { /// Returns the `None` object. diff --git a/src/types/notimplemented.rs b/src/types/notimplemented.rs index 2b4fe04421c..b33f9217fb9 100644 --- a/src/types/notimplemented.rs +++ b/src/types/notimplemented.rs @@ -11,7 +11,6 @@ use crate::{ pub struct PyNotImplemented(PyAny); pyobject_native_type_named!(PyNotImplemented); -pyobject_native_type_extract!(PyNotImplemented); impl PyNotImplemented { /// Returns the `NotImplemented` object. diff --git a/src/types/sequence.rs b/src/types/sequence.rs index c618b15dd48..d2a6b6b3806 100644 --- a/src/types/sequence.rs +++ b/src/types/sequence.rs @@ -21,7 +21,6 @@ use crate::{ffi, FromPyObject, Py, PyTypeCheck, Python, ToPyObject}; #[repr(transparent)] pub struct PySequence(PyAny); pyobject_native_type_named!(PySequence); -pyobject_native_type_extract!(PySequence); impl PySequence { /// Register a pyclass as a subclass of `collections.abc.Sequence` (from the Python standard diff --git a/src/types/set.rs b/src/types/set.rs index 975f4bd00aa..d1b514264d7 100644 --- a/src/types/set.rs +++ b/src/types/set.rs @@ -1,6 +1,4 @@ use crate::types::PyIterator; -#[cfg(feature = "gil-refs")] -use crate::PyNativeType; use crate::{ err::{self, PyErr, PyResult}, ffi_ptr_ext::FfiPtrExt, @@ -180,50 +178,6 @@ impl<'py> PySetMethods<'py> for Bound<'py, PySet> { } } -/// PyO3 implementation of an iterator for a Python `set` object. -#[cfg(feature = "gil-refs")] -pub struct PySetIterator<'py>(BoundSetIterator<'py>); - -#[cfg(feature = "gil-refs")] -impl<'py> Iterator for PySetIterator<'py> { - type Item = &'py super::PyAny; - - /// Advances the iterator and returns the next value. - /// - /// # Panics - /// - /// If PyO3 detects that the set is mutated during iteration, it will panic. - #[inline] - fn next(&mut self) -> Option { - self.0.next().map(Bound::into_gil_ref) - } - - fn size_hint(&self) -> (usize, Option) { - self.0.size_hint() - } -} - -#[cfg(feature = "gil-refs")] -impl ExactSizeIterator for PySetIterator<'_> { - fn len(&self) -> usize { - self.0.len() - } -} - -#[cfg(feature = "gil-refs")] -impl<'py> IntoIterator for &'py PySet { - type Item = &'py PyAny; - type IntoIter = PySetIterator<'py>; - /// Returns an iterator of values in this set. - /// - /// # Panics - /// - /// If PyO3 detects that the set is mutated during iteration, it will panic. - fn into_iter(self) -> Self::IntoIter { - PySetIterator(BoundSetIterator::new(self.as_borrowed().to_owned())) - } -} - impl<'py> IntoIterator for Bound<'py, PySet> { type Item = Bound<'py, PyAny>; type IntoIter = BoundSetIterator<'py>; diff --git a/src/types/tuple.rs b/src/types/tuple.rs index eb17c6320fb..ed99d40c22f 100644 --- a/src/types/tuple.rs +++ b/src/types/tuple.rs @@ -7,8 +7,6 @@ use crate::inspect::types::TypeInfo; use crate::instance::Borrowed; use crate::internal_tricks::get_ssize_index; use crate::types::{any::PyAnyMethods, sequence::PySequenceMethods, PyList, PySequence}; -#[cfg(feature = "gil-refs")] -use crate::PyNativeType; use crate::{ exceptions, Bound, FromPyObject, IntoPy, Py, PyAny, PyErr, PyObject, PyResult, Python, ToPyObject, @@ -313,53 +311,6 @@ impl<'a, 'py> Borrowed<'a, 'py, PyTuple> { } } -/// Used by `PyTuple::iter()`. -#[cfg(feature = "gil-refs")] -pub struct PyTupleIterator<'a>(BorrowedTupleIterator<'a, 'a>); - -#[cfg(feature = "gil-refs")] -impl<'a> Iterator for PyTupleIterator<'a> { - type Item = &'a PyAny; - - #[inline] - fn next(&mut self) -> Option { - self.0.next().map(Borrowed::into_gil_ref) - } - - #[inline] - fn size_hint(&self) -> (usize, Option) { - self.0.size_hint() - } -} - -#[cfg(feature = "gil-refs")] -impl<'a> DoubleEndedIterator for PyTupleIterator<'a> { - #[inline] - fn next_back(&mut self) -> Option { - self.0.next_back().map(Borrowed::into_gil_ref) - } -} - -#[cfg(feature = "gil-refs")] -impl<'a> ExactSizeIterator for PyTupleIterator<'a> { - fn len(&self) -> usize { - self.0.len() - } -} - -#[cfg(feature = "gil-refs")] -impl FusedIterator for PyTupleIterator<'_> {} - -#[cfg(feature = "gil-refs")] -impl<'a> IntoIterator for &'a PyTuple { - type Item = &'a PyAny; - type IntoIter = PyTupleIterator<'a>; - - fn into_iter(self) -> Self::IntoIter { - PyTupleIterator(BorrowedTupleIterator::new(self.as_borrowed())) - } -} - /// Used by `PyTuple::into_iter()`. pub struct BoundTupleIterator<'py> { tuple: Bound<'py, PyTuple>, diff --git a/src/types/weakref/anyref.rs b/src/types/weakref/anyref.rs index 75643fceb6a..d4e0aa5e447 100644 --- a/src/types/weakref/anyref.rs +++ b/src/types/weakref/anyref.rs @@ -11,7 +11,6 @@ use crate::{ffi, Borrowed, Bound}; pub struct PyWeakref(PyAny); pyobject_native_type_named!(PyWeakref); -pyobject_native_type_extract!(PyWeakref); // TODO: We known the layout but this cannot be implemented, due to the lack of public typeobject pointers // #[cfg(not(Py_LIMITED_API))] diff --git a/src/types/weakref/proxy.rs b/src/types/weakref/proxy.rs index 497b86a08bc..495cb1a5705 100644 --- a/src/types/weakref/proxy.rs +++ b/src/types/weakref/proxy.rs @@ -15,7 +15,6 @@ use super::PyWeakrefMethods; pub struct PyWeakrefProxy(PyAny); pyobject_native_type_named!(PyWeakrefProxy); -pyobject_native_type_extract!(PyWeakrefProxy); // TODO: We known the layout but this cannot be implemented, due to the lack of public typeobject pointers. And it is 2 distinct types // #[cfg(not(Py_LIMITED_API))] diff --git a/src/types/weakref/reference.rs b/src/types/weakref/reference.rs index 634257d4a3e..a2da67149da 100644 --- a/src/types/weakref/reference.rs +++ b/src/types/weakref/reference.rs @@ -27,8 +27,6 @@ pyobject_native_type!( // When targetting alternative or multiple interpreters, it is better to not use the internal API. #[cfg(any(PyPy, GraalPy, Py_LIMITED_API))] pyobject_native_type_named!(PyWeakrefReference); -#[cfg(any(PyPy, GraalPy, Py_LIMITED_API))] -pyobject_native_type_extract!(PyWeakrefReference); #[cfg(any(PyPy, GraalPy, Py_LIMITED_API))] impl PyTypeCheck for PyWeakrefReference { From 0881fa0542261781565a77d62d98602e5e8d59ba Mon Sep 17 00:00:00 2001 From: Matthijs Kok Date: Sat, 13 Jul 2024 21:06:24 +0200 Subject: [PATCH 162/495] chore: update `ruff` configuration to resolve deprecation warning (#4346) * chore: update `ruff` configuration to resolve deprecation warning * add newsfragment --- newsfragments/4346.changed.md | 1 + pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 newsfragments/4346.changed.md diff --git a/newsfragments/4346.changed.md b/newsfragments/4346.changed.md new file mode 100644 index 00000000000..1ac7952eae7 --- /dev/null +++ b/newsfragments/4346.changed.md @@ -0,0 +1 @@ +chore: update `ruff` configuration to resolve deprecation warning diff --git a/pyproject.toml b/pyproject.toml index 27178a2e402..0b58bb9a71b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,4 +1,4 @@ -[tool.ruff.extend-per-file-ignores] +[tool.ruff.lint.extend-per-file-ignores] "__init__.py" = ["F403"] [tool.towncrier] From 17f40caea1c516230fb05447e70acff378cf1dca Mon Sep 17 00:00:00 2001 From: Matthijs Kok Date: Sun, 14 Jul 2024 16:54:12 +0200 Subject: [PATCH 163/495] rename `PyLong` to `PyInt` (#4347) * rename `PyLong` to `PyInt` * add news fragment * change `PyLong` to a type alias to properly show deprecation warning --- guide/src/conversions/tables.md | 2 +- newsfragments/4347.changed.md | 1 + src/conversions/num_bigint.rs | 20 ++++++++++---------- src/conversions/std/num.rs | 2 +- src/types/any.rs | 22 +++++++++++----------- src/types/mod.rs | 4 ++-- src/types/num.rs | 10 +++++++--- src/types/typeobject.rs | 8 +++----- 8 files changed, 36 insertions(+), 33 deletions(-) create mode 100644 newsfragments/4347.changed.md diff --git a/guide/src/conversions/tables.md b/guide/src/conversions/tables.md index 208e61671ec..8afb95fbfb3 100644 --- a/guide/src/conversions/tables.md +++ b/guide/src/conversions/tables.md @@ -16,7 +16,7 @@ The table below contains the Python type and the corresponding function argument | `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` | +| `int` | `i8`, `u8`, `i16`, `u16`, `i32`, `u32`, `i64`, `u64`, `i128`, `u128`, `isize`, `usize`, `num_bigint::BigInt`[^1], `num_bigint::BigUint`[^1] | `PyInt` | | `float` | `f32`, `f64` | `PyFloat` | | `complex` | `num_complex::Complex`[^2] | `PyComplex` | | `fractions.Fraction`| `num_rational::Ratio`[^8] | - | diff --git a/newsfragments/4347.changed.md b/newsfragments/4347.changed.md new file mode 100644 index 00000000000..e64ad2c6395 --- /dev/null +++ b/newsfragments/4347.changed.md @@ -0,0 +1 @@ +Deprecate `PyLong` in favor of `PyInt`. diff --git a/src/conversions/num_bigint.rs b/src/conversions/num_bigint.rs index 196ae28e788..99b5962622d 100644 --- a/src/conversions/num_bigint.rs +++ b/src/conversions/num_bigint.rs @@ -54,7 +54,7 @@ use crate::types::{bytes::PyBytesMethods, PyBytes}; use crate::{ ffi, instance::Bound, - types::{any::PyAnyMethods, PyLong}, + types::{any::PyAnyMethods, PyInt}, FromPyObject, IntoPy, Py, PyAny, PyObject, PyResult, Python, ToPyObject, }; @@ -120,7 +120,7 @@ macro_rules! bigint_conversion { } else { None }; - py.get_type_bound::() + py.get_type_bound::() .call_method("from_bytes", (bytes_obj, "little"), kwargs.as_ref()) .expect("int.from_bytes() failed during to_object()") // FIXME: #1813 or similar .into() @@ -144,8 +144,8 @@ impl<'py> FromPyObject<'py> for BigInt { fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult { let py = ob.py(); // fast path - checking for subclass of `int` just checks a bit in the type object - let num_owned: Py; - let num = if let Ok(long) = ob.downcast::() { + let num_owned: Py; + let num = if let Ok(long) = ob.downcast::() { long } else { num_owned = unsafe { Py::from_owned_ptr_or_err(py, ffi::PyNumber_Index(ob.as_ptr()))? }; @@ -192,8 +192,8 @@ impl<'py> FromPyObject<'py> for BigUint { fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult { let py = ob.py(); // fast path - checking for subclass of `int` just checks a bit in the type object - let num_owned: Py; - let num = if let Ok(long) = ob.downcast::() { + let num_owned: Py; + let num = if let Ok(long) = ob.downcast::() { long } else { num_owned = unsafe { Py::from_owned_ptr_or_err(py, ffi::PyNumber_Index(ob.as_ptr()))? }; @@ -218,7 +218,7 @@ impl<'py> FromPyObject<'py> for BigUint { #[cfg(not(any(Py_LIMITED_API, Py_3_13)))] #[inline] -fn int_to_u32_vec(long: &Bound<'_, PyLong>) -> PyResult> { +fn int_to_u32_vec(long: &Bound<'_, PyInt>) -> PyResult> { let mut buffer = Vec::new(); let n_bits = int_n_bits(long)?; if n_bits == 0 { @@ -252,7 +252,7 @@ fn int_to_u32_vec(long: &Bound<'_, PyLong>) -> PyResult(long: &Bound<'_, PyLong>) -> PyResult> { +fn int_to_u32_vec(long: &Bound<'_, PyInt>) -> PyResult> { let mut buffer = Vec::new(); let mut flags = ffi::Py_ASNATIVEBYTES_LITTLE_ENDIAN; if !SIGNED { @@ -290,7 +290,7 @@ fn int_to_u32_vec(long: &Bound<'_, PyLong>) -> PyResult( - long: &Bound<'py, PyLong>, + long: &Bound<'py, PyInt>, n_bytes: usize, is_signed: bool, ) -> PyResult> { @@ -313,7 +313,7 @@ fn int_to_py_bytes<'py>( #[inline] #[cfg(any(not(Py_3_13), Py_LIMITED_API))] -fn int_n_bits(long: &Bound<'_, PyLong>) -> PyResult { +fn int_n_bits(long: &Bound<'_, PyInt>) -> PyResult { let py = long.py(); #[cfg(not(Py_LIMITED_API))] { diff --git a/src/conversions/std/num.rs b/src/conversions/std/num.rs index 1304e73e4b6..1431b232169 100644 --- a/src/conversions/std/num.rs +++ b/src/conversions/std/num.rs @@ -59,7 +59,7 @@ macro_rules! extract_int { // See https://github.com/PyO3/pyo3/pull/3742 for detials if cfg!(Py_3_10) && !$force_index_call { err_if_invalid_value($obj.py(), $error_val, unsafe { $pylong_as($obj.as_ptr()) }) - } else if let Ok(long) = $obj.downcast::() { + } else if let Ok(long) = $obj.downcast::() { // fast path - checking for subclass of `int` just checks a bit in the type $object err_if_invalid_value($obj.py(), $error_val, unsafe { $pylong_as(long.as_ptr()) }) } else { diff --git a/src/types/any.rs b/src/types/any.rs index 745847b86ee..33ac04df763 100644 --- a/src/types/any.rs +++ b/src/types/any.rs @@ -1495,7 +1495,7 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed { /// /// ```rust /// use pyo3::prelude::*; - /// use pyo3::types::{PyBool, PyLong}; + /// use pyo3::types::{PyBool, PyInt}; /// /// Python::with_gil(|py| { /// let b = PyBool::new_bound(py, true); @@ -1504,8 +1504,8 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed { /// /// // `bool` is a subtype of `int`, so `downcast` will accept a `bool` as an `int` /// // but `downcast_exact` will not. - /// assert!(any.downcast::().is_ok()); - /// assert!(any.downcast_exact::().is_err()); + /// assert!(any.downcast::().is_ok()); + /// assert!(any.downcast_exact::().is_err()); /// /// assert!(any.downcast_exact::().is_ok()); /// }); @@ -2269,7 +2269,7 @@ impl<'py> Bound<'py, PyAny> { mod tests { use crate::{ basic::CompareOp, - types::{IntoPyDict, PyAny, PyAnyMethods, PyBool, PyList, PyLong, PyModule, PyTypeMethods}, + types::{IntoPyDict, PyAny, PyAnyMethods, PyBool, PyInt, PyList, PyModule, PyTypeMethods}, Bound, PyTypeInfo, Python, ToPyObject, }; @@ -2424,7 +2424,7 @@ class SimpleClass: fn test_hasattr() { Python::with_gil(|py| { let x = 5.to_object(py).into_bound(py); - assert!(x.is_instance_of::()); + assert!(x.is_instance_of::()); assert!(x.hasattr("to_bytes").unwrap()); assert!(!x.hasattr("bbbbbbytes").unwrap()); @@ -2471,7 +2471,7 @@ class SimpleClass: fn test_any_is_instance_of() { Python::with_gil(|py| { let x = 5.to_object(py).into_bound(py); - assert!(x.is_instance_of::()); + assert!(x.is_instance_of::()); let l = vec![&x, &x].to_object(py).into_bound(py); assert!(l.is_instance_of::()); @@ -2490,11 +2490,11 @@ class SimpleClass: fn test_any_is_exact_instance_of() { Python::with_gil(|py| { let x = 5.to_object(py).into_bound(py); - assert!(x.is_exact_instance_of::()); + assert!(x.is_exact_instance_of::()); let t = PyBool::new_bound(py, true); - assert!(t.is_instance_of::()); - assert!(!t.is_exact_instance_of::()); + assert!(t.is_instance_of::()); + assert!(!t.is_exact_instance_of::()); assert!(t.is_exact_instance_of::()); let l = vec![&x, &x].to_object(py).into_bound(py); @@ -2506,8 +2506,8 @@ class SimpleClass: fn test_any_is_exact_instance() { Python::with_gil(|py| { let t = PyBool::new_bound(py, true); - assert!(t.is_instance(&py.get_type_bound::()).unwrap()); - assert!(!t.is_exact_instance(&py.get_type_bound::())); + assert!(t.is_instance(&py.get_type_bound::()).unwrap()); + assert!(!t.is_exact_instance(&py.get_type_bound::())); assert!(t.is_exact_instance(&py.get_type_bound::())); }); } diff --git a/src/types/mod.rs b/src/types/mod.rs index 2326fc5b8ef..0ca0eec66f2 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -31,8 +31,8 @@ pub use self::memoryview::PyMemoryView; pub use self::module::{PyModule, PyModuleMethods}; pub use self::none::PyNone; pub use self::notimplemented::PyNotImplemented; -pub use self::num::PyLong; -pub use self::num::PyLong as PyInt; +#[allow(deprecated)] +pub use self::num::{PyInt, PyLong}; #[cfg(not(any(PyPy, GraalPy)))] pub use self::pysuper::PySuper; pub use self::sequence::{PySequence, PySequenceMethods}; diff --git a/src/types/num.rs b/src/types/num.rs index b43dddbef88..517e769742b 100644 --- a/src/types/num.rs +++ b/src/types/num.rs @@ -3,13 +3,17 @@ use crate::{ffi, PyAny}; /// Represents a Python `int` object. /// /// Values of this type are accessed via PyO3's smart pointers, e.g. as -/// [`Py`][crate::Py] or [`Bound<'py, PyLong>`][crate::Bound]. +/// [`Py`][crate::Py] or [`Bound<'py, PyInt>`][crate::Bound]. /// /// You can usually avoid directly working with this type /// by using [`ToPyObject`](crate::conversion::ToPyObject) /// and [`extract`](super::PyAnyMethods::extract) /// with the primitive Rust integer types. #[repr(transparent)] -pub struct PyLong(PyAny); +pub struct PyInt(PyAny); -pyobject_native_type_core!(PyLong, pyobject_native_static_type_object!(ffi::PyLong_Type), #checkfunction=ffi::PyLong_Check); +pyobject_native_type_core!(PyInt, pyobject_native_static_type_object!(ffi::PyLong_Type), #checkfunction=ffi::PyLong_Check); + +/// Deprecated alias for [`PyInt`]. +#[deprecated(since = "0.23.0", note = "use `PyInt` instead")] +pub type PyLong = PyInt; diff --git a/src/types/typeobject.rs b/src/types/typeobject.rs index f4b7a6fbc43..9c2d8c17334 100644 --- a/src/types/typeobject.rs +++ b/src/types/typeobject.rs @@ -243,9 +243,7 @@ impl<'py> PyTypeMethods<'py> for Bound<'py, PyType> { #[cfg(test)] mod tests { - use crate::types::{ - PyAnyMethods, PyBool, PyInt, PyLong, PyModule, PyTuple, PyType, PyTypeMethods, - }; + use crate::types::{PyAnyMethods, PyBool, PyInt, PyModule, PyTuple, PyType, PyTypeMethods}; use crate::PyAny; use crate::Python; @@ -253,7 +251,7 @@ mod tests { fn test_type_is_subclass() { Python::with_gil(|py| { let bool_type = py.get_type_bound::(); - let long_type = py.get_type_bound::(); + let long_type = py.get_type_bound::(); assert!(bool_type.is_subclass(&long_type).unwrap()); }); } @@ -263,7 +261,7 @@ mod tests { Python::with_gil(|py| { assert!(py .get_type_bound::() - .is_subclass_of::() + .is_subclass_of::() .unwrap()); }); } From d598d1f4209cfce7be2208f6ca88b7aaceaf6600 Mon Sep 17 00:00:00 2001 From: Joshua Rudolph Date: Sun, 14 Jul 2024 17:11:32 -0500 Subject: [PATCH 164/495] Added `as_super` and `into_super` methods for `Bound` (#4351) * Added `Bound::as_super` and `Bound::into_super` methods. * Added tests for the `Bound::as_super` and `Bound::into_super` methods. * Added newsfragment entry. * Fixed newsfragment PR number. * Fixed doc links. * Fixed missing spaces in method docstrings. Co-authored-by: Lily Foote * fixup docs * fixup docs --------- Co-authored-by: jrudolph Co-authored-by: Lily Foote Co-authored-by: Icxolu <10486322+Icxolu@users.noreply.github.com> --- newsfragments/4351.added.md | 1 + src/instance.rs | 142 ++++++++++++++++++++++++++++++++++++ 2 files changed, 143 insertions(+) create mode 100644 newsfragments/4351.added.md diff --git a/newsfragments/4351.added.md b/newsfragments/4351.added.md new file mode 100644 index 00000000000..f9276ae0d4f --- /dev/null +++ b/newsfragments/4351.added.md @@ -0,0 +1 @@ +Added `as_super` and `into_super` methods for `Bound`. \ No newline at end of file diff --git a/src/instance.rs b/src/instance.rs index 6967306a73f..3b8e2529a6e 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -359,6 +359,110 @@ where self.1.get() } + /// Upcast this `Bound` to its base type by reference. + /// + /// If this type defined an explicit base class in its `pyclass` declaration + /// (e.g. `#[pyclass(extends = BaseType)]`), the returned type will be + /// `&Bound`. If an explicit base class was _not_ declared, the + /// return value will be `&Bound` (making this method equivalent + /// to [`as_any`]). + /// + /// This method is particularly useful for calling methods defined in an + /// extension trait that has been implemented for `Bound`. + /// + /// See also the [`into_super`] method to upcast by value, and the + /// [`PyRef::as_super`]/[`PyRefMut::as_super`] methods for upcasting a pyclass + /// that has already been [`borrow`]ed. + /// + /// # Example: Calling a method defined on the `Bound` base type + /// + /// ```rust + /// # fn main() { + /// use pyo3::prelude::*; + /// + /// #[pyclass(subclass)] + /// struct BaseClass; + /// + /// trait MyClassMethods<'py> { + /// fn pyrepr(&self) -> PyResult; + /// } + /// impl<'py> MyClassMethods<'py> for Bound<'py, BaseClass> { + /// fn pyrepr(&self) -> PyResult { + /// self.call_method0("__repr__")?.extract() + /// } + /// } + /// + /// #[pyclass(extends = BaseClass)] + /// struct SubClass; + /// + /// Python::with_gil(|py| { + /// let obj = Bound::new(py, (SubClass, BaseClass)).unwrap(); + /// assert!(obj.as_super().pyrepr().is_ok()); + /// }) + /// # } + /// ``` + /// + /// [`as_any`]: Bound::as_any + /// [`into_super`]: Bound::into_super + /// [`borrow`]: Bound::borrow + #[inline] + pub fn as_super(&self) -> &Bound<'py, T::BaseType> { + // a pyclass can always be safely "downcast" to its base type + unsafe { self.as_any().downcast_unchecked() } + } + + /// Upcast this `Bound` to its base type by value. + /// + /// If this type defined an explicit base class in its `pyclass` declaration + /// (e.g. `#[pyclass(extends = BaseType)]`), the returned type will be + /// `Bound`. If an explicit base class was _not_ declared, the + /// return value will be `Bound` (making this method equivalent + /// to [`into_any`]). + /// + /// This method is particularly useful for calling methods defined in an + /// extension trait that has been implemented for `Bound`. + /// + /// See also the [`as_super`] method to upcast by reference, and the + /// [`PyRef::into_super`]/[`PyRefMut::into_super`] methods for upcasting a pyclass + /// that has already been [`borrow`]ed. + /// + /// # Example: Calling a method defined on the `Bound` base type + /// + /// ```rust + /// # fn main() { + /// use pyo3::prelude::*; + /// + /// #[pyclass(subclass)] + /// struct BaseClass; + /// + /// trait MyClassMethods<'py> { + /// fn pyrepr(self) -> PyResult; + /// } + /// impl<'py> MyClassMethods<'py> for Bound<'py, BaseClass> { + /// fn pyrepr(self) -> PyResult { + /// self.call_method0("__repr__")?.extract() + /// } + /// } + /// + /// #[pyclass(extends = BaseClass)] + /// struct SubClass; + /// + /// Python::with_gil(|py| { + /// let obj = Bound::new(py, (SubClass, BaseClass)).unwrap(); + /// assert!(obj.into_super().pyrepr().is_ok()); + /// }) + /// # } + /// ``` + /// + /// [`into_any`]: Bound::into_any + /// [`as_super`]: Bound::as_super + /// [`borrow`]: Bound::borrow + #[inline] + pub fn into_super(self) -> Bound<'py, T::BaseType> { + // a pyclass can always be safely "downcast" to its base type + unsafe { self.into_any().downcast_into_unchecked() } + } + #[inline] pub(crate) fn get_class_object(&self) -> &PyClassObject { self.1.get_class_object() @@ -2324,5 +2428,43 @@ a = A() } }) } + + #[crate::pyclass(crate = "crate", subclass)] + struct BaseClass; + + trait MyClassMethods<'py>: Sized { + fn pyrepr_by_ref(&self) -> PyResult; + fn pyrepr_by_val(self) -> PyResult { + self.pyrepr_by_ref() + } + } + impl<'py> MyClassMethods<'py> for Bound<'py, BaseClass> { + fn pyrepr_by_ref(&self) -> PyResult { + self.call_method0("__repr__")?.extract() + } + } + + #[crate::pyclass(crate = "crate", extends = BaseClass)] + struct SubClass; + + #[test] + fn test_as_super() { + Python::with_gil(|py| { + let obj = Bound::new(py, (SubClass, BaseClass)).unwrap(); + let _: &Bound<'_, BaseClass> = obj.as_super(); + let _: &Bound<'_, PyAny> = obj.as_super().as_super(); + assert!(obj.as_super().pyrepr_by_ref().is_ok()); + }) + } + + #[test] + fn test_into_super() { + Python::with_gil(|py| { + let obj = Bound::new(py, (SubClass, BaseClass)).unwrap(); + let _: Bound<'_, BaseClass> = obj.clone().into_super(); + let _: Bound<'_, PyAny> = obj.clone().into_super().into_super(); + assert!(obj.into_super().pyrepr_by_val().is_ok()); + }) + } } } From e3531667771e9459e10b7a055b64dbde72ca86e8 Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Mon, 15 Jul 2024 21:00:54 +0200 Subject: [PATCH 165/495] only emit c-string literals on Rust 1.79 and later (#4352) (#4353) * only emit c-string literals on Rust 1.79 and later (#4352) * add newsfragment --- newsfragments/4353.fixed.md | 1 + pyo3-build-config/src/lib.rs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 newsfragments/4353.fixed.md diff --git a/newsfragments/4353.fixed.md b/newsfragments/4353.fixed.md new file mode 100644 index 00000000000..73783479085 --- /dev/null +++ b/newsfragments/4353.fixed.md @@ -0,0 +1 @@ +fixed compile error due to c-string literals on Rust < 1.79 \ No newline at end of file diff --git a/pyo3-build-config/src/lib.rs b/pyo3-build-config/src/lib.rs index 2da3e56d3b6..0bc2274e0e5 100644 --- a/pyo3-build-config/src/lib.rs +++ b/pyo3-build-config/src/lib.rs @@ -143,7 +143,7 @@ pub fn print_feature_cfgs() { println!("cargo:rustc-cfg=invalid_from_utf8_lint"); } - if rustc_minor_version >= 77 { + if rustc_minor_version >= 79 { println!("cargo:rustc-cfg=c_str_lit"); } From f635afcae511e3b25aa33f657731390996905ad9 Mon Sep 17 00:00:00 2001 From: Owen Leung Date: Tue, 16 Jul 2024 21:15:26 +0800 Subject: [PATCH 166/495] Implement PartialEq for PyFloat with f64 and f32 (#4348) * Implement PartialEq for PyFloat Implement PartialEq for PyFloat * Add changelog under newsfragments, and fix failing fmt CI job * Use match arm instead of if else * use value API instead of extract API --- newsfragments/4348.added.md | 1 + src/types/float.rs | 144 +++++++++++++++++++++++++++++++++++- 2 files changed, 143 insertions(+), 2 deletions(-) create mode 100644 newsfragments/4348.added.md diff --git a/newsfragments/4348.added.md b/newsfragments/4348.added.md new file mode 100644 index 00000000000..57d3e415187 --- /dev/null +++ b/newsfragments/4348.added.md @@ -0,0 +1 @@ +Implement `PartialEq` and `PartialEq` for `Bound<'py, PyFloat>`. \ No newline at end of file diff --git a/src/types/float.rs b/src/types/float.rs index 0a981d60cec..b1992d3983a 100644 --- a/src/types/float.rs +++ b/src/types/float.rs @@ -2,8 +2,8 @@ use super::any::PyAnyMethods; #[cfg(feature = "experimental-inspect")] use crate::inspect::types::TypeInfo; use crate::{ - ffi, ffi_ptr_ext::FfiPtrExt, instance::Bound, FromPyObject, IntoPy, PyAny, PyErr, PyObject, - PyResult, Python, ToPyObject, + ffi, ffi_ptr_ext::FfiPtrExt, instance::Bound, Borrowed, FromPyObject, IntoPy, PyAny, PyErr, + PyObject, PyResult, Python, ToPyObject, }; use std::os::raw::c_double; @@ -140,6 +140,83 @@ impl<'py> FromPyObject<'py> for f32 { } } +macro_rules! impl_partial_eq_for_float { + ($float_type: ty) => { + impl PartialEq<$float_type> for Bound<'_, PyFloat> { + #[inline] + fn eq(&self, other: &$float_type) -> bool { + self.value() as $float_type == *other + } + } + + impl PartialEq<$float_type> for &Bound<'_, PyFloat> { + #[inline] + fn eq(&self, other: &$float_type) -> bool { + self.value() as $float_type == *other + } + } + + impl PartialEq<&$float_type> for Bound<'_, PyFloat> { + #[inline] + fn eq(&self, other: &&$float_type) -> bool { + self.value() as $float_type == **other + } + } + + impl PartialEq> for $float_type { + #[inline] + fn eq(&self, other: &Bound<'_, PyFloat>) -> bool { + other.value() as $float_type == *self + } + } + + impl PartialEq<&'_ Bound<'_, PyFloat>> for $float_type { + #[inline] + fn eq(&self, other: &&'_ Bound<'_, PyFloat>) -> bool { + other.value() as $float_type == *self + } + } + + impl PartialEq> for &'_ $float_type { + #[inline] + fn eq(&self, other: &Bound<'_, PyFloat>) -> bool { + other.value() as $float_type == **self + } + } + + impl PartialEq<$float_type> for Borrowed<'_, '_, PyFloat> { + #[inline] + fn eq(&self, other: &$float_type) -> bool { + self.value() as $float_type == *other + } + } + + impl PartialEq<&$float_type> for Borrowed<'_, '_, PyFloat> { + #[inline] + fn eq(&self, other: &&$float_type) -> bool { + self.value() as $float_type == **other + } + } + + impl PartialEq> for $float_type { + #[inline] + fn eq(&self, other: &Borrowed<'_, '_, PyFloat>) -> bool { + other.value() as $float_type == *self + } + } + + impl PartialEq> for &$float_type { + #[inline] + fn eq(&self, other: &Borrowed<'_, '_, PyFloat>) -> bool { + other.value() as $float_type == **self + } + } + }; +} + +impl_partial_eq_for_float!(f64); +impl_partial_eq_for_float!(f32); + #[cfg(test)] mod tests { use crate::{ @@ -177,4 +254,67 @@ mod tests { assert_approx_eq!(v, obj.value()); }); } + + #[test] + fn test_pyfloat_comparisons() { + Python::with_gil(|py| { + let f_64 = 1.01f64; + let py_f64 = PyFloat::new_bound(py, 1.01); + let py_f64_ref = &py_f64; + let py_f64_borrowed = py_f64.as_borrowed(); + + // Bound<'_, PyFloat> == f64 and vice versa + assert_eq!(py_f64, f_64); + assert_eq!(f_64, py_f64); + + // Bound<'_, PyFloat> == &f64 and vice versa + assert_eq!(py_f64, &f_64); + assert_eq!(&f_64, py_f64); + + // &Bound<'_, PyFloat> == &f64 and vice versa + assert_eq!(py_f64_ref, f_64); + assert_eq!(f_64, py_f64_ref); + + // &Bound<'_, PyFloat> == &f64 and vice versa + assert_eq!(py_f64_ref, &f_64); + assert_eq!(&f_64, py_f64_ref); + + // Borrowed<'_, '_, PyFloat> == f64 and vice versa + assert_eq!(py_f64_borrowed, f_64); + assert_eq!(f_64, py_f64_borrowed); + + // Borrowed<'_, '_, PyFloat> == &f64 and vice versa + assert_eq!(py_f64_borrowed, &f_64); + assert_eq!(&f_64, py_f64_borrowed); + + let f_32 = 2.02f32; + let py_f32 = PyFloat::new_bound(py, 2.02); + let py_f32_ref = &py_f32; + let py_f32_borrowed = py_f32.as_borrowed(); + + // Bound<'_, PyFloat> == f32 and vice versa + assert_eq!(py_f32, f_32); + assert_eq!(f_32, py_f32); + + // Bound<'_, PyFloat> == &f32 and vice versa + assert_eq!(py_f32, &f_32); + assert_eq!(&f_32, py_f32); + + // &Bound<'_, PyFloat> == &f32 and vice versa + assert_eq!(py_f32_ref, f_32); + assert_eq!(f_32, py_f32_ref); + + // &Bound<'_, PyFloat> == &f32 and vice versa + assert_eq!(py_f32_ref, &f_32); + assert_eq!(&f_32, py_f32_ref); + + // Borrowed<'_, '_, PyFloat> == f32 and vice versa + assert_eq!(py_f32_borrowed, f_32); + assert_eq!(f_32, py_f32_borrowed); + + // Borrowed<'_, '_, PyFloat> == &f32 and vice versa + assert_eq!(py_f32_borrowed, &f_32); + assert_eq!(&f_32, py_f32_borrowed); + }); + } } From c1f524f5c7570e6af70aff82c28e368a326ec0c0 Mon Sep 17 00:00:00 2001 From: Michael Gilbert Date: Wed, 17 Jul 2024 14:02:33 -0700 Subject: [PATCH 167/495] fix: adding tests for pyclass hygiene cases (#4359) * fix: updated pyclass heighten check to validate for eq and ord, fixing Ok issue in eq implementation. identified similar issues in Complex enum and tuple enum, resolved serveral cases, but working on current error coming from traits not being in scope * fix: fully qualified clone and from calls for enums. * update: added changelog entry --------- Co-authored-by: MG --- newsfragments/4359.fixed.md | 1 + pyo3-macros-backend/src/pyclass.rs | 23 +++++++++++------------ src/tests/hygiene/pyclass.rs | 25 +++++++++++++++++++++++++ 3 files changed, 37 insertions(+), 12 deletions(-) create mode 100644 newsfragments/4359.fixed.md diff --git a/newsfragments/4359.fixed.md b/newsfragments/4359.fixed.md new file mode 100644 index 00000000000..7174cab0a9d --- /dev/null +++ b/newsfragments/4359.fixed.md @@ -0,0 +1 @@ +Fixed `pyclass` macro hygiene issues for structs and enums and added tests. \ No newline at end of file diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index 078a7b7f4af..125fdab4927 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -923,7 +923,7 @@ fn impl_complex_enum( let variant_cls = gen_complex_enum_variant_class_ident(cls, variant.get_ident()); quote! { #cls::#variant_ident { .. } => { - let pyclass_init = #pyo3_path::PyClassInitializer::from(self).add_subclass(#variant_cls); + let pyclass_init = <#pyo3_path::PyClassInitializer as ::std::convert::From>::from(self).add_subclass(#variant_cls); let variant_value = #pyo3_path::Py::new(py, pyclass_init).unwrap(); #pyo3_path::IntoPy::into_py(variant_value, py) } @@ -1091,8 +1091,8 @@ fn impl_complex_enum_struct_variant_cls( let field_getter_impl = quote! { fn #field_name(slf: #pyo3_path::PyRef) -> #pyo3_path::PyResult<#field_type> { match &*slf.into_super() { - #enum_name::#variant_ident { #field_name, .. } => Ok(#field_name.clone()), - _ => unreachable!("Wrong complex enum variant found in variant wrapper PyClass"), + #enum_name::#variant_ident { #field_name, .. } => ::std::result::Result::Ok(::std::clone::Clone::clone(&#field_name)), + _ => ::core::unreachable!("Wrong complex enum variant found in variant wrapper PyClass"), } } }; @@ -1114,7 +1114,7 @@ fn impl_complex_enum_struct_variant_cls( impl #variant_cls { fn __pymethod_constructor__(py: #pyo3_path::Python<'_>, #(#fields_with_types,)*) -> #pyo3_path::PyClassInitializer<#variant_cls> { let base_value = #enum_name::#variant_ident { #(#field_names,)* }; - #pyo3_path::PyClassInitializer::from(base_value).add_subclass(#variant_cls) + <#pyo3_path::PyClassInitializer<#enum_name> as ::std::convert::From<#enum_name>>::from(base_value).add_subclass(#variant_cls) } #match_args_const_impl @@ -1157,12 +1157,11 @@ fn impl_complex_enum_tuple_variant_field_getters( } }) .collect(); - let field_getter_impl: syn::ImplItemFn = parse_quote! { fn #field_name(slf: #pyo3_path::PyRef) -> #pyo3_path::PyResult<#field_type> { match &*slf.into_super() { - #enum_name::#variant_ident ( #(#field_access_tokens), *) => Ok(val.clone()), - _ => unreachable!("Wrong complex enum variant found in variant wrapper PyClass"), + #enum_name::#variant_ident ( #(#field_access_tokens), *) => ::std::result::Result::Ok(::std::clone::Clone::clone(&val)), + _ => ::core::unreachable!("Wrong complex enum variant found in variant wrapper PyClass"), } } }; @@ -1186,7 +1185,7 @@ fn impl_complex_enum_tuple_variant_len( let mut len_method_impl: syn::ImplItemFn = parse_quote! { fn __len__(slf: #pyo3_path::PyRef) -> #pyo3_path::PyResult { - Ok(#num_fields) + ::std::result::Result::Ok(#num_fields) } }; @@ -1208,7 +1207,7 @@ fn impl_complex_enum_tuple_variant_getitem( .map(|i| { let field_access = format_ident!("_{}", i); quote! { - #i => Ok( + #i => ::std::result::Result::Ok( #pyo3_path::IntoPy::into_py( #variant_cls::#field_access(slf)? , py) @@ -1223,7 +1222,7 @@ fn impl_complex_enum_tuple_variant_getitem( let py = slf.py(); match idx { #( #match_arms, )* - _ => Err(pyo3::exceptions::PyIndexError::new_err("tuple index out of range")), + _ => ::std::result::Result::Err(#pyo3_path::exceptions::PyIndexError::new_err("tuple index out of range")), } } }; @@ -1287,7 +1286,7 @@ fn impl_complex_enum_tuple_variant_cls( impl #variant_cls { fn __pymethod_constructor__(py: #pyo3_path::Python<'_>, #(#field_names : #field_types,)*) -> #pyo3_path::PyClassInitializer<#variant_cls> { let base_value = #enum_name::#variant_ident ( #(#field_names,)* ); - #pyo3_path::PyClassInitializer::from(base_value).add_subclass(#variant_cls) + <#pyo3_path::PyClassInitializer<#enum_name> as ::std::convert::From<#enum_name>>::from(base_value).add_subclass(#variant_cls) } #len_method_impl @@ -1828,7 +1827,7 @@ fn pyclass_richcmp( op: #pyo3_path::pyclass::CompareOp ) -> #pyo3_path::PyResult<#pyo3_path::PyObject> { let self_val = self; - if let Ok(other) = #pyo3_path::types::PyAnyMethods::downcast::(other) { + if let ::std::result::Result::Ok(other) = #pyo3_path::types::PyAnyMethods::downcast::(other) { let other = &*other.borrow(); match op { #arms diff --git a/src/tests/hygiene/pyclass.rs b/src/tests/hygiene/pyclass.rs index 3c078f580d5..dcd347fb5f2 100644 --- a/src/tests/hygiene/pyclass.rs +++ b/src/tests/hygiene/pyclass.rs @@ -66,3 +66,28 @@ pub struct Foo4 { #[cfg(not(any()))] field: u32, } + +#[crate::pyclass(eq, ord)] +#[pyo3(crate = "crate")] +#[derive(PartialEq, PartialOrd)] +pub struct PointEqOrd { + x: u32, + y: u32, + z: u32, +} + +#[crate::pyclass(eq, ord)] +#[pyo3(crate = "crate")] +#[derive(PartialEq, PartialOrd)] +pub enum ComplexEnumEqOrd { + Variant1 { a: u32, b: u32 }, + Variant2 { c: u32 }, +} + +#[crate::pyclass(eq, ord)] +#[pyo3(crate = "crate")] +#[derive(PartialEq, PartialOrd)] +pub enum TupleEnumEqOrd { + Variant1(u32, u32), + Variant2(u32), +} From 5ac5cef965ed50ba8e0a17e8cfed31dc5bd22072 Mon Sep 17 00:00:00 2001 From: Michael Gilbert Date: Wed, 17 Jul 2024 15:39:04 -0700 Subject: [PATCH 168/495] Trait ergonomics str implementation (#4233) * feat: Implement custom string formatting for PyClass This update brings custom string formatting for PyClass with a #[pyclass(str = "format string")] attribute. It allows users to specify how their PyClass objects are converted to string in Python. The implementation includes additional tests and parsing logic. * update: removed debug print statements * update: added members to ToTokens implementation. * update: reverted to display * update: initial tests * update: made STR public for pyclass default implementations * update: generalizing str implementation * update: remove redundant test * update: implemented compile test to validate that manually implemented str is not allowed when automated str is requested * update: updated compile time error check * update: rename test file and code cleanup * update: format cleanup * update: added news fragment * fix: corrected clippy findings * update: fixed mixed formatting case and improved test coverage * update: improved test coverage * refactor: generalized formatting function to accommodate __repr__ in a future implementation since it will use the same shorthand formatting logic * update: Add support for rename formatting in PyEnum3 Implemented the capacity to handle renamed variants in enum string representation. Now, custom Python names for enum variants will be correctly reflected when calling the __str__() method on an enum instance. Additionally, the related test has been updated to reflect this change. * fix: fixed clippy finding * update: fixed test function names * Update pyo3-macros-backend/src/pyclass.rs Co-authored-by: Bruno Kolenbrander <59372212+mejrs@users.noreply.github.com> * Update newsfragments/4233.added.md Co-authored-by: Bruno Kolenbrander <59372212+mejrs@users.noreply.github.com> * update: implemented hygienic calls and added hygiene tests. * update: cargo fmt * update: retained LitStr usage in the quote in order to preserve a more targeted span for the format string. * update: retained LitStr usage in the quote in order to preserve a more targeted span for the format string. * update: added compile time error check for invalid fields (looking to reduce span of invalid member) * update: implemented a subspan to improve errors in format string on nightly, verified additional test cases on both nightly and stable * update: updated test output * update: updated with clippy findings * update: added doc entries. * update: corrected error output for compile errors after updating from main. * update: added support for raw identifiers used in field names * update: aligning branch with main * update: added compile time error when mixing rename_all or name pyclass, field, or variant args when mixed with a str shorthand formatter. * update: removed self option from str format shorthand, restricted str shorthand format to structs only, updated docs with changes, refactored renaming incompatibility check with str shorthand. * update: removed checks for shorthand and renaming for enums and simplified back to inline check for structs * update: added additional test case to increase coverage in match branch * fix: updated pyclass heighten check to validate for eq and ord, fixing Ok issue in eq implementation. * Revert "fix: updated pyclass heighten check to validate for eq and ord, fixing Ok issue in eq implementation." This reverts commit a37c24bce6c263bd553d08ef94ecf52c3026ebfc. * update: improved error comments, naming, and added reference to the PR for additional details regarding the implementation of `str` * update: fixed merge conflict --------- Co-authored-by: Michael Gilbert Co-authored-by: Bruno Kolenbrander <59372212+mejrs@users.noreply.github.com> Co-authored-by: MG --- guide/pyclass-parameters.md | 1 + guide/src/class/object.md | 40 ++++ newsfragments/4233.added.md | 1 + pyo3-macros-backend/src/attributes.rs | 153 +++++++++++++- pyo3-macros-backend/src/pyclass.rs | 93 ++++++++- pyo3-macros-backend/src/pymethod.rs | 2 +- src/tests/hygiene/pyclass.rs | 22 ++ tests/test_class_formatting.rs | 179 ++++++++++++++++ tests/ui/invalid_pyclass_args.rs | 98 +++++++++ tests/ui/invalid_pyclass_args.stderr | 289 +++++++++++++++++++------- 10 files changed, 794 insertions(+), 84 deletions(-) create mode 100644 newsfragments/4233.added.md create mode 100644 tests/test_class_formatting.rs diff --git a/guide/pyclass-parameters.md b/guide/pyclass-parameters.md index a3a4e1f0c7d..b471f5dd3ae 100644 --- a/guide/pyclass-parameters.md +++ b/guide/pyclass-parameters.md @@ -19,6 +19,7 @@ | `rename_all = "renaming_rule"` | Applies renaming rules to every getters and setters of a struct, or every variants of an enum. Possible values are: "camelCase", "kebab-case", "lowercase", "PascalCase", "SCREAMING-KEBAB-CASE", "SCREAMING_SNAKE_CASE", "snake_case", "UPPERCASE". | | `sequence` | Inform PyO3 that this class is a [`Sequence`][params-sequence], and so leave its C-API mapping length slot empty. | | `set_all` | Generates setters for all fields of the pyclass. | +| `str` | Implements `__str__` using the `Display` implementation of the underlying Rust datatype or by passing an optional format string `str=""`. *Note: The optional format string is only allowed for structs. `name` and `rename_all` are incompatible with the optional format string. Additional details can be found in the discussion on this [PR](https://github.com/PyO3/pyo3/pull/4233).* | | `subclass` | Allows other Python classes and `#[pyclass]` to inherit from this class. Enums cannot be subclassed. | | `text_signature = "(arg1, arg2, ...)"` | Sets the text signature for the Python class' `__new__` method. | | `unsendable` | Required if your struct is not [`Send`][params-3]. Rather than using `unsendable`, consider implementing your struct in a threadsafe way by e.g. substituting [`Rc`][params-4] with [`Arc`][params-5]. By using `unsendable`, your class will panic when accessed by another thread. Also note the Python's GC is multi-threaded and while unsendable classes will not be traversed on foreign threads to avoid UB, this can lead to memory leaks. | diff --git a/guide/src/class/object.md b/guide/src/class/object.md index e9ea549aab4..e2565427838 100644 --- a/guide/src/class/object.md +++ b/guide/src/class/object.md @@ -70,6 +70,46 @@ impl Number { } ``` +To automatically generate the `__str__` implementation using a `Display` trait implementation, pass the `str` argument to `pyclass`. + +```rust +# use std::fmt::{Display, Formatter}; +# use pyo3::prelude::*; +# +# #[pyclass(str)] +# struct Coordinate { + x: i32, + y: i32, + z: i32, +} + +impl Display for Coordinate { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "({}, {}, {})", self.x, self.y, self.z) + } +} +``` + +For convenience, a shorthand format string can be passed to `str` as `str=""` for **structs only**. It expands and is passed into the `format!` macro in the following ways: + +* `"{x}"` -> `"{}", self.x` +* `"{0}"` -> `"{}", self.0` +* `"{x:?}"` -> `"{:?}", self.x` + +*Note: Depending upon the format string you use, this may require implementation of the `Display` or `Debug` traits for the given Rust types.* +*Note: the pyclass args `name` and `rename_all` are incompatible with the shorthand format string and will raise a compile time error.* + +```rust +# use pyo3::prelude::*; +# +# #[pyclass(str="({x}, {y}, {z})")] +# struct Coordinate { + x: i32, + y: i32, + z: i32, +} +``` + #### Accessing the class name In the `__repr__`, we used a hard-coded class name. This is sometimes not ideal, diff --git a/newsfragments/4233.added.md b/newsfragments/4233.added.md new file mode 100644 index 00000000000..cd45d163951 --- /dev/null +++ b/newsfragments/4233.added.md @@ -0,0 +1 @@ +Added `#[pyclass(str="")]` option to generate `__str__` based on a `Display` implementation or format string. \ No newline at end of file diff --git a/pyo3-macros-backend/src/attributes.rs b/pyo3-macros-backend/src/attributes.rs index 6a45ee875e3..780ad7035f0 100644 --- a/pyo3-macros-backend/src/attributes.rs +++ b/pyo3-macros-backend/src/attributes.rs @@ -1,12 +1,13 @@ use proc_macro2::TokenStream; -use quote::ToTokens; +use quote::{quote, ToTokens}; +use syn::parse::Parser; use syn::{ ext::IdentExt, parse::{Parse, ParseStream}, punctuated::Punctuated, spanned::Spanned, token::Comma, - Attribute, Expr, ExprPath, Ident, LitStr, Path, Result, Token, + Attribute, Expr, ExprPath, Ident, Index, LitStr, Member, Path, Result, Token, }; pub mod kw { @@ -36,6 +37,7 @@ pub mod kw { syn::custom_keyword!(set); syn::custom_keyword!(set_all); syn::custom_keyword!(signature); + syn::custom_keyword!(str); syn::custom_keyword!(subclass); syn::custom_keyword!(submodule); syn::custom_keyword!(text_signature); @@ -44,12 +46,137 @@ pub mod kw { syn::custom_keyword!(weakref); } +fn take_int(read: &mut &str, tracker: &mut usize) -> String { + let mut int = String::new(); + for (i, ch) in read.char_indices() { + match ch { + '0'..='9' => { + *tracker += 1; + int.push(ch) + } + _ => { + *read = &read[i..]; + break; + } + } + } + int +} + +fn take_ident(read: &mut &str, tracker: &mut usize) -> Ident { + let mut ident = String::new(); + if read.starts_with("r#") { + ident.push_str("r#"); + *tracker += 2; + *read = &read[2..]; + } + for (i, ch) in read.char_indices() { + match ch { + 'a'..='z' | 'A'..='Z' | '0'..='9' | '_' => { + *tracker += 1; + ident.push(ch) + } + _ => { + *read = &read[i..]; + break; + } + } + } + Ident::parse_any.parse_str(&ident).unwrap() +} + +// shorthand parsing logic inspiration taken from https://github.com/dtolnay/thiserror/blob/master/impl/src/fmt.rs +fn parse_shorthand_format(fmt: LitStr) -> Result<(LitStr, Vec)> { + let span = fmt.span(); + let token = fmt.token(); + let value = fmt.value(); + let mut read = value.as_str(); + let mut out = String::new(); + let mut members = Vec::new(); + let mut tracker = 1; + while let Some(brace) = read.find('{') { + tracker += brace; + out += &read[..brace + 1]; + read = &read[brace + 1..]; + if read.starts_with('{') { + out.push('{'); + read = &read[1..]; + tracker += 2; + continue; + } + let next = match read.chars().next() { + Some(next) => next, + None => break, + }; + tracker += 1; + let member = match next { + '0'..='9' => { + let start = tracker; + let index = take_int(&mut read, &mut tracker).parse::().unwrap(); + let end = tracker; + let subspan = token.subspan(start..end).unwrap_or(span); + let idx = Index { + index, + span: subspan, + }; + Member::Unnamed(idx) + } + 'a'..='z' | 'A'..='Z' | '_' => { + let start = tracker; + let mut ident = take_ident(&mut read, &mut tracker); + let end = tracker; + let subspan = token.subspan(start..end).unwrap_or(span); + ident.set_span(subspan); + Member::Named(ident) + } + '}' | ':' => { + let start = tracker; + tracker += 1; + let end = tracker; + let subspan = token.subspan(start..end).unwrap_or(span); + // we found a closing bracket or formatting ':' without finding a member, we assume the user wants the instance formatted here + bail_spanned!(subspan.span() => "No member found, you must provide a named or positionally specified member.") + } + _ => continue, + }; + members.push(member); + } + out += read; + Ok((LitStr::new(&out, span), members)) +} + +#[derive(Clone, Debug)] +pub struct StringFormatter { + pub fmt: LitStr, + pub args: Vec, +} + +impl Parse for crate::attributes::StringFormatter { + fn parse(input: ParseStream<'_>) -> Result { + let (fmt, args) = parse_shorthand_format(input.parse()?)?; + Ok(Self { fmt, args }) + } +} + +impl ToTokens for crate::attributes::StringFormatter { + fn to_tokens(&self, tokens: &mut TokenStream) { + self.fmt.to_tokens(tokens); + tokens.extend(quote! {self.args}) + } +} + #[derive(Clone, Debug)] pub struct KeywordAttribute { pub kw: K, pub value: V, } +#[derive(Clone, Debug)] +pub struct OptionalKeywordAttribute { + pub kw: K, + pub value: Option, +} + /// A helper type which parses the inner type via a literal string /// e.g. `LitStrValue` -> parses "some::path" in quotes. #[derive(Clone, Debug, PartialEq, Eq)] @@ -178,6 +305,7 @@ pub type FreelistAttribute = KeywordAttribute>; pub type ModuleAttribute = KeywordAttribute; pub type NameAttribute = KeywordAttribute; pub type RenameAllAttribute = KeywordAttribute; +pub type StrFormatterAttribute = OptionalKeywordAttribute; pub type TextSignatureAttribute = KeywordAttribute; pub type SubmoduleAttribute = kw::submodule; @@ -198,6 +326,27 @@ impl ToTokens for KeywordAttribute { } } +impl Parse for OptionalKeywordAttribute { + fn parse(input: ParseStream<'_>) -> Result { + let kw: K = input.parse()?; + let value = match input.parse::() { + Ok(_) => Some(input.parse()?), + Err(_) => None, + }; + Ok(OptionalKeywordAttribute { kw, value }) + } +} + +impl ToTokens for OptionalKeywordAttribute { + fn to_tokens(&self, tokens: &mut TokenStream) { + self.kw.to_tokens(tokens); + if self.value.is_some() { + Token![=](self.kw.span()).to_tokens(tokens); + self.value.to_tokens(tokens); + } + } +} + pub type FromPyWithAttribute = KeywordAttribute>; /// For specifying the path to the pyo3 crate. diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index 125fdab4927..dd1b023149f 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -1,16 +1,17 @@ use std::borrow::Cow; +use std::fmt::Debug; use proc_macro2::{Ident, Span, TokenStream}; use quote::{format_ident, quote, quote_spanned, ToTokens}; use syn::ext::IdentExt; use syn::parse::{Parse, ParseStream}; use syn::punctuated::Punctuated; -use syn::{parse_quote, parse_quote_spanned, spanned::Spanned, Result, Token}; +use syn::{parse_quote, parse_quote_spanned, spanned::Spanned, ImplItemFn, Result, Token}; use crate::attributes::kw::frozen; use crate::attributes::{ self, kw, take_pyo3_options, CrateAttribute, ExtendsAttribute, FreelistAttribute, - ModuleAttribute, NameAttribute, NameLitStr, RenameAllAttribute, + ModuleAttribute, NameAttribute, NameLitStr, RenameAllAttribute, StrFormatterAttribute, }; use crate::konst::{ConstAttributes, ConstSpec}; use crate::method::{FnArg, FnSpec, PyArg, RegularArg}; @@ -18,7 +19,7 @@ use crate::pyfunction::ConstructorAttribute; use crate::pyimpl::{gen_py_const, PyClassMethodsType}; use crate::pymethod::{ impl_py_getter_def, impl_py_setter_def, MethodAndMethodDef, MethodAndSlotDef, PropertyType, - SlotDef, __GETITEM__, __HASH__, __INT__, __LEN__, __REPR__, __RICHCMP__, + SlotDef, __GETITEM__, __HASH__, __INT__, __LEN__, __REPR__, __RICHCMP__, __STR__, }; use crate::pyversions; use crate::utils::{self, apply_renaming_rule, LitCStr, PythonDoc}; @@ -74,6 +75,7 @@ pub struct PyClassPyO3Options { pub rename_all: Option, pub sequence: Option, pub set_all: Option, + pub str: Option, pub subclass: Option, pub unsendable: Option, pub weakref: Option, @@ -96,6 +98,7 @@ pub enum PyClassPyO3Option { RenameAll(RenameAllAttribute), Sequence(kw::sequence), SetAll(kw::set_all), + Str(StrFormatterAttribute), Subclass(kw::subclass), Unsendable(kw::unsendable), Weakref(kw::weakref), @@ -136,6 +139,8 @@ impl Parse for PyClassPyO3Option { input.parse().map(PyClassPyO3Option::Sequence) } else if lookahead.peek(attributes::kw::set_all) { input.parse().map(PyClassPyO3Option::SetAll) + } else if lookahead.peek(attributes::kw::str) { + input.parse().map(PyClassPyO3Option::Str) } else if lookahead.peek(attributes::kw::subclass) { input.parse().map(PyClassPyO3Option::Subclass) } else if lookahead.peek(attributes::kw::unsendable) { @@ -205,6 +210,7 @@ impl PyClassPyO3Options { PyClassPyO3Option::RenameAll(rename_all) => set_option!(rename_all), PyClassPyO3Option::Sequence(sequence) => set_option!(sequence), PyClassPyO3Option::SetAll(set_all) => set_option!(set_all), + PyClassPyO3Option::Str(str) => set_option!(str), PyClassPyO3Option::Subclass(subclass) => set_option!(subclass), PyClassPyO3Option::Unsendable(unsendable) => set_option!(unsendable), PyClassPyO3Option::Weakref(weakref) => { @@ -387,6 +393,19 @@ fn impl_class( let Ctx { pyo3_path, .. } = ctx; let pytypeinfo_impl = impl_pytypeinfo(cls, args, ctx); + if let Some(str) = &args.options.str { + if str.value.is_some() { + // check if any renaming is present + let no_naming_conflict = field_options.iter().all(|x| x.1.name.is_none()) + & args.options.name.is_none() + & args.options.rename_all.is_none(); + ensure_spanned!(no_naming_conflict, str.value.span() => "The format string syntax is incompatible with any renaming via `name` or `rename_all`"); + } + } + + let (default_str, default_str_slot) = + implement_pyclass_str(&args.options, &syn::parse_quote!(#cls), ctx); + let (default_richcmp, default_richcmp_slot) = pyclass_richcmp(&args.options, &syn::parse_quote!(#cls), ctx)?; @@ -396,6 +415,7 @@ fn impl_class( let mut slots = Vec::new(); slots.extend(default_richcmp_slot); slots.extend(default_hash_slot); + slots.extend(default_str_slot); let py_class_impl = PyClassImplsBuilder::new( cls, @@ -425,6 +445,7 @@ fn impl_class( impl #cls { #default_richcmp #default_hash + #default_str } }) } @@ -753,6 +774,60 @@ impl EnumVariantPyO3Options { } } +// todo(remove this dead code allowance once __repr__ is implemented +#[allow(dead_code)] +pub enum PyFmtName { + Str, + Repr, +} + +fn implement_py_formatting( + ty: &syn::Type, + ctx: &Ctx, + option: &StrFormatterAttribute, +) -> (ImplItemFn, MethodAndSlotDef) { + let mut fmt_impl = match &option.value { + Some(opt) => { + let fmt = &opt.fmt; + let args = &opt + .args + .iter() + .map(|member| quote! {self.#member}) + .collect::>(); + let fmt_impl: ImplItemFn = syn::parse_quote! { + fn __pyo3__generated____str__(&self) -> ::std::string::String { + ::std::format!(#fmt, #(#args, )*) + } + }; + fmt_impl + } + None => { + let fmt_impl: syn::ImplItemFn = syn::parse_quote! { + fn __pyo3__generated____str__(&self) -> ::std::string::String { + ::std::format!("{}", &self) + } + }; + fmt_impl + } + }; + let fmt_slot = generate_protocol_slot(ty, &mut fmt_impl, &__STR__, "__str__", ctx).unwrap(); + (fmt_impl, fmt_slot) +} + +fn implement_pyclass_str( + options: &PyClassPyO3Options, + ty: &syn::Type, + ctx: &Ctx, +) -> (Option, Option) { + match &options.str { + Some(option) => { + let (default_str, default_str_slot) = implement_py_formatting(ty, ctx, option); + (Some(default_str), Some(default_str_slot)) + } + _ => (None, None), + } +} + fn impl_enum( enum_: PyClassEnum<'_>, args: &PyClassArgs, @@ -760,6 +835,10 @@ fn impl_enum( methods_type: PyClassMethodsType, ctx: &Ctx, ) -> Result { + if let Some(str_fmt) = &args.options.str { + ensure_spanned!(str_fmt.value.is_none(), str_fmt.value.span() => "The format string syntax cannot be used with enums") + } + match enum_ { PyClassEnum::Simple(simple_enum) => { impl_simple_enum(simple_enum, args, doc, methods_type, ctx) @@ -809,6 +888,8 @@ fn impl_simple_enum( (repr_impl, repr_slot) }; + let (default_str, default_str_slot) = implement_pyclass_str(&args.options, &ty, ctx); + let repr_type = &simple_enum.repr_type; let (default_int, default_int_slot) = { @@ -835,6 +916,7 @@ fn impl_simple_enum( let mut default_slots = vec![default_repr_slot, default_int_slot]; default_slots.extend(default_richcmp_slot); default_slots.extend(default_hash_slot); + default_slots.extend(default_str_slot); let pyclass_impls = PyClassImplsBuilder::new( cls, @@ -862,6 +944,7 @@ fn impl_simple_enum( #default_int #default_richcmp #default_hash + #default_str } }) } @@ -895,9 +978,12 @@ fn impl_complex_enum( let (default_richcmp, default_richcmp_slot) = pyclass_richcmp(&args.options, &ty, ctx)?; let (default_hash, default_hash_slot) = pyclass_hash(&args.options, &ty, ctx)?; + let (default_str, default_str_slot) = implement_pyclass_str(&args.options, &ty, ctx); + let mut default_slots = vec![]; default_slots.extend(default_richcmp_slot); default_slots.extend(default_hash_slot); + default_slots.extend(default_str_slot); let impl_builder = PyClassImplsBuilder::new( cls, @@ -1010,6 +1096,7 @@ fn impl_complex_enum( impl #cls { #default_richcmp #default_hash + #default_str } #(#variant_cls_zsts)* diff --git a/pyo3-macros-backend/src/pymethod.rs b/pyo3-macros-backend/src/pymethod.rs index 150c29ae64f..77cc9ed5cc6 100644 --- a/pyo3-macros-backend/src/pymethod.rs +++ b/pyo3-macros-backend/src/pymethod.rs @@ -902,7 +902,7 @@ impl PropertyType<'_> { } } -const __STR__: SlotDef = SlotDef::new("Py_tp_str", "reprfunc"); +pub const __STR__: SlotDef = SlotDef::new("Py_tp_str", "reprfunc"); pub const __REPR__: SlotDef = SlotDef::new("Py_tp_repr", "reprfunc"); pub const __HASH__: SlotDef = SlotDef::new("Py_tp_hash", "hashfunc") .ret_ty(Ty::PyHashT) diff --git a/src/tests/hygiene/pyclass.rs b/src/tests/hygiene/pyclass.rs index dcd347fb5f2..4270da34be3 100644 --- a/src/tests/hygiene/pyclass.rs +++ b/src/tests/hygiene/pyclass.rs @@ -91,3 +91,25 @@ pub enum TupleEnumEqOrd { Variant1(u32, u32), Variant2(u32), } + +#[crate::pyclass(str = "{x}, {y}, {z}")] +#[pyo3(crate = "crate")] +pub struct PointFmt { + x: u32, + y: u32, + z: u32, +} + +#[crate::pyclass(str)] +#[pyo3(crate = "crate")] +pub struct Point { + x: i32, + y: i32, + z: i32, +} + +impl ::std::fmt::Display for Point { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { + ::std::write!(f, "({}, {}, {})", self.x, self.y, self.z) + } +} diff --git a/tests/test_class_formatting.rs b/tests/test_class_formatting.rs new file mode 100644 index 00000000000..10f760b9c4a --- /dev/null +++ b/tests/test_class_formatting.rs @@ -0,0 +1,179 @@ +#![cfg(feature = "macros")] + +use pyo3::prelude::*; +use pyo3::py_run; +use std::fmt::{Display, Formatter}; + +#[path = "../src/tests/common.rs"] +mod common; + +#[pyclass(eq, str)] +#[derive(Debug, PartialEq)] +pub enum MyEnum2 { + Variant, + OtherVariant, +} + +impl Display for MyEnum2 { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} + +#[pyclass(eq, str)] +#[derive(Debug, PartialEq)] +pub enum MyEnum3 { + #[pyo3(name = "AwesomeVariant")] + Variant, + OtherVariant, +} + +impl Display for MyEnum3 { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let variant = match self { + MyEnum3::Variant => "AwesomeVariant", + MyEnum3::OtherVariant => "OtherVariant", + }; + write!(f, "MyEnum.{}", variant) + } +} + +#[test] +fn test_enum_class_fmt() { + Python::with_gil(|py| { + let var2 = Py::new(py, MyEnum2::Variant).unwrap(); + let var3 = Py::new(py, MyEnum3::Variant).unwrap(); + let var4 = Py::new(py, MyEnum3::OtherVariant).unwrap(); + py_assert!(py, var2, "str(var2) == 'Variant'"); + py_assert!(py, var3, "str(var3) == 'MyEnum.AwesomeVariant'"); + py_assert!(py, var4, "str(var4) == 'MyEnum.OtherVariant'"); + }) +} + +#[pyclass(str = "X: {x}, Y: {y}, Z: {z}")] +#[derive(PartialEq, Eq, Clone, PartialOrd)] +pub struct Point { + x: i32, + y: i32, + z: i32, +} + +#[test] +fn test_custom_struct_custom_str() { + Python::with_gil(|py| { + let var1 = Py::new(py, Point { x: 1, y: 2, z: 3 }).unwrap(); + py_assert!(py, var1, "str(var1) == 'X: 1, Y: 2, Z: 3'"); + }) +} + +#[pyclass(str)] +#[derive(PartialEq, Eq, Clone, PartialOrd)] +pub struct Point2 { + x: i32, + y: i32, + z: i32, +} + +impl Display for Point2 { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "({}, {}, {})", self.x, self.y, self.z) + } +} + +#[test] +fn test_struct_str() { + Python::with_gil(|py| { + let var1 = Py::new(py, Point2 { x: 1, y: 2, z: 3 }).unwrap(); + py_assert!(py, var1, "str(var1) == '(1, 2, 3)'"); + }) +} + +#[pyclass(str)] +#[derive(PartialEq, Debug)] +enum ComplexEnumWithStr { + A(u32), + B { msg: String }, +} + +impl Display for ComplexEnumWithStr { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} + +#[test] +fn test_custom_complex_enum_str() { + Python::with_gil(|py| { + let var1 = Py::new(py, ComplexEnumWithStr::A(45)).unwrap(); + let var2 = Py::new( + py, + ComplexEnumWithStr::B { + msg: "Hello".to_string(), + }, + ) + .unwrap(); + py_assert!(py, var1, "str(var1) == 'A(45)'"); + py_assert!(py, var2, "str(var2) == 'B { msg: \"Hello\" }'"); + }) +} + +#[pyclass(str = "{0}, {1}, {2}")] +#[derive(PartialEq)] +struct Coord(u32, u32, u32); + +#[pyclass(str = "{{{0}, {1}, {2}}}")] +#[derive(PartialEq)] +struct Coord2(u32, u32, u32); + +#[test] +fn test_str_representation_by_position() { + Python::with_gil(|py| { + let var1 = Py::new(py, Coord(1, 2, 3)).unwrap(); + let var2 = Py::new(py, Coord2(1, 2, 3)).unwrap(); + py_assert!(py, var1, "str(var1) == '1, 2, 3'"); + py_assert!(py, var2, "str(var2) == '{1, 2, 3}'"); + }) +} + +#[pyclass(str = "name: {name}: {name}, idn: {idn:03} with message: {msg}")] +#[derive(PartialEq, Debug)] +struct Point4 { + name: String, + msg: String, + idn: u32, +} + +#[test] +fn test_mixed_and_repeated_str_formats() { + Python::with_gil(|py| { + let var1 = Py::new( + py, + Point4 { + name: "aaa".to_string(), + msg: "hello".to_string(), + idn: 1, + }, + ) + .unwrap(); + py_run!( + py, + var1, + r#" + assert str(var1) == 'name: aaa: aaa, idn: 001 with message: hello' + "# + ); + }) +} + +#[pyclass(str = "type: {r#type}")] +struct Foo { + r#type: u32, +} + +#[test] +fn test_raw_identifier_struct_custom_str() { + Python::with_gil(|py| { + let var1 = Py::new(py, Foo { r#type: 3 }).unwrap(); + py_assert!(py, var1, "str(var1) == 'type: 3'"); + }) +} diff --git a/tests/ui/invalid_pyclass_args.rs b/tests/ui/invalid_pyclass_args.rs index f74fa49d8de..c39deab47bc 100644 --- a/tests/ui/invalid_pyclass_args.rs +++ b/tests/ui/invalid_pyclass_args.rs @@ -1,3 +1,4 @@ +use std::fmt::{Display, Formatter}; use pyo3::prelude::*; #[pyclass(extend=pyo3::types::PyDict)] @@ -76,4 +77,101 @@ struct InvalidOrderedStruct { inner: i32 } +#[pyclass(str)] +struct StrOptAndManualStr {} + +impl Display for StrOptAndManualStr { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self) + } +} + +#[pymethods] +impl StrOptAndManualStr { + fn __str__( + &self, + ) -> String { + todo!() + } +} + +#[pyclass(str = "{")] +#[derive(PartialEq)] +struct Coord(u32, u32, u32); + +#[pyclass(str = "{$}")] +#[derive(PartialEq)] +struct Coord2(u32, u32, u32); + +#[pyclass(str = "X: {aaaa}, Y: {y}, Z: {z}")] +#[derive(PartialEq, Eq, Clone, PartialOrd)] +pub struct Point { + x: i32, + y: i32, + z: i32, +} + +#[pyclass(str = "X: {x}, Y: {y}}}, Z: {zzz}")] +#[derive(PartialEq, Eq, Clone, PartialOrd)] +pub struct Point2 { + x: i32, + y: i32, + z: i32, +} + +#[pyclass(str = "{0}, {162543}, {2}")] +#[derive(PartialEq)] +struct Coord3(u32, u32, u32); + +#[pyclass(name = "aaa", str="unsafe: {unsafe_variable}")] +struct StructRenamingWithStrFormatter { + #[pyo3(name = "unsafe", get, set)] + unsafe_variable: usize, +} + +#[pyclass(name = "aaa", str="unsafe: {unsafe_variable}")] +struct StructRenamingWithStrFormatter2 { + unsafe_variable: usize, +} + +#[pyclass(str="unsafe: {unsafe_variable}")] +struct StructRenamingWithStrFormatter3 { + #[pyo3(name = "unsafe", get, set)] + unsafe_variable: usize, +} + +#[pyclass(rename_all = "SCREAMING_SNAKE_CASE", str="{a_a}, {b_b}, {c_d_e}")] +struct RenameAllVariantsStruct { + a_a: u32, + b_b: u32, + c_d_e: String, +} + +#[pyclass(str="{:?}")] +#[derive(Debug)] +struct StructWithNoMember { + a: String, + b: String, +} + +#[pyclass(str="{}")] +#[derive(Debug)] +struct StructWithNoMember2 { + a: String, + b: String, +} + +#[pyclass(eq, str="Stuff...")] +#[derive(Debug, PartialEq)] +pub enum MyEnumInvalidStrFmt { + Variant, + OtherVariant, +} + +impl Display for MyEnumInvalidStrFmt { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} + fn main() {} diff --git a/tests/ui/invalid_pyclass_args.stderr b/tests/ui/invalid_pyclass_args.stderr index 23d3c3bbc64..6faeca51502 100644 --- a/tests/ui/invalid_pyclass_args.stderr +++ b/tests/ui/invalid_pyclass_args.stderr @@ -1,223 +1,356 @@ -error: expected one of: `crate`, `dict`, `eq`, `eq_int`, `extends`, `freelist`, `frozen`, `get_all`, `hash`, `mapping`, `module`, `name`, `ord`, `rename_all`, `sequence`, `set_all`, `subclass`, `unsendable`, `weakref` - --> tests/ui/invalid_pyclass_args.rs:3:11 +error: expected one of: `crate`, `dict`, `eq`, `eq_int`, `extends`, `freelist`, `frozen`, `get_all`, `hash`, `mapping`, `module`, `name`, `ord`, `rename_all`, `sequence`, `set_all`, `str`, `subclass`, `unsendable`, `weakref` + --> tests/ui/invalid_pyclass_args.rs:4:11 | -3 | #[pyclass(extend=pyo3::types::PyDict)] +4 | #[pyclass(extend=pyo3::types::PyDict)] | ^^^^^^ error: expected identifier - --> tests/ui/invalid_pyclass_args.rs:6:21 + --> tests/ui/invalid_pyclass_args.rs:7:21 | -6 | #[pyclass(extends = "PyDict")] +7 | #[pyclass(extends = "PyDict")] | ^^^^^^^^ error: expected string literal - --> tests/ui/invalid_pyclass_args.rs:9:18 - | -9 | #[pyclass(name = m::MyClass)] - | ^ + --> tests/ui/invalid_pyclass_args.rs:10:18 + | +10 | #[pyclass(name = m::MyClass)] + | ^ error: expected a single identifier in double quotes - --> tests/ui/invalid_pyclass_args.rs:12:18 + --> tests/ui/invalid_pyclass_args.rs:13:18 | -12 | #[pyclass(name = "Custom Name")] +13 | #[pyclass(name = "Custom Name")] | ^^^^^^^^^^^^^ error: expected string literal - --> tests/ui/invalid_pyclass_args.rs:15:18 + --> tests/ui/invalid_pyclass_args.rs:16:18 | -15 | #[pyclass(name = CustomName)] +16 | #[pyclass(name = CustomName)] | ^^^^^^^^^^ error: expected string literal - --> tests/ui/invalid_pyclass_args.rs:18:24 + --> tests/ui/invalid_pyclass_args.rs:19:24 | -18 | #[pyclass(rename_all = camelCase)] +19 | #[pyclass(rename_all = camelCase)] | ^^^^^^^^^ error: expected a valid renaming rule, possible values are: "camelCase", "kebab-case", "lowercase", "PascalCase", "SCREAMING-KEBAB-CASE", "SCREAMING_SNAKE_CASE", "snake_case", "UPPERCASE" - --> tests/ui/invalid_pyclass_args.rs:21:24 + --> tests/ui/invalid_pyclass_args.rs:22:24 | -21 | #[pyclass(rename_all = "Camel-Case")] +22 | #[pyclass(rename_all = "Camel-Case")] | ^^^^^^^^^^^^ error: expected string literal - --> tests/ui/invalid_pyclass_args.rs:24:20 + --> tests/ui/invalid_pyclass_args.rs:25:20 | -24 | #[pyclass(module = my_module)] +25 | #[pyclass(module = my_module)] | ^^^^^^^^^ -error: expected one of: `crate`, `dict`, `eq`, `eq_int`, `extends`, `freelist`, `frozen`, `get_all`, `hash`, `mapping`, `module`, `name`, `ord`, `rename_all`, `sequence`, `set_all`, `subclass`, `unsendable`, `weakref` - --> tests/ui/invalid_pyclass_args.rs:27:11 +error: expected one of: `crate`, `dict`, `eq`, `eq_int`, `extends`, `freelist`, `frozen`, `get_all`, `hash`, `mapping`, `module`, `name`, `ord`, `rename_all`, `sequence`, `set_all`, `str`, `subclass`, `unsendable`, `weakref` + --> tests/ui/invalid_pyclass_args.rs:28:11 | -27 | #[pyclass(weakrev)] +28 | #[pyclass(weakrev)] | ^^^^^^^ error: a `#[pyclass]` cannot be both a `mapping` and a `sequence` - --> tests/ui/invalid_pyclass_args.rs:31:8 + --> tests/ui/invalid_pyclass_args.rs:32:8 | -31 | struct CannotBeMappingAndSequence {} +32 | struct CannotBeMappingAndSequence {} | ^^^^^^^^^^^^^^^^^^^^^^^^^^ error: `eq_int` can only be used on simple enums. - --> tests/ui/invalid_pyclass_args.rs:52:11 + --> tests/ui/invalid_pyclass_args.rs:53:11 | -52 | #[pyclass(eq_int)] +53 | #[pyclass(eq_int)] | ^^^^^^ error: The `hash` option requires the `frozen` option. - --> tests/ui/invalid_pyclass_args.rs:59:11 + --> tests/ui/invalid_pyclass_args.rs:60:11 | -59 | #[pyclass(hash)] +60 | #[pyclass(hash)] | ^^^^ error: The `hash` option requires the `eq` option. - --> tests/ui/invalid_pyclass_args.rs:59:11 + --> tests/ui/invalid_pyclass_args.rs:60:11 | -59 | #[pyclass(hash)] +60 | #[pyclass(hash)] | ^^^^ error: The `ord` option requires the `eq` option. - --> tests/ui/invalid_pyclass_args.rs:74:11 + --> tests/ui/invalid_pyclass_args.rs:75:11 | -74 | #[pyclass(ord)] +75 | #[pyclass(ord)] | ^^^ +error: invalid format string: expected `'}'` but string was terminated + --> tests/ui/invalid_pyclass_args.rs:98:19 + | +98 | #[pyclass(str = "{")] + | -^ expected `'}'` in format string + | | + | because of this opening brace + | + = note: if you intended to print `{`, you can escape it using `{{` + +error: invalid format string: expected `'}'`, found `'$'` + --> tests/ui/invalid_pyclass_args.rs:102:19 + | +102 | #[pyclass(str = "{$}")] + | -^ expected `'}'` in format string + | | + | because of this opening brace + | + = note: if you intended to print `{`, you can escape it using `{{` + +error: The format string syntax is incompatible with any renaming via `name` or `rename_all` + --> tests/ui/invalid_pyclass_args.rs:126:29 + | +126 | #[pyclass(name = "aaa", str="unsafe: {unsafe_variable}")] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +error: The format string syntax is incompatible with any renaming via `name` or `rename_all` + --> tests/ui/invalid_pyclass_args.rs:132:29 + | +132 | #[pyclass(name = "aaa", str="unsafe: {unsafe_variable}")] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +error: The format string syntax is incompatible with any renaming via `name` or `rename_all` + --> tests/ui/invalid_pyclass_args.rs:137:15 + | +137 | #[pyclass(str="unsafe: {unsafe_variable}")] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +error: The format string syntax is incompatible with any renaming via `name` or `rename_all` + --> tests/ui/invalid_pyclass_args.rs:143:52 + | +143 | #[pyclass(rename_all = "SCREAMING_SNAKE_CASE", str="{a_a}, {b_b}, {c_d_e}")] + | ^^^^^^^^^^^^^^^^^^^^^^^ + +error: No member found, you must provide a named or positionally specified member. + --> tests/ui/invalid_pyclass_args.rs:150:15 + | +150 | #[pyclass(str="{:?}")] + | ^^^^^^ + +error: No member found, you must provide a named or positionally specified member. + --> tests/ui/invalid_pyclass_args.rs:157:15 + | +157 | #[pyclass(str="{}")] + | ^^^^ + +error: The format string syntax cannot be used with enums + --> tests/ui/invalid_pyclass_args.rs:164:19 + | +164 | #[pyclass(eq, str="Stuff...")] + | ^^^^^^^^^^ + error[E0592]: duplicate definitions with name `__pymethod___richcmp____` - --> tests/ui/invalid_pyclass_args.rs:36:1 + --> tests/ui/invalid_pyclass_args.rs:37:1 | -36 | #[pyclass(eq)] +37 | #[pyclass(eq)] | ^^^^^^^^^^^^^^ duplicate definitions for `__pymethod___richcmp____` ... -40 | #[pymethods] +41 | #[pymethods] | ------------ other definition for `__pymethod___richcmp____` | = note: this error originates in the attribute macro `pyclass` (in Nightly builds, run with -Z macro-backtrace for more info) error[E0592]: duplicate definitions with name `__pymethod___hash____` - --> tests/ui/invalid_pyclass_args.rs:63:1 + --> tests/ui/invalid_pyclass_args.rs:64:1 | -63 | #[pyclass(frozen, eq, hash)] +64 | #[pyclass(frozen, eq, hash)] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ duplicate definitions for `__pymethod___hash____` ... -67 | #[pymethods] +68 | #[pymethods] | ------------ other definition for `__pymethod___hash____` | = note: this error originates in the attribute macro `pyclass` (in Nightly builds, run with -Z macro-backtrace for more info) +error[E0592]: duplicate definitions with name `__pymethod___str____` + --> tests/ui/invalid_pyclass_args.rs:80:1 + | +80 | #[pyclass(str)] + | ^^^^^^^^^^^^^^^ duplicate definitions for `__pymethod___str____` +... +89 | #[pymethods] + | ------------ other definition for `__pymethod___str____` + | + = note: this error originates in the attribute macro `pyclass` (in Nightly builds, run with -Z macro-backtrace for more info) + error[E0369]: binary operation `==` cannot be applied to type `&EqOptRequiresEq` - --> tests/ui/invalid_pyclass_args.rs:33:11 + --> tests/ui/invalid_pyclass_args.rs:34:11 | -33 | #[pyclass(eq)] +34 | #[pyclass(eq)] | ^^ | note: an implementation of `PartialEq` might be missing for `EqOptRequiresEq` - --> tests/ui/invalid_pyclass_args.rs:34:1 + --> tests/ui/invalid_pyclass_args.rs:35:1 | -34 | struct EqOptRequiresEq {} +35 | struct EqOptRequiresEq {} | ^^^^^^^^^^^^^^^^^^^^^^ must implement `PartialEq` help: consider annotating `EqOptRequiresEq` with `#[derive(PartialEq)]` | -34 + #[derive(PartialEq)] -35 | struct EqOptRequiresEq {} +35 + #[derive(PartialEq)] +36 | struct EqOptRequiresEq {} | error[E0369]: binary operation `!=` cannot be applied to type `&EqOptRequiresEq` - --> tests/ui/invalid_pyclass_args.rs:33:11 + --> tests/ui/invalid_pyclass_args.rs:34:11 | -33 | #[pyclass(eq)] +34 | #[pyclass(eq)] | ^^ | note: an implementation of `PartialEq` might be missing for `EqOptRequiresEq` - --> tests/ui/invalid_pyclass_args.rs:34:1 + --> tests/ui/invalid_pyclass_args.rs:35:1 | -34 | struct EqOptRequiresEq {} +35 | struct EqOptRequiresEq {} | ^^^^^^^^^^^^^^^^^^^^^^ must implement `PartialEq` help: consider annotating `EqOptRequiresEq` with `#[derive(PartialEq)]` | -34 + #[derive(PartialEq)] -35 | struct EqOptRequiresEq {} +35 + #[derive(PartialEq)] +36 | struct EqOptRequiresEq {} | error[E0034]: multiple applicable items in scope - --> tests/ui/invalid_pyclass_args.rs:36:1 + --> tests/ui/invalid_pyclass_args.rs:37:1 | -36 | #[pyclass(eq)] +37 | #[pyclass(eq)] | ^^^^^^^^^^^^^^ multiple `__pymethod___richcmp____` found | note: candidate #1 is defined in an impl for the type `EqOptAndManualRichCmp` - --> tests/ui/invalid_pyclass_args.rs:36:1 + --> tests/ui/invalid_pyclass_args.rs:37:1 | -36 | #[pyclass(eq)] +37 | #[pyclass(eq)] | ^^^^^^^^^^^^^^ note: candidate #2 is defined in an impl for the type `EqOptAndManualRichCmp` - --> tests/ui/invalid_pyclass_args.rs:40:1 + --> tests/ui/invalid_pyclass_args.rs:41:1 | -40 | #[pymethods] +41 | #[pymethods] | ^^^^^^^^^^^^ = note: this error originates in the attribute macro `pyclass` which comes from the expansion of the attribute macro `pymethods` (in Nightly builds, run with -Z macro-backtrace for more info) error[E0034]: multiple applicable items in scope - --> tests/ui/invalid_pyclass_args.rs:40:1 + --> tests/ui/invalid_pyclass_args.rs:41:1 | -40 | #[pymethods] +41 | #[pymethods] | ^^^^^^^^^^^^ multiple `__pymethod___richcmp____` found | note: candidate #1 is defined in an impl for the type `EqOptAndManualRichCmp` - --> tests/ui/invalid_pyclass_args.rs:36:1 + --> tests/ui/invalid_pyclass_args.rs:37:1 | -36 | #[pyclass(eq)] +37 | #[pyclass(eq)] | ^^^^^^^^^^^^^^ note: candidate #2 is defined in an impl for the type `EqOptAndManualRichCmp` - --> tests/ui/invalid_pyclass_args.rs:40:1 + --> tests/ui/invalid_pyclass_args.rs:41:1 | -40 | #[pymethods] +41 | #[pymethods] | ^^^^^^^^^^^^ = 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 `HashOptRequiresHash: Hash` is not satisfied - --> tests/ui/invalid_pyclass_args.rs:55:23 + --> tests/ui/invalid_pyclass_args.rs:56:23 | -55 | #[pyclass(frozen, eq, hash)] +56 | #[pyclass(frozen, eq, hash)] | ^^^^ the trait `Hash` is not implemented for `HashOptRequiresHash` | help: consider annotating `HashOptRequiresHash` with `#[derive(Hash)]` | -57 + #[derive(Hash)] -58 | struct HashOptRequiresHash; +58 + #[derive(Hash)] +59 | struct HashOptRequiresHash; | error[E0034]: multiple applicable items in scope - --> tests/ui/invalid_pyclass_args.rs:63:1 + --> tests/ui/invalid_pyclass_args.rs:64:1 | -63 | #[pyclass(frozen, eq, hash)] +64 | #[pyclass(frozen, eq, hash)] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ multiple `__pymethod___hash____` found | note: candidate #1 is defined in an impl for the type `HashOptAndManualHash` - --> tests/ui/invalid_pyclass_args.rs:63:1 + --> tests/ui/invalid_pyclass_args.rs:64:1 | -63 | #[pyclass(frozen, eq, hash)] +64 | #[pyclass(frozen, eq, hash)] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ note: candidate #2 is defined in an impl for the type `HashOptAndManualHash` - --> tests/ui/invalid_pyclass_args.rs:67:1 + --> tests/ui/invalid_pyclass_args.rs:68:1 | -67 | #[pymethods] +68 | #[pymethods] | ^^^^^^^^^^^^ = note: this error originates in the attribute macro `pyclass` which comes from the expansion of the attribute macro `pymethods` (in Nightly builds, run with -Z macro-backtrace for more info) error[E0034]: multiple applicable items in scope - --> tests/ui/invalid_pyclass_args.rs:67:1 + --> tests/ui/invalid_pyclass_args.rs:68:1 | -67 | #[pymethods] +68 | #[pymethods] | ^^^^^^^^^^^^ multiple `__pymethod___hash____` found | note: candidate #1 is defined in an impl for the type `HashOptAndManualHash` - --> tests/ui/invalid_pyclass_args.rs:63:1 + --> tests/ui/invalid_pyclass_args.rs:64:1 | -63 | #[pyclass(frozen, eq, hash)] +64 | #[pyclass(frozen, eq, hash)] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ note: candidate #2 is defined in an impl for the type `HashOptAndManualHash` - --> tests/ui/invalid_pyclass_args.rs:67:1 + --> tests/ui/invalid_pyclass_args.rs:68:1 + | +68 | #[pymethods] + | ^^^^^^^^^^^^ + = note: this error originates in the attribute macro `pymethods` (in Nightly builds, run with -Z macro-backtrace for more info) + +error[E0034]: multiple applicable items in scope + --> tests/ui/invalid_pyclass_args.rs:80:1 + | +80 | #[pyclass(str)] + | ^^^^^^^^^^^^^^^ multiple `__pymethod___str____` found + | +note: candidate #1 is defined in an impl for the type `StrOptAndManualStr` + --> tests/ui/invalid_pyclass_args.rs:80:1 + | +80 | #[pyclass(str)] + | ^^^^^^^^^^^^^^^ +note: candidate #2 is defined in an impl for the type `StrOptAndManualStr` + --> tests/ui/invalid_pyclass_args.rs:89:1 + | +89 | #[pymethods] + | ^^^^^^^^^^^^ + = note: this error originates in the attribute macro `pyclass` which comes from the expansion of the attribute macro `pymethods` (in Nightly builds, run with -Z macro-backtrace for more info) + +error[E0034]: multiple applicable items in scope + --> tests/ui/invalid_pyclass_args.rs:89:1 + | +89 | #[pymethods] + | ^^^^^^^^^^^^ multiple `__pymethod___str____` found | -67 | #[pymethods] +note: candidate #1 is defined in an impl for the type `StrOptAndManualStr` + --> tests/ui/invalid_pyclass_args.rs:80:1 + | +80 | #[pyclass(str)] + | ^^^^^^^^^^^^^^^ +note: candidate #2 is defined in an impl for the type `StrOptAndManualStr` + --> tests/ui/invalid_pyclass_args.rs:89:1 + | +89 | #[pymethods] | ^^^^^^^^^^^^ = note: this error originates in the attribute macro `pymethods` (in Nightly builds, run with -Z macro-backtrace for more info) + +error[E0609]: no field `aaaa` on type `&Point` + --> tests/ui/invalid_pyclass_args.rs:106:17 + | +106 | #[pyclass(str = "X: {aaaa}, Y: {y}, Z: {z}")] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ unknown field + | + = note: available fields are: `x`, `y`, `z` + +error[E0609]: no field `zzz` on type `&Point2` + --> tests/ui/invalid_pyclass_args.rs:114:17 + | +114 | #[pyclass(str = "X: {x}, Y: {y}}}, Z: {zzz}")] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ unknown field + | + = note: available fields are: `x`, `y`, `z` + +error[E0609]: no field `162543` on type `&Coord3` + --> tests/ui/invalid_pyclass_args.rs:122:17 + | +122 | #[pyclass(str = "{0}, {162543}, {2}")] + | ^^^^^^^^^^^^^^^^^^^^ unknown field + | + = note: available fields are: `0`, `1`, `2` From a84dae02134ed2f43cba03b75c18328b6b9fb39a Mon Sep 17 00:00:00 2001 From: Cheuk Ting Ho Date: Sat, 20 Jul 2024 07:28:16 +0100 Subject: [PATCH 169/495] Collecting multiple attribute error (#4243) * collecting multiple errors * collecting errors from different fileds * adding changelog * Adding UI test * refactoring * Update pyo3-macros-backend/src/attributes.rs Co-authored-by: David Hewitt * Update newsfragments/4243.changed.md Co-authored-by: David Hewitt * Update tests/ui/invalid_pyclass_args.rs Co-authored-by: David Hewitt * using pural for names * get rid of internidiate field_options_res * reset ordering --------- Co-authored-by: David Hewitt --- newsfragments/4243.changed.md | 1 + pyo3-macros-backend/src/attributes.rs | 38 ++++++-- pyo3-macros-backend/src/pyclass.rs | 39 ++++++--- tests/ui/invalid_pyclass_args.rs | 31 ++++--- tests/ui/invalid_pyclass_args.stderr | 120 +++++++++++++++----------- 5 files changed, 149 insertions(+), 80 deletions(-) create mode 100644 newsfragments/4243.changed.md diff --git a/newsfragments/4243.changed.md b/newsfragments/4243.changed.md new file mode 100644 index 00000000000..d9464ab37aa --- /dev/null +++ b/newsfragments/4243.changed.md @@ -0,0 +1 @@ +Report multiple errors from `#[pyclass]` and `#[pyo3(..)]` attributes. diff --git a/pyo3-macros-backend/src/attributes.rs b/pyo3-macros-backend/src/attributes.rs index 780ad7035f0..94526e7dafc 100644 --- a/pyo3-macros-backend/src/attributes.rs +++ b/pyo3-macros-backend/src/attributes.rs @@ -384,13 +384,41 @@ pub fn take_attributes( pub fn take_pyo3_options(attrs: &mut Vec) -> Result> { let mut out = Vec::new(); - take_attributes(attrs, |attr| { - if let Some(options) = get_pyo3_options(attr)? { - out.extend(options); + let mut all_errors = ErrorCombiner(None); + take_attributes(attrs, |attr| match get_pyo3_options(attr) { + Ok(result) => { + if let Some(options) = result { + out.extend(options); + Ok(true) + } else { + Ok(false) + } + } + Err(err) => { + all_errors.combine(err); Ok(true) - } else { - Ok(false) } })?; + all_errors.ensure_empty()?; Ok(out) } + +pub struct ErrorCombiner(pub Option); + +impl ErrorCombiner { + pub fn combine(&mut self, error: syn::Error) { + if let Some(existing) = &mut self.0 { + existing.combine(error); + } else { + self.0 = Some(error); + } + } + + pub fn ensure_empty(self) -> Result<()> { + if let Some(error) = self.0 { + Err(error) + } else { + Ok(()) + } + } +} diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index dd1b023149f..65fffc1655f 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -10,8 +10,9 @@ use syn::{parse_quote, parse_quote_spanned, spanned::Spanned, ImplItemFn, Result use crate::attributes::kw::frozen; use crate::attributes::{ - self, kw, take_pyo3_options, CrateAttribute, ExtendsAttribute, FreelistAttribute, - ModuleAttribute, NameAttribute, NameLitStr, RenameAllAttribute, StrFormatterAttribute, + self, kw, take_pyo3_options, CrateAttribute, ErrorCombiner, ExtendsAttribute, + FreelistAttribute, ModuleAttribute, NameAttribute, NameLitStr, RenameAllAttribute, + StrFormatterAttribute, }; use crate::konst::{ConstAttributes, ConstSpec}; use crate::method::{FnArg, FnSpec, PyArg, RegularArg}; @@ -252,23 +253,35 @@ pub fn build_py_class( ) ); + let mut all_errors = ErrorCombiner(None); + let mut field_options: Vec<(&syn::Field, FieldPyO3Options)> = match &mut class.fields { syn::Fields::Named(fields) => fields .named .iter_mut() - .map(|field| { - FieldPyO3Options::take_pyo3_options(&mut field.attrs) - .map(move |options| (&*field, options)) - }) - .collect::>()?, + .filter_map( + |field| match FieldPyO3Options::take_pyo3_options(&mut field.attrs) { + Ok(options) => Some((&*field, options)), + Err(e) => { + all_errors.combine(e); + None + } + }, + ) + .collect::>(), syn::Fields::Unnamed(fields) => fields .unnamed .iter_mut() - .map(|field| { - FieldPyO3Options::take_pyo3_options(&mut field.attrs) - .map(move |options| (&*field, options)) - }) - .collect::>()?, + .filter_map( + |field| match FieldPyO3Options::take_pyo3_options(&mut field.attrs) { + Ok(options) => Some((&*field, options)), + Err(e) => { + all_errors.combine(e); + None + } + }, + ) + .collect::>(), syn::Fields::Unit => { if let Some(attr) = args.options.set_all { return Err(syn::Error::new_spanned(attr, UNIT_SET)); @@ -281,6 +294,8 @@ pub fn build_py_class( } }; + all_errors.ensure_empty()?; + if let Some(attr) = args.options.get_all { for (_, FieldPyO3Options { get, .. }) in &mut field_options { if let Some(old_get) = get.replace(Annotated::Struct(attr)) { diff --git a/tests/ui/invalid_pyclass_args.rs b/tests/ui/invalid_pyclass_args.rs index c39deab47bc..221fc6ca35f 100644 --- a/tests/ui/invalid_pyclass_args.rs +++ b/tests/ui/invalid_pyclass_args.rs @@ -1,5 +1,5 @@ -use std::fmt::{Display, Formatter}; use pyo3::prelude::*; +use std::fmt::{Display, Formatter}; #[pyclass(extend=pyo3::types::PyDict)] struct TypoIntheKey {} @@ -74,7 +74,16 @@ impl HashOptAndManualHash { #[pyclass(ord)] struct InvalidOrderedStruct { - inner: i32 + inner: i32, +} + +#[pyclass] +struct MultipleErrors { + #[pyo3(foo)] + #[pyo3(blah)] + x: i32, + #[pyo3(pop)] + y: i32, } #[pyclass(str)] @@ -88,9 +97,7 @@ impl Display for StrOptAndManualStr { #[pymethods] impl StrOptAndManualStr { - fn __str__( - &self, - ) -> String { + fn __str__(&self) -> String { todo!() } } @@ -123,45 +130,45 @@ pub struct Point2 { #[derive(PartialEq)] struct Coord3(u32, u32, u32); -#[pyclass(name = "aaa", str="unsafe: {unsafe_variable}")] +#[pyclass(name = "aaa", str = "unsafe: {unsafe_variable}")] struct StructRenamingWithStrFormatter { #[pyo3(name = "unsafe", get, set)] unsafe_variable: usize, } -#[pyclass(name = "aaa", str="unsafe: {unsafe_variable}")] +#[pyclass(name = "aaa", str = "unsafe: {unsafe_variable}")] struct StructRenamingWithStrFormatter2 { unsafe_variable: usize, } -#[pyclass(str="unsafe: {unsafe_variable}")] +#[pyclass(str = "unsafe: {unsafe_variable}")] struct StructRenamingWithStrFormatter3 { #[pyo3(name = "unsafe", get, set)] unsafe_variable: usize, } -#[pyclass(rename_all = "SCREAMING_SNAKE_CASE", str="{a_a}, {b_b}, {c_d_e}")] +#[pyclass(rename_all = "SCREAMING_SNAKE_CASE", str = "{a_a}, {b_b}, {c_d_e}")] struct RenameAllVariantsStruct { a_a: u32, b_b: u32, c_d_e: String, } -#[pyclass(str="{:?}")] +#[pyclass(str = "{:?}")] #[derive(Debug)] struct StructWithNoMember { a: String, b: String, } -#[pyclass(str="{}")] +#[pyclass(str = "{}")] #[derive(Debug)] struct StructWithNoMember2 { a: String, b: String, } -#[pyclass(eq, str="Stuff...")] +#[pyclass(eq, str = "Stuff...")] #[derive(Debug, PartialEq)] pub enum MyEnumInvalidStrFmt { Variant, diff --git a/tests/ui/invalid_pyclass_args.stderr b/tests/ui/invalid_pyclass_args.stderr index 6faeca51502..15aa0387cc6 100644 --- a/tests/ui/invalid_pyclass_args.stderr +++ b/tests/ui/invalid_pyclass_args.stderr @@ -82,20 +82,38 @@ error: The `ord` option requires the `eq` option. 75 | #[pyclass(ord)] | ^^^ -error: invalid format string: expected `'}'` but string was terminated - --> tests/ui/invalid_pyclass_args.rs:98:19 +error: expected one of: `get`, `set`, `name` + --> tests/ui/invalid_pyclass_args.rs:82:12 | -98 | #[pyclass(str = "{")] - | -^ expected `'}'` in format string - | | - | because of this opening brace +82 | #[pyo3(foo)] + | ^^^ + +error: expected one of: `get`, `set`, `name` + --> tests/ui/invalid_pyclass_args.rs:83:12 | - = note: if you intended to print `{`, you can escape it using `{{` +83 | #[pyo3(blah)] + | ^^^^ + +error: expected one of: `get`, `set`, `name` + --> tests/ui/invalid_pyclass_args.rs:85:12 + | +85 | #[pyo3(pop)] + | ^^^ + +error: invalid format string: expected `'}'` but string was terminated + --> tests/ui/invalid_pyclass_args.rs:105:19 + | +105 | #[pyclass(str = "{")] + | -^ expected `'}'` in format string + | | + | because of this opening brace + | + = note: if you intended to print `{`, you can escape it using `{{` error: invalid format string: expected `'}'`, found `'$'` - --> tests/ui/invalid_pyclass_args.rs:102:19 + --> tests/ui/invalid_pyclass_args.rs:109:19 | -102 | #[pyclass(str = "{$}")] +109 | #[pyclass(str = "{$}")] | -^ expected `'}'` in format string | | | because of this opening brace @@ -103,46 +121,46 @@ error: invalid format string: expected `'}'`, found `'$'` = note: if you intended to print `{`, you can escape it using `{{` error: The format string syntax is incompatible with any renaming via `name` or `rename_all` - --> tests/ui/invalid_pyclass_args.rs:126:29 + --> tests/ui/invalid_pyclass_args.rs:133:31 | -126 | #[pyclass(name = "aaa", str="unsafe: {unsafe_variable}")] - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ +133 | #[pyclass(name = "aaa", str = "unsafe: {unsafe_variable}")] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ error: The format string syntax is incompatible with any renaming via `name` or `rename_all` - --> tests/ui/invalid_pyclass_args.rs:132:29 + --> tests/ui/invalid_pyclass_args.rs:139:31 | -132 | #[pyclass(name = "aaa", str="unsafe: {unsafe_variable}")] - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ +139 | #[pyclass(name = "aaa", str = "unsafe: {unsafe_variable}")] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ error: The format string syntax is incompatible with any renaming via `name` or `rename_all` - --> tests/ui/invalid_pyclass_args.rs:137:15 + --> tests/ui/invalid_pyclass_args.rs:144:17 | -137 | #[pyclass(str="unsafe: {unsafe_variable}")] - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ +144 | #[pyclass(str = "unsafe: {unsafe_variable}")] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ error: The format string syntax is incompatible with any renaming via `name` or `rename_all` - --> tests/ui/invalid_pyclass_args.rs:143:52 + --> tests/ui/invalid_pyclass_args.rs:150:54 | -143 | #[pyclass(rename_all = "SCREAMING_SNAKE_CASE", str="{a_a}, {b_b}, {c_d_e}")] - | ^^^^^^^^^^^^^^^^^^^^^^^ +150 | #[pyclass(rename_all = "SCREAMING_SNAKE_CASE", str = "{a_a}, {b_b}, {c_d_e}")] + | ^^^^^^^^^^^^^^^^^^^^^^^ error: No member found, you must provide a named or positionally specified member. - --> tests/ui/invalid_pyclass_args.rs:150:15 + --> tests/ui/invalid_pyclass_args.rs:157:17 | -150 | #[pyclass(str="{:?}")] - | ^^^^^^ +157 | #[pyclass(str = "{:?}")] + | ^^^^^^ error: No member found, you must provide a named or positionally specified member. - --> tests/ui/invalid_pyclass_args.rs:157:15 + --> tests/ui/invalid_pyclass_args.rs:164:17 | -157 | #[pyclass(str="{}")] - | ^^^^ +164 | #[pyclass(str = "{}")] + | ^^^^ error: The format string syntax cannot be used with enums - --> tests/ui/invalid_pyclass_args.rs:164:19 + --> tests/ui/invalid_pyclass_args.rs:171:21 | -164 | #[pyclass(eq, str="Stuff...")] - | ^^^^^^^^^^ +171 | #[pyclass(eq, str = "Stuff...")] + | ^^^^^^^^^^ error[E0592]: duplicate definitions with name `__pymethod___richcmp____` --> tests/ui/invalid_pyclass_args.rs:37:1 @@ -167,12 +185,12 @@ error[E0592]: duplicate definitions with name `__pymethod___hash____` = note: this error originates in the attribute macro `pyclass` (in Nightly builds, run with -Z macro-backtrace for more info) error[E0592]: duplicate definitions with name `__pymethod___str____` - --> tests/ui/invalid_pyclass_args.rs:80:1 + --> tests/ui/invalid_pyclass_args.rs:89:1 | -80 | #[pyclass(str)] +89 | #[pyclass(str)] | ^^^^^^^^^^^^^^^ duplicate definitions for `__pymethod___str____` ... -89 | #[pymethods] +98 | #[pymethods] | ------------ other definition for `__pymethod___str____` | = note: this error originates in the attribute macro `pyclass` (in Nightly builds, run with -Z macro-backtrace for more info) @@ -296,61 +314,61 @@ note: candidate #2 is defined in an impl for the type `HashOptAndManualHash` = note: this error originates in the attribute macro `pymethods` (in Nightly builds, run with -Z macro-backtrace for more info) error[E0034]: multiple applicable items in scope - --> tests/ui/invalid_pyclass_args.rs:80:1 + --> tests/ui/invalid_pyclass_args.rs:89:1 | -80 | #[pyclass(str)] +89 | #[pyclass(str)] | ^^^^^^^^^^^^^^^ multiple `__pymethod___str____` found | note: candidate #1 is defined in an impl for the type `StrOptAndManualStr` - --> tests/ui/invalid_pyclass_args.rs:80:1 + --> tests/ui/invalid_pyclass_args.rs:89:1 | -80 | #[pyclass(str)] +89 | #[pyclass(str)] | ^^^^^^^^^^^^^^^ note: candidate #2 is defined in an impl for the type `StrOptAndManualStr` - --> tests/ui/invalid_pyclass_args.rs:89:1 + --> tests/ui/invalid_pyclass_args.rs:98:1 | -89 | #[pymethods] +98 | #[pymethods] | ^^^^^^^^^^^^ = note: this error originates in the attribute macro `pyclass` which comes from the expansion of the attribute macro `pymethods` (in Nightly builds, run with -Z macro-backtrace for more info) error[E0034]: multiple applicable items in scope - --> tests/ui/invalid_pyclass_args.rs:89:1 + --> tests/ui/invalid_pyclass_args.rs:98:1 | -89 | #[pymethods] +98 | #[pymethods] | ^^^^^^^^^^^^ multiple `__pymethod___str____` found | note: candidate #1 is defined in an impl for the type `StrOptAndManualStr` - --> tests/ui/invalid_pyclass_args.rs:80:1 + --> tests/ui/invalid_pyclass_args.rs:89:1 | -80 | #[pyclass(str)] +89 | #[pyclass(str)] | ^^^^^^^^^^^^^^^ note: candidate #2 is defined in an impl for the type `StrOptAndManualStr` - --> tests/ui/invalid_pyclass_args.rs:89:1 + --> tests/ui/invalid_pyclass_args.rs:98:1 | -89 | #[pymethods] +98 | #[pymethods] | ^^^^^^^^^^^^ = note: this error originates in the attribute macro `pymethods` (in Nightly builds, run with -Z macro-backtrace for more info) error[E0609]: no field `aaaa` on type `&Point` - --> tests/ui/invalid_pyclass_args.rs:106:17 + --> tests/ui/invalid_pyclass_args.rs:113:17 | -106 | #[pyclass(str = "X: {aaaa}, Y: {y}, Z: {z}")] +113 | #[pyclass(str = "X: {aaaa}, Y: {y}, Z: {z}")] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ unknown field | = note: available fields are: `x`, `y`, `z` error[E0609]: no field `zzz` on type `&Point2` - --> tests/ui/invalid_pyclass_args.rs:114:17 + --> tests/ui/invalid_pyclass_args.rs:121:17 | -114 | #[pyclass(str = "X: {x}, Y: {y}}}, Z: {zzz}")] +121 | #[pyclass(str = "X: {x}, Y: {y}}}, Z: {zzz}")] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ unknown field | = note: available fields are: `x`, `y`, `z` error[E0609]: no field `162543` on type `&Coord3` - --> tests/ui/invalid_pyclass_args.rs:122:17 + --> tests/ui/invalid_pyclass_args.rs:129:17 | -122 | #[pyclass(str = "{0}, {162543}, {2}")] +129 | #[pyclass(str = "{0}, {162543}, {2}")] | ^^^^^^^^^^^^^^^^^^^^ unknown field | = note: available fields are: `0`, `1`, `2` From b2b5203ca4fb103e84f1ae8a267ea2d2912d5307 Mon Sep 17 00:00:00 2001 From: Cheuk Ting Ho Date: Mon, 22 Jul 2024 21:56:00 +0100 Subject: [PATCH 170/495] Deprecate PyUnicode (#4370) * deprecate PyUnicode * adding changelog * added deprication warning * Update src/types/string.rs Co-authored-by: Icxolu <10486322+Icxolu@users.noreply.github.com> --------- Co-authored-by: Icxolu <10486322+Icxolu@users.noreply.github.com> --- guide/src/conversions/tables.md | 6 +++--- newsfragments/4370.removed.md | 1 + src/types/mod.rs | 3 ++- src/types/string.rs | 4 ++++ 4 files changed, 10 insertions(+), 4 deletions(-) create mode 100644 newsfragments/4370.removed.md diff --git a/guide/src/conversions/tables.md b/guide/src/conversions/tables.md index 8afb95fbfb3..531e48b8f73 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`, `char`, `OsString`, `PathBuf`, `Path` | `PyString`, `PyUnicode` | +| `str` | `String`, `Cow`, `&str`, `char`, `OsString`, `PathBuf`, `Path` | `PyString` | | `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] | `PyInt` | @@ -38,8 +38,8 @@ The table below contains the Python type and the corresponding function argument | `decimal.Decimal` | `rust_decimal::Decimal`[^7] | - | | `ipaddress.IPv4Address` | `std::net::IpAddr`, `std::net::IpV4Addr` | - | | `ipaddress.IPv6Address` | `std::net::IpAddr`, `std::net::IpV6Addr` | - | -| `os.PathLike ` | `PathBuf`, `Path` | `PyString`, `PyUnicode` | -| `pathlib.Path` | `PathBuf`, `Path` | `PyString`, `PyUnicode` | +| `os.PathLike ` | `PathBuf`, `Path` | `PyString` | +| `pathlib.Path` | `PathBuf`, `Path` | `PyString` | | `typing.Optional[T]` | `Option` | - | | `typing.Sequence[T]` | `Vec` | `PySequence` | | `typing.Mapping[K, V]` | `HashMap`, `BTreeMap`, `hashbrown::HashMap`[^3], `indexmap::IndexMap`[^4] | `&PyMapping` | diff --git a/newsfragments/4370.removed.md b/newsfragments/4370.removed.md new file mode 100644 index 00000000000..d54c6a26601 --- /dev/null +++ b/newsfragments/4370.removed.md @@ -0,0 +1 @@ +Deprecated PyUnicode in favour of PyString. diff --git a/src/types/mod.rs b/src/types/mod.rs index 0ca0eec66f2..d02e3606aa7 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -40,7 +40,8 @@ pub use self::set::{PySet, PySetMethods}; pub use self::slice::{PySlice, PySliceIndices, PySliceMethods}; #[cfg(not(Py_LIMITED_API))] pub use self::string::PyStringData; -pub use self::string::{PyString, PyString as PyUnicode, PyStringMethods}; +#[allow(deprecated)] +pub use self::string::{PyString, PyStringMethods, PyUnicode}; pub use self::traceback::{PyTraceback, PyTracebackMethods}; pub use self::tuple::{PyTuple, PyTupleMethods}; pub use self::typeobject::{PyType, PyTypeMethods}; diff --git a/src/types/string.rs b/src/types/string.rs index aec11ecfde2..97ede1a94b1 100644 --- a/src/types/string.rs +++ b/src/types/string.rs @@ -10,6 +10,10 @@ use crate::{ffi, Bound, IntoPy, Py, PyAny, PyResult, Python}; use std::borrow::Cow; use std::str; +/// Deprecated alias for [`PyString`]. +#[deprecated(since = "0.23.0", note = "use `PyString` instead")] +pub type PyUnicode = PyString; + /// Represents raw data backing a Python `str`. /// /// Python internally stores strings in various representations. This enumeration From 4a1355b19cb58ed3db5ee03a6663c3439c7c93d7 Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Tue, 23 Jul 2024 21:06:06 +0200 Subject: [PATCH 171/495] relax cfgs on `PyFunction` reexport to match its definition (#4375) --- src/types/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types/mod.rs b/src/types/mod.rs index d02e3606aa7..11d409edfa3 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -22,7 +22,7 @@ pub use self::float::{PyFloat, PyFloatMethods}; pub use self::frame::PyFrame; pub use self::frozenset::{PyFrozenSet, PyFrozenSetBuilder, PyFrozenSetMethods}; pub use self::function::PyCFunction; -#[cfg(all(not(Py_LIMITED_API), not(PyPy), not(GraalPy)))] +#[cfg(all(not(Py_LIMITED_API), not(all(PyPy, not(Py_3_8))), not(GraalPy)))] pub use self::function::PyFunction; pub use self::iterator::PyIterator; pub use self::list::{PyList, PyListMethods}; From 3bd87774dd4368e12014154b67b5083d4efda355 Mon Sep 17 00:00:00 2001 From: Zsolt Cserna Date: Wed, 24 Jul 2024 10:02:19 +0200 Subject: [PATCH 172/495] Improve error messages for #[pyfunction] defined inside #[pymethods] (#4349) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Improve error messages for #[pyfunction] defined inside #[pymethods] Make error message more specific when `#[pyfunction]` is used in `#[pymethods]`. Effectively, this replaces the error message: ``` error: static method needs #[staticmethod] attribute ``` To: ``` functions inside #[pymethods] do not need to be annotated with #[pyfunction] ``` ...and also removes the other misleading error messages to the function in question. Fixes #4340 Co-authored-by: László Vaskó <1771332+vlaci@users.noreply.github.com> * review fixes --------- Co-authored-by: László Vaskó <1771332+vlaci@users.noreply.github.com> --- newsfragments/4349.fixed.md | 1 + pyo3-macros-backend/src/module.rs | 37 +------------------ pyo3-macros-backend/src/pyimpl.rs | 25 ++++++++++++- pyo3-macros-backend/src/utils.rs | 36 ++++++++++++++++++ tests/test_compile_error.rs | 1 + tests/ui/invalid_pyfunction_definition.rs | 15 ++++++++ tests/ui/invalid_pyfunction_definition.stderr | 5 +++ 7 files changed, 83 insertions(+), 37 deletions(-) create mode 100644 newsfragments/4349.fixed.md create mode 100644 tests/ui/invalid_pyfunction_definition.rs create mode 100644 tests/ui/invalid_pyfunction_definition.stderr diff --git a/newsfragments/4349.fixed.md b/newsfragments/4349.fixed.md new file mode 100644 index 00000000000..0895ffa1ae1 --- /dev/null +++ b/newsfragments/4349.fixed.md @@ -0,0 +1 @@ +Improve error messages for `#[pyfunction]` defined inside `#[pymethods]` diff --git a/pyo3-macros-backend/src/module.rs b/pyo3-macros-backend/src/module.rs index 1e764176550..ea982f686b8 100644 --- a/pyo3-macros-backend/src/module.rs +++ b/pyo3-macros-backend/src/module.rs @@ -8,7 +8,7 @@ use crate::{ get_doc, pyclass::PyClassPyO3Option, pyfunction::{impl_wrap_pyfunction, PyFunctionOptions}, - utils::{Ctx, LitCStr, PyO3CratePath}, + utils::{has_attribute, has_attribute_with_namespace, Ctx, IdentOrStr, LitCStr}, }; use proc_macro2::{Span, TokenStream}; use quote::quote; @@ -565,11 +565,6 @@ fn find_and_remove_attribute(attrs: &mut Vec, ident: &str) -> bo found } -enum IdentOrStr<'a> { - Str(&'a str), - Ident(syn::Ident), -} - impl<'a> PartialEq for IdentOrStr<'a> { fn eq(&self, other: &syn::Ident) -> bool { match self { @@ -578,36 +573,6 @@ impl<'a> PartialEq for IdentOrStr<'a> { } } } -fn has_attribute(attrs: &[syn::Attribute], ident: &str) -> bool { - has_attribute_with_namespace(attrs, None, &[ident]) -} - -fn has_attribute_with_namespace( - attrs: &[syn::Attribute], - crate_path: Option<&PyO3CratePath>, - idents: &[&str], -) -> bool { - let mut segments = vec![]; - if let Some(c) = crate_path { - match c { - PyO3CratePath::Given(paths) => { - for p in &paths.segments { - segments.push(IdentOrStr::Ident(p.ident.clone())); - } - } - PyO3CratePath::Default => segments.push(IdentOrStr::Str("pyo3")), - } - }; - for i in idents { - segments.push(IdentOrStr::Str(i)); - } - - attrs.iter().any(|attr| { - segments - .iter() - .eq(attr.path().segments.iter().map(|v| &v.ident)) - }) -} fn set_module_attribute(attrs: &mut Vec, module_name: &str) { attrs.push(parse_quote!(#[pyo3(module = #module_name)])); diff --git a/pyo3-macros-backend/src/pyimpl.rs b/pyo3-macros-backend/src/pyimpl.rs index 786682f3882..2f054be165f 100644 --- a/pyo3-macros-backend/src/pyimpl.rs +++ b/pyo3-macros-backend/src/pyimpl.rs @@ -1,6 +1,6 @@ use std::collections::HashSet; -use crate::utils::Ctx; +use crate::utils::{has_attribute, has_attribute_with_namespace, Ctx, PyO3CratePath}; use crate::{ attributes::{take_pyo3_options, CrateAttribute}, konst::{ConstAttributes, ConstSpec}, @@ -10,6 +10,7 @@ use crate::{ use proc_macro2::TokenStream; use pymethod::GeneratedPyMethod; use quote::{format_ident, quote}; +use syn::ImplItemFn; use syn::{ parse::{Parse, ParseStream}, spanned::Spanned, @@ -84,6 +85,25 @@ pub fn build_py_methods( } } +fn check_pyfunction(pyo3_path: &PyO3CratePath, meth: &mut ImplItemFn) -> syn::Result<()> { + let mut error = None; + + meth.attrs.retain(|attr| { + let attrs = [attr.clone()]; + + if has_attribute(&attrs, "pyfunction") + || has_attribute_with_namespace(&attrs, Some(pyo3_path), &["pyfunction"]) + || has_attribute_with_namespace(&attrs, Some(pyo3_path), &["prelude", "pyfunction"]) { + error = Some(err_spanned!(meth.sig.span() => "functions inside #[pymethods] do not need to be annotated with #[pyfunction]")); + false + } else { + true + } + }); + + error.map_or(Ok(()), Err) +} + pub fn impl_methods( ty: &syn::Type, impls: &mut [syn::ImplItem], @@ -103,6 +123,9 @@ pub fn impl_methods( let ctx = &Ctx::new(&options.krate, Some(&meth.sig)); let mut fun_options = PyFunctionOptions::from_attrs(&mut meth.attrs)?; fun_options.krate = fun_options.krate.or_else(|| options.krate.clone()); + + check_pyfunction(&ctx.pyo3_path, meth)?; + match pymethod::gen_py_method(ty, &mut meth.sig, &mut meth.attrs, fun_options, ctx)? { GeneratedPyMethod::Method(MethodAndMethodDef { diff --git a/pyo3-macros-backend/src/utils.rs b/pyo3-macros-backend/src/utils.rs index 005884a557c..350abb6bbf6 100644 --- a/pyo3-macros-backend/src/utils.rs +++ b/pyo3-macros-backend/src/utils.rs @@ -291,3 +291,39 @@ pub fn apply_renaming_rule(rule: RenamingRule, name: &str) -> String { pub(crate) fn is_abi3() -> bool { pyo3_build_config::get().abi3 } + +pub(crate) enum IdentOrStr<'a> { + Str(&'a str), + Ident(syn::Ident), +} + +pub(crate) fn has_attribute(attrs: &[syn::Attribute], ident: &str) -> bool { + has_attribute_with_namespace(attrs, None, &[ident]) +} + +pub(crate) fn has_attribute_with_namespace( + attrs: &[syn::Attribute], + crate_path: Option<&PyO3CratePath>, + idents: &[&str], +) -> bool { + let mut segments = vec![]; + if let Some(c) = crate_path { + match c { + PyO3CratePath::Given(paths) => { + for p in &paths.segments { + segments.push(IdentOrStr::Ident(p.ident.clone())); + } + } + PyO3CratePath::Default => segments.push(IdentOrStr::Str("pyo3")), + } + }; + for i in idents { + segments.push(IdentOrStr::Str(i)); + } + + attrs.iter().any(|attr| { + segments + .iter() + .eq(attr.path().segments.iter().map(|v| &v.ident)) + }) +} diff --git a/tests/test_compile_error.rs b/tests/test_compile_error.rs index 0d3fa0459d3..eb9934af949 100644 --- a/tests/test_compile_error.rs +++ b/tests/test_compile_error.rs @@ -11,6 +11,7 @@ fn test_compile_errors() { t.compile_fail("tests/ui/invalid_pyclass_enum.rs"); t.compile_fail("tests/ui/invalid_pyclass_item.rs"); t.compile_fail("tests/ui/invalid_pyfunction_signatures.rs"); + t.compile_fail("tests/ui/invalid_pyfunction_definition.rs"); #[cfg(any(not(Py_LIMITED_API), Py_3_11))] t.compile_fail("tests/ui/invalid_pymethods_buffer.rs"); // The output is not stable across abi3 / not abi3 and features diff --git a/tests/ui/invalid_pyfunction_definition.rs b/tests/ui/invalid_pyfunction_definition.rs new file mode 100644 index 00000000000..2f08ff421b9 --- /dev/null +++ b/tests/ui/invalid_pyfunction_definition.rs @@ -0,0 +1,15 @@ +#[pyo3::pymodule] +mod pyo3_scratch { + use pyo3::prelude::*; + + #[pyclass] + struct Foo {} + + #[pymethods] + impl Foo { + #[pyfunction] + fn bug() {} + } +} + +fn main() {} diff --git a/tests/ui/invalid_pyfunction_definition.stderr b/tests/ui/invalid_pyfunction_definition.stderr new file mode 100644 index 00000000000..9c7cac1f0f3 --- /dev/null +++ b/tests/ui/invalid_pyfunction_definition.stderr @@ -0,0 +1,5 @@ +error: functions inside #[pymethods] do not need to be annotated with #[pyfunction] + --> tests/ui/invalid_pyfunction_definition.rs:11:9 + | +11 | fn bug() {} + | ^^ From 9a8e5b4df98f5cbe2bfb850b6556846795c8e392 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Wed, 24 Jul 2024 10:45:16 +0100 Subject: [PATCH 173/495] release: 0.22.2 (#4376) --- CHANGELOG.md | 20 +++++++++++++++++-- README.md | 4 ++-- 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/4324.changed.md | 1 - newsfragments/4327.packaging.md | 3 --- newsfragments/4330.changed.md | 1 - newsfragments/4346.changed.md | 1 - newsfragments/4353.fixed.md | 1 - 12 files changed, 25 insertions(+), 16 deletions(-) delete mode 100644 newsfragments/4324.changed.md delete mode 100644 newsfragments/4327.packaging.md delete mode 100644 newsfragments/4330.changed.md delete mode 100644 newsfragments/4346.changed.md delete mode 100644 newsfragments/4353.fixed.md diff --git a/CHANGELOG.md b/CHANGELOG.md index beabedc28de..caabd04129c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,22 @@ To see unreleased changes, please see the [CHANGELOG on the main branch guide](h +## [0.22.2] - 2024-07-17 + +### Packaging + +- Require opt-in to freethreaded Python using the `UNSAFE_PYO3_BUILD_FREE_THREADED=1` environment variable (it is not yet supported by PyO3). [#4327](https://github.com/PyO3/pyo3/pull/4327) + +### Changed + +- Use FFI function calls for reference counting on all abi3 versions. [#4324](https://github.com/PyO3/pyo3/pull/4324) +- `#[pymodule(...)]` now directly accepts all relevant `#[pyo3(...)]` options. [#4330](https://github.com/PyO3/pyo3/pull/4330) + +### Fixed + +- Fix compile failure in declarative `#[pymodule]` under presence of `#![no_implicit_prelude]`. [#4328](https://github.com/PyO3/pyo3/pull/4328) +- Fix compile failure due to c-string literals on Rust < 1.79. [#4353](https://github.com/PyO3/pyo3/pull/4353) + ## [0.22.1] - 2024-07-06 ### Added @@ -25,7 +41,6 @@ To see unreleased changes, please see the [CHANGELOG on the main branch guide](h - Stop generating code that will never be covered with declarative modules. [#4297](https://github.com/PyO3/pyo3/pull/4297) - Fix invalid deprecation warning for trailing optional on `#[setter]` function. [#4304](https://github.com/PyO3/pyo3/pull/4304) - ## [0.22.0] - 2024-06-24 ### Packaging @@ -1824,7 +1839,8 @@ Yanked - Initial release -[Unreleased]: https://github.com/pyo3/pyo3/compare/v0.22.1...HEAD +[Unreleased]: https://github.com/pyo3/pyo3/compare/v0.22.2...HEAD +[0.22.2]: https://github.com/pyo3/pyo3/compare/v0.22.1...v0.22.2 [0.22.1]: https://github.com/pyo3/pyo3/compare/v0.22.0...v0.22.1 [0.22.0]: https://github.com/pyo3/pyo3/compare/v0.21.2...v0.22.0 [0.21.2]: https://github.com/pyo3/pyo3/compare/v0.21.1...v0.21.2 diff --git a/README.md b/README.md index fd4acdd4154..2a6348437a2 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ name = "string_sum" crate-type = ["cdylib"] [dependencies] -pyo3 = { version = "0.22.1", features = ["extension-module"] } +pyo3 = { version = "0.22.2", 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.22.1" +version = "0.22.2" features = ["auto-initialize"] ``` diff --git a/examples/decorator/.template/pre-script.rhai b/examples/decorator/.template/pre-script.rhai index 659b9c14e2b..a9f0eb4a289 100644 --- a/examples/decorator/.template/pre-script.rhai +++ b/examples/decorator/.template/pre-script.rhai @@ -1,4 +1,4 @@ -variable::set("PYO3_VERSION", "0.23.0-dev"); +variable::set("PYO3_VERSION", "0.22.2"); 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 659b9c14e2b..a9f0eb4a289 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.23.0-dev"); +variable::set("PYO3_VERSION", "0.22.2"); 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 5c9f9686c0b..bbc96358a23 100644 --- a/examples/plugin/.template/pre-script.rhai +++ b/examples/plugin/.template/pre-script.rhai @@ -1,4 +1,4 @@ -variable::set("PYO3_VERSION", "0.23.0-dev"); +variable::set("PYO3_VERSION", "0.22.2"); file::rename(".template/Cargo.toml", "Cargo.toml"); file::rename(".template/plugin_api/Cargo.toml", "plugin_api/Cargo.toml"); file::delete(".template"); diff --git a/examples/setuptools-rust-starter/.template/pre-script.rhai b/examples/setuptools-rust-starter/.template/pre-script.rhai index 00bb560445b..d28d9d09987 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.23.0-dev"); +variable::set("PYO3_VERSION", "0.22.2"); 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 659b9c14e2b..a9f0eb4a289 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.23.0-dev"); +variable::set("PYO3_VERSION", "0.22.2"); file::rename(".template/Cargo.toml", "Cargo.toml"); file::rename(".template/pyproject.toml", "pyproject.toml"); file::delete(".template"); diff --git a/newsfragments/4324.changed.md b/newsfragments/4324.changed.md deleted file mode 100644 index 2818159dda3..00000000000 --- a/newsfragments/4324.changed.md +++ /dev/null @@ -1 +0,0 @@ -Use FFI function calls for reference counting on all abi3 versions. diff --git a/newsfragments/4327.packaging.md b/newsfragments/4327.packaging.md deleted file mode 100644 index c98d06f7cba..00000000000 --- a/newsfragments/4327.packaging.md +++ /dev/null @@ -1,3 +0,0 @@ -This PR lets PyO3 checks `Py_GIL_DISABLED` build flag and prevents `pyo3-ffi` crate building against GIL-less Python, -unless -explicitly opt using the `UNSAFE_PYO3_BUILD_FREE_THREADED` environment flag. \ No newline at end of file diff --git a/newsfragments/4330.changed.md b/newsfragments/4330.changed.md deleted file mode 100644 index d465ec99cec..00000000000 --- a/newsfragments/4330.changed.md +++ /dev/null @@ -1 +0,0 @@ -`#[pymodule(...)]` now directly accepts all relevant `#[pyo3(...)]` options. diff --git a/newsfragments/4346.changed.md b/newsfragments/4346.changed.md deleted file mode 100644 index 1ac7952eae7..00000000000 --- a/newsfragments/4346.changed.md +++ /dev/null @@ -1 +0,0 @@ -chore: update `ruff` configuration to resolve deprecation warning diff --git a/newsfragments/4353.fixed.md b/newsfragments/4353.fixed.md deleted file mode 100644 index 73783479085..00000000000 --- a/newsfragments/4353.fixed.md +++ /dev/null @@ -1 +0,0 @@ -fixed compile error due to c-string literals on Rust < 1.79 \ No newline at end of file From cf559609f2956ed2a0529da0c4b6a3c1934fd1c9 Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Thu, 25 Jul 2024 18:50:08 +0200 Subject: [PATCH 174/495] remove gil-refs from `wrap_pyfunction` and `from_py_with` (#4343) * deprecate `wrap_pyfunction_bound` * remove `from_py_with` gil-ref extractors * reword deprecation msg Co-authored-by: David Hewitt --------- Co-authored-by: David Hewitt --- guide/src/function.md | 2 +- guide/src/function/error-handling.md | 8 +++--- guide/src/function/signature.md | 10 +++---- guide/src/migration.md | 4 +-- pytests/src/enums.rs | 10 +++---- src/conversions/anyhow.rs | 2 +- src/conversions/eyre.rs | 2 +- src/coroutine/waker.rs | 4 +-- src/derive_utils.rs | 33 --------------------- src/err/mod.rs | 4 +-- src/exceptions.rs | 4 +-- src/impl_/extract_argument.rs | 6 ++-- src/impl_/frompyobject.rs | 37 +++--------------------- src/impl_/pyfunction.rs | 38 ------------------------- src/lib.rs | 4 --- src/macros.rs | 27 ++++-------------- src/marker.rs | 2 +- src/prelude.rs | 5 +++- src/pycell.rs | 6 ++-- src/sync.rs | 4 +-- src/tests/hygiene/pymodule.rs | 2 +- src/types/bytearray.rs | 2 +- src/types/iterator.rs | 2 +- src/types/weakref/proxy.rs | 2 +- src/types/weakref/reference.rs | 2 +- tests/test_anyhow.rs | 6 ++-- tests/test_bytes.rs | 6 ++-- tests/test_coroutine.rs | 19 ++++++------- tests/test_enum.rs | 4 +-- tests/test_exceptions.rs | 4 +-- tests/test_macros.rs | 2 +- tests/test_methods.rs | 2 +- tests/test_pyfunction.rs | 32 ++++++++++----------- tests/test_string.rs | 2 +- tests/test_text_signature.rs | 18 ++++++------ tests/test_various.rs | 4 +-- tests/test_wrap_pyfunction_deduction.rs | 29 ------------------- tests/ui/invalid_result_conversion.rs | 2 +- 38 files changed, 101 insertions(+), 251 deletions(-) delete mode 100644 src/derive_utils.rs delete mode 100644 tests/test_wrap_pyfunction_deduction.rs diff --git a/guide/src/function.md b/guide/src/function.md index 5e6cfaba773..fd215a1550e 100644 --- a/guide/src/function.md +++ b/guide/src/function.md @@ -120,7 +120,7 @@ The `#[pyo3]` attribute can be used on individual arguments to modify properties } # Python::with_gil(|py| { - # let f = pyo3::wrap_pyfunction_bound!(object_length)(py).unwrap(); + # let f = pyo3::wrap_pyfunction!(object_length)(py).unwrap(); # assert_eq!(f.call1((vec![1, 2, 3],)).unwrap().extract::().unwrap(), 3); # }); ``` diff --git a/guide/src/function/error-handling.md b/guide/src/function/error-handling.md index f55fee90e54..0d7fae7976e 100644 --- a/guide/src/function/error-handling.md +++ b/guide/src/function/error-handling.md @@ -44,7 +44,7 @@ fn check_positive(x: i32) -> PyResult<()> { # # fn main(){ # Python::with_gil(|py|{ -# let fun = pyo3::wrap_pyfunction_bound!(check_positive, py).unwrap(); +# let fun = pyo3::wrap_pyfunction!(check_positive, py).unwrap(); # fun.call1((-1,)).unwrap_err(); # fun.call1((1,)).unwrap(); # }); @@ -72,7 +72,7 @@ fn parse_int(x: &str) -> Result { # fn main() { # Python::with_gil(|py| { -# let fun = pyo3::wrap_pyfunction_bound!(parse_int, py).unwrap(); +# let fun = pyo3::wrap_pyfunction!(parse_int, py).unwrap(); # let value: usize = fun.call1(("5",)).unwrap().extract().unwrap(); # assert_eq!(value, 5); # }); @@ -132,7 +132,7 @@ fn connect(s: String) -> Result<(), CustomIOError> { fn main() { Python::with_gil(|py| { - let fun = pyo3::wrap_pyfunction_bound!(connect, py).unwrap(); + let fun = pyo3::wrap_pyfunction!(connect, py).unwrap(); let err = fun.call1(("0.0.0.0",)).unwrap_err(); assert!(err.is_instance_of::(py)); }); @@ -224,7 +224,7 @@ fn wrapped_get_x() -> Result { # fn main() { # Python::with_gil(|py| { -# let fun = pyo3::wrap_pyfunction_bound!(wrapped_get_x, py).unwrap(); +# let fun = pyo3::wrap_pyfunction!(wrapped_get_x, py).unwrap(); # let value: usize = fun.call0().unwrap().extract().unwrap(); # assert_eq!(value, 5); # }); diff --git a/guide/src/function/signature.md b/guide/src/function/signature.md index a9be5983422..0b3d7a9a507 100644 --- a/guide/src/function/signature.md +++ b/guide/src/function/signature.md @@ -148,7 +148,7 @@ fn increment(x: u64, amount: Option) -> u64 { # # fn main() -> PyResult<()> { # Python::with_gil(|py| { -# let fun = pyo3::wrap_pyfunction_bound!(increment, py)?; +# let fun = pyo3::wrap_pyfunction!(increment, py)?; # # let inspect = PyModule::import_bound(py, "inspect")?.getattr("signature")?; # let sig: String = inspect @@ -176,7 +176,7 @@ fn increment(x: u64, amount: Option) -> u64 { # # fn main() -> PyResult<()> { # Python::with_gil(|py| { -# let fun = pyo3::wrap_pyfunction_bound!(increment, py)?; +# let fun = pyo3::wrap_pyfunction!(increment, py)?; # # let inspect = PyModule::import_bound(py, "inspect")?.getattr("signature")?; # let sig: String = inspect @@ -216,7 +216,7 @@ fn add(a: u64, b: u64) -> u64 { # # fn main() -> PyResult<()> { # Python::with_gil(|py| { -# let fun = pyo3::wrap_pyfunction_bound!(add, py)?; +# let fun = pyo3::wrap_pyfunction!(add, py)?; # # let doc: String = fun.getattr("__doc__")?.extract()?; # assert_eq!(doc, "This function adds two unsigned 64-bit integers."); @@ -264,7 +264,7 @@ fn add(a: u64, b: u64) -> u64 { # # fn main() -> PyResult<()> { # Python::with_gil(|py| { -# let fun = pyo3::wrap_pyfunction_bound!(add, py)?; +# let fun = pyo3::wrap_pyfunction!(add, py)?; # # let doc: String = fun.getattr("__doc__")?.extract()?; # assert_eq!(doc, "This function adds two unsigned 64-bit integers."); @@ -306,7 +306,7 @@ fn add(a: u64, b: u64) -> u64 { # # fn main() -> PyResult<()> { # Python::with_gil(|py| { -# let fun = pyo3::wrap_pyfunction_bound!(add, py)?; +# let fun = pyo3::wrap_pyfunction!(add, py)?; # # let doc: String = fun.getattr("__doc__")?.extract()?; # assert_eq!(doc, "This function adds two unsigned 64-bit integers."); diff --git a/guide/src/migration.md b/guide/src/migration.md index ab301affac0..f45ba567291 100644 --- a/guide/src/migration.md +++ b/guide/src/migration.md @@ -884,9 +884,9 @@ fn function_with_defaults(a: i32, b: i32, c: i32) {} # fn main() { # Python::with_gil(|py| { -# let simple = wrap_pyfunction_bound!(simple_function, py).unwrap(); +# let simple = wrap_pyfunction!(simple_function, py).unwrap(); # assert_eq!(simple.getattr("__text_signature__").unwrap().to_string(), "(a, b, c)"); -# let defaulted = wrap_pyfunction_bound!(function_with_defaults, py).unwrap(); +# let defaulted = wrap_pyfunction!(function_with_defaults, py).unwrap(); # assert_eq!(defaulted.getattr("__text_signature__").unwrap().to_string(), "(a, b=1, c=2)"); # }) # } diff --git a/pytests/src/enums.rs b/pytests/src/enums.rs index 80d7550e1ec..fb96c0a9366 100644 --- a/pytests/src/enums.rs +++ b/pytests/src/enums.rs @@ -1,7 +1,7 @@ use pyo3::{ pyclass, pyfunction, pymodule, types::{PyModule, PyModuleMethods}, - wrap_pyfunction_bound, Bound, PyResult, + wrap_pyfunction, Bound, PyResult, }; #[pymodule] @@ -11,10 +11,10 @@ pub fn enums(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; - m.add_wrapped(wrap_pyfunction_bound!(do_simple_stuff))?; - m.add_wrapped(wrap_pyfunction_bound!(do_complex_stuff))?; - m.add_wrapped(wrap_pyfunction_bound!(do_tuple_stuff))?; - m.add_wrapped(wrap_pyfunction_bound!(do_mixed_complex_stuff))?; + m.add_wrapped(wrap_pyfunction!(do_simple_stuff))?; + m.add_wrapped(wrap_pyfunction!(do_complex_stuff))?; + m.add_wrapped(wrap_pyfunction!(do_tuple_stuff))?; + m.add_wrapped(wrap_pyfunction!(do_mixed_complex_stuff))?; Ok(()) } diff --git a/src/conversions/anyhow.rs b/src/conversions/anyhow.rs index 623ee7d548c..d6880ac4e96 100644 --- a/src/conversions/anyhow.rs +++ b/src/conversions/anyhow.rs @@ -47,7 +47,7 @@ //! //! fn main() { //! let error = Python::with_gil(|py| -> PyResult> { -//! let fun = wrap_pyfunction_bound!(py_open, py)?; +//! let fun = wrap_pyfunction!(py_open, py)?; //! let text = fun.call1(("foo.txt",))?.extract::>()?; //! Ok(text) //! }).unwrap_err(); diff --git a/src/conversions/eyre.rs b/src/conversions/eyre.rs index d4704e411c5..2d8b623fa58 100644 --- a/src/conversions/eyre.rs +++ b/src/conversions/eyre.rs @@ -46,7 +46,7 @@ //! //! fn main() { //! let error = Python::with_gil(|py| -> PyResult> { -//! let fun = wrap_pyfunction_bound!(py_open, py)?; +//! let fun = wrap_pyfunction!(py_open, py)?; //! let text = fun.call1(("foo.txt",))?.extract::>()?; //! Ok(text) //! }).unwrap_err(); diff --git a/src/coroutine/waker.rs b/src/coroutine/waker.rs index fc7c54e1f5a..bb93974aa1e 100644 --- a/src/coroutine/waker.rs +++ b/src/coroutine/waker.rs @@ -1,7 +1,7 @@ use crate::sync::GILOnceCell; use crate::types::any::PyAnyMethods; use crate::types::PyCFunction; -use crate::{intern, wrap_pyfunction_bound, Bound, Py, PyAny, PyObject, PyResult, Python}; +use crate::{intern, wrap_pyfunction, Bound, Py, PyAny, PyObject, PyResult, Python}; use pyo3_macros::pyfunction; use std::sync::Arc; use std::task::Wake; @@ -71,7 +71,7 @@ impl LoopAndFuture { fn set_result(&self, py: Python<'_>) -> PyResult<()> { static RELEASE_WAITER: GILOnceCell> = GILOnceCell::new(); let release_waiter = RELEASE_WAITER.get_or_try_init(py, || { - wrap_pyfunction_bound!(release_waiter, py).map(Bound::unbind) + wrap_pyfunction!(release_waiter, py).map(Bound::unbind) })?; // `Future.set_result` must be called in event loop thread, // so it requires `call_soon_threadsafe` diff --git a/src/derive_utils.rs b/src/derive_utils.rs deleted file mode 100644 index a47f489ceb8..00000000000 --- a/src/derive_utils.rs +++ /dev/null @@ -1,33 +0,0 @@ -//! Functionality for the code generated by the derive backend - -use crate::{types::PyModule, Python}; - -/// Enum to abstract over the arguments of Python function wrappers. -pub enum PyFunctionArguments<'a> { - Python(Python<'a>), - PyModule(&'a PyModule), -} - -impl<'a> PyFunctionArguments<'a> { - pub fn into_py_and_maybe_module(self) -> (Python<'a>, Option<&'a PyModule>) { - match self { - PyFunctionArguments::Python(py) => (py, None), - PyFunctionArguments::PyModule(module) => { - let py = crate::PyNativeType::py(module); - (py, Some(module)) - } - } - } -} - -impl<'a> From> for PyFunctionArguments<'a> { - fn from(py: Python<'a>) -> PyFunctionArguments<'a> { - PyFunctionArguments::Python(py) - } -} - -impl<'a> From<&'a PyModule> for PyFunctionArguments<'a> { - fn from(module: &'a PyModule) -> PyFunctionArguments<'a> { - PyFunctionArguments::PyModule(module) - } -} diff --git a/src/err/mod.rs b/src/err/mod.rs index b8ede9c3e3e..b51b9defc38 100644 --- a/src/err/mod.rs +++ b/src/err/mod.rs @@ -127,7 +127,7 @@ impl PyErr { /// } /// # /// # Python::with_gil(|py| { - /// # let fun = pyo3::wrap_pyfunction_bound!(always_throws, py).unwrap(); + /// # let fun = pyo3::wrap_pyfunction!(always_throws, py).unwrap(); /// # let err = fun.call0().expect_err("called a function that should always return an error but the return value was Ok"); /// # assert!(err.is_instance_of::(py)) /// # }); @@ -145,7 +145,7 @@ impl PyErr { /// } /// # /// # Python::with_gil(|py| { - /// # let fun = pyo3::wrap_pyfunction_bound!(always_throws, py).unwrap(); + /// # let fun = pyo3::wrap_pyfunction!(always_throws, py).unwrap(); /// # let err = fun.call0().expect_err("called a function that should always return an error but the return value was Ok"); /// # assert!(err.is_instance_of::(py)) /// # }); diff --git a/src/exceptions.rs b/src/exceptions.rs index d5260729c1c..bca706c43d4 100644 --- a/src/exceptions.rs +++ b/src/exceptions.rs @@ -181,7 +181,7 @@ macro_rules! import_exception_bound { /// } /// # fn main() -> PyResult<()> { /// # Python::with_gil(|py| -> PyResult<()> { -/// # let fun = wrap_pyfunction_bound!(raise_myerror, py)?; +/// # let fun = wrap_pyfunction!(raise_myerror, py)?; /// # let locals = pyo3::types::PyDict::new_bound(py); /// # locals.set_item("MyError", py.get_type_bound::())?; /// # locals.set_item("raise_myerror", fun)?; @@ -332,7 +332,7 @@ fn always_throws() -> PyResult<()> { } # # Python::with_gil(|py| { -# let fun = pyo3::wrap_pyfunction_bound!(always_throws, py).unwrap(); +# let fun = pyo3::wrap_pyfunction!(always_throws, py).unwrap(); # let err = fun.call0().expect_err(\"called a function that should always return an error but the return value was Ok\"); # assert!(err.is_instance_of::(py)) # }); diff --git a/src/impl_/extract_argument.rs b/src/impl_/extract_argument.rs index a354e578c5f..ef62ef26e7b 100644 --- a/src/impl_/extract_argument.rs +++ b/src/impl_/extract_argument.rs @@ -171,9 +171,9 @@ where pub fn from_py_with<'a, 'py, T>( obj: &'a Bound<'py, PyAny>, arg_name: &str, - extractor: impl Into>, + extractor: fn(&'a Bound<'py, PyAny>) -> PyResult, ) -> PyResult { - match extractor.into().call(obj) { + match extractor(obj) { Ok(value) => Ok(value), Err(e) => Err(argument_extraction_error(obj.py(), arg_name, e)), } @@ -184,7 +184,7 @@ pub fn from_py_with<'a, 'py, T>( pub fn from_py_with_with_default<'a, 'py, T>( obj: Option<&'a Bound<'py, PyAny>>, arg_name: &str, - extractor: impl Into>, + extractor: fn(&'a Bound<'py, PyAny>) -> PyResult, default: fn() -> T, ) -> PyResult { match obj { diff --git a/src/impl_/frompyobject.rs b/src/impl_/frompyobject.rs index 1e46efaeae3..9f3f77340bc 100644 --- a/src/impl_/frompyobject.rs +++ b/src/impl_/frompyobject.rs @@ -2,35 +2,6 @@ use crate::types::any::PyAnyMethods; use crate::Bound; use crate::{exceptions::PyTypeError, FromPyObject, PyAny, PyErr, PyResult, Python}; -pub enum Extractor<'a, 'py, T> { - Bound(fn(&'a Bound<'py, PyAny>) -> PyResult), - #[cfg(feature = "gil-refs")] - GilRef(fn(&'a PyAny) -> PyResult), -} - -impl<'a, 'py, T> From) -> PyResult> for Extractor<'a, 'py, T> { - fn from(value: fn(&'a Bound<'py, PyAny>) -> PyResult) -> Self { - Self::Bound(value) - } -} - -#[cfg(feature = "gil-refs")] -impl<'a, T> From PyResult> for Extractor<'a, '_, T> { - fn from(value: fn(&'a PyAny) -> PyResult) -> Self { - Self::GilRef(value) - } -} - -impl<'a, 'py, T> Extractor<'a, 'py, T> { - pub(crate) fn call(self, obj: &'a Bound<'py, PyAny>) -> PyResult { - match self { - Extractor::Bound(f) => f(obj), - #[cfg(feature = "gil-refs")] - Extractor::GilRef(f) => f(obj.as_gil_ref()), - } - } -} - #[cold] pub fn failed_to_extract_enum( py: Python<'_>, @@ -91,12 +62,12 @@ where } pub fn extract_struct_field_with<'a, 'py, T>( - extractor: impl Into>, + extractor: fn(&'a Bound<'py, PyAny>) -> PyResult, obj: &'a Bound<'py, PyAny>, struct_name: &str, field_name: &str, ) -> PyResult { - match extractor.into().call(obj) { + match extractor(obj) { Ok(value) => Ok(value), Err(err) => Err(failed_to_extract_struct_field( obj.py(), @@ -142,12 +113,12 @@ where } pub fn extract_tuple_struct_field_with<'a, 'py, T>( - extractor: impl Into>, + extractor: fn(&'a Bound<'py, PyAny>) -> PyResult, obj: &'a Bound<'py, PyAny>, struct_name: &str, index: usize, ) -> PyResult { - match extractor.into().call(obj) { + match extractor(obj) { Ok(value) => Ok(value), Err(err) => Err(failed_to_extract_tuple_struct_field( obj.py(), diff --git a/src/impl_/pyfunction.rs b/src/impl_/pyfunction.rs index 0be5174487f..c389165488c 100644 --- a/src/impl_/pyfunction.rs +++ b/src/impl_/pyfunction.rs @@ -35,46 +35,8 @@ impl<'py> WrapPyFunctionArg<'py, Bound<'py, PyCFunction>> for &'_ Borrowed<'_, ' } } -// For Python<'py>, only the GIL Ref form exists to avoid causing type inference to kick in. -// The `wrap_pyfunction_bound!` macro is needed for the Bound form. -#[cfg(feature = "gil-refs")] -impl<'py> WrapPyFunctionArg<'py, &'py PyCFunction> for Python<'py> { - fn wrap_pyfunction(self, method_def: &PyMethodDef) -> PyResult<&'py PyCFunction> { - PyCFunction::internal_new(self, method_def, None).map(Bound::into_gil_ref) - } -} - -#[cfg(not(feature = "gil-refs"))] impl<'py> WrapPyFunctionArg<'py, Bound<'py, PyCFunction>> for Python<'py> { fn wrap_pyfunction(self, method_def: &PyMethodDef) -> PyResult> { PyCFunction::internal_new(self, method_def, None) } } - -#[cfg(feature = "gil-refs")] -impl<'py> WrapPyFunctionArg<'py, &'py PyCFunction> for &'py PyModule { - fn wrap_pyfunction(self, method_def: &PyMethodDef) -> PyResult<&'py PyCFunction> { - use crate::PyNativeType; - PyCFunction::internal_new(self.py(), method_def, Some(&self.as_borrowed())) - .map(Bound::into_gil_ref) - } -} - -/// Helper for `wrap_pyfunction_bound!` to guarantee return type of `Bound<'py, PyCFunction>`. -pub struct OnlyBound(pub T); - -impl<'py, T> WrapPyFunctionArg<'py, Bound<'py, PyCFunction>> for OnlyBound -where - T: WrapPyFunctionArg<'py, Bound<'py, PyCFunction>>, -{ - fn wrap_pyfunction(self, method_def: &PyMethodDef) -> PyResult> { - WrapPyFunctionArg::wrap_pyfunction(self.0, method_def) - } -} - -#[cfg(feature = "gil-refs")] -impl<'py> WrapPyFunctionArg<'py, Bound<'py, PyCFunction>> for OnlyBound> { - fn wrap_pyfunction(self, method_def: &PyMethodDef) -> PyResult> { - PyCFunction::internal_new(self.0, method_def, None) - } -} diff --git a/src/lib.rs b/src/lib.rs index 41525b16fa8..f0854d547b8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -416,10 +416,6 @@ pub mod conversion; mod conversions; #[cfg(feature = "experimental-async")] pub mod coroutine; -#[macro_use] -#[doc(hidden)] -#[cfg(feature = "gil-refs")] -pub mod derive_utils; mod err; pub mod exceptions; pub mod ffi; diff --git a/src/macros.rs b/src/macros.rs index 43fbef12b89..4de52185eda 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -124,14 +124,6 @@ macro_rules! py_run_impl { /// This can be used with [`PyModule::add_function`](crate::types::PyModuleMethods::add_function) to /// add free functions to a [`PyModule`](crate::types::PyModule) - see its documentation for more /// information. -/// -/// During the migration from the GIL Ref API to the Bound API, the return type of this macro will -/// be either the `&'py PyModule` GIL Ref or `Bound<'py, PyModule>` according to the second -/// argument. -/// -/// For backwards compatibility, if the second argument is `Python<'py>` then the return type will -/// be `&'py PyModule` GIL Ref. To get `Bound<'py, PyModule>`, use the [`crate::wrap_pyfunction_bound!`] -/// macro instead. #[macro_export] macro_rules! wrap_pyfunction { ($function:path) => { @@ -157,24 +149,15 @@ macro_rules! wrap_pyfunction { /// This can be used with [`PyModule::add_function`](crate::types::PyModuleMethods::add_function) to /// add free functions to a [`PyModule`](crate::types::PyModule) - see its documentation for more /// information. +#[deprecated(since = "0.23.0", note = "renamed to `wrap_pyfunction!`")] #[macro_export] macro_rules! wrap_pyfunction_bound { ($function:path) => { - &|py_or_module| { - use $function as wrapped_pyfunction; - $crate::impl_::pyfunction::WrapPyFunctionArg::wrap_pyfunction( - $crate::impl_::pyfunction::OnlyBound(py_or_module), - &wrapped_pyfunction::_PYO3_DEF, - ) - } + $crate::wrap_pyfunction!($function) + }; + ($function:path, $py_or_module:expr) => { + $crate::wrap_pyfunction!($function, $py_or_module) }; - ($function:path, $py_or_module:expr) => {{ - use $function as wrapped_pyfunction; - $crate::impl_::pyfunction::WrapPyFunctionArg::wrap_pyfunction( - $crate::impl_::pyfunction::OnlyBound($py_or_module), - &wrapped_pyfunction::_PYO3_DEF, - ) - }}; } /// Returns a function that takes a [`Python`](crate::Python) instance and returns a diff --git a/src/marker.rs b/src/marker.rs index 689f58db311..58843cf29aa 100644 --- a/src/marker.rs +++ b/src/marker.rs @@ -480,7 +480,7 @@ impl<'py> Python<'py> { /// # /// # fn main() -> PyResult<()> { /// # Python::with_gil(|py| -> PyResult<()> { - /// # let fun = pyo3::wrap_pyfunction_bound!(sum_numbers, py)?; + /// # let fun = pyo3::wrap_pyfunction!(sum_numbers, py)?; /// # let res = fun.call1((vec![1_u32, 2, 3],))?; /// # assert_eq!(res.extract::()?, 6_u32); /// # Ok(()) diff --git a/src/prelude.rs b/src/prelude.rs index 3f45cb52cf0..eac04d0e048 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -25,7 +25,10 @@ pub use crate::PyNativeType; pub use pyo3_macros::{pyclass, pyfunction, pymethods, pymodule, FromPyObject}; #[cfg(feature = "macros")] -pub use crate::{wrap_pyfunction, wrap_pyfunction_bound}; +pub use crate::wrap_pyfunction; +#[cfg(feature = "macros")] +#[allow(deprecated)] +pub use crate::wrap_pyfunction_bound; pub use crate::types::any::PyAnyMethods; pub use crate::types::boolobject::PyBoolMethods; diff --git a/src/pycell.rs b/src/pycell.rs index 77d174cb9e1..7dadb8361d5 100644 --- a/src/pycell.rs +++ b/src/pycell.rs @@ -132,7 +132,7 @@ //! # let n = Py::new(py, Number{inner: 35}).unwrap(); //! # let n2 = n.clone_ref(py); //! # assert!(n.is(&n2)); -//! # let fun = pyo3::wrap_pyfunction_bound!(swap_numbers, py).unwrap(); +//! # let fun = pyo3::wrap_pyfunction!(swap_numbers, py).unwrap(); //! # fun.call1((n, n2)).expect_err("Managed to create overlapping mutable references. Note: this is undefined behaviour."); //! # }); //! # } @@ -170,7 +170,7 @@ //! # let n = Py::new(py, Number{inner: 35}).unwrap(); //! # let n2 = n.clone_ref(py); //! # assert!(n.is(&n2)); -//! # let fun = pyo3::wrap_pyfunction_bound!(swap_numbers, py).unwrap(); +//! # let fun = pyo3::wrap_pyfunction!(swap_numbers, py).unwrap(); //! # fun.call1((n, n2)).unwrap(); //! # }); //! # @@ -179,7 +179,7 @@ //! # let n = Py::new(py, Number{inner: 35}).unwrap(); //! # let n2 = Py::new(py, Number{inner: 42}).unwrap(); //! # assert!(!n.is(&n2)); -//! # let fun = pyo3::wrap_pyfunction_bound!(swap_numbers, py).unwrap(); +//! # let fun = pyo3::wrap_pyfunction!(swap_numbers, py).unwrap(); //! # fun.call1((&n, &n2)).unwrap(); //! # let n: u32 = n.borrow(py).inner; //! # let n2: u32 = n2.borrow(py).inner; diff --git a/src/sync.rs b/src/sync.rs index 390011fdd5b..dee39fdfd83 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -248,10 +248,10 @@ impl GILOnceCell> { /// } /// # /// # Python::with_gil(|py| { -/// # let fun_slow = wrap_pyfunction_bound!(create_dict, py).unwrap(); +/// # let fun_slow = wrap_pyfunction!(create_dict, py).unwrap(); /// # let dict = fun_slow.call0().unwrap(); /// # assert!(dict.contains("foo").unwrap()); -/// # let fun = wrap_pyfunction_bound!(create_dict_faster, py).unwrap(); +/// # let fun = wrap_pyfunction!(create_dict_faster, py).unwrap(); /// # let dict = fun.call0().unwrap(); /// # assert!(dict.contains("foo").unwrap()); /// # }); diff --git a/src/tests/hygiene/pymodule.rs b/src/tests/hygiene/pymodule.rs index 5a45ab189ee..576cc75e0d2 100644 --- a/src/tests/hygiene/pymodule.rs +++ b/src/tests/hygiene/pymodule.rs @@ -18,7 +18,7 @@ fn foo( fn my_module(m: &crate::Bound<'_, crate::types::PyModule>) -> crate::PyResult<()> { as crate::types::PyModuleMethods>::add_function( m, - crate::wrap_pyfunction_bound!(do_something, m)?, + crate::wrap_pyfunction!(do_something, m)?, )?; as crate::types::PyModuleMethods>::add_wrapped( m, diff --git a/src/types/bytearray.rs b/src/types/bytearray.rs index 9885f637cec..c56c312acbd 100644 --- a/src/types/bytearray.rs +++ b/src/types/bytearray.rs @@ -160,7 +160,7 @@ pub trait PyByteArrayMethods<'py>: crate::sealed::Sealed { /// } /// # fn main() -> PyResult<()> { /// # Python::with_gil(|py| -> PyResult<()> { - /// # let fun = wrap_pyfunction_bound!(a_valid_function, py)?; + /// # let fun = wrap_pyfunction!(a_valid_function, py)?; /// # let locals = pyo3::types::PyDict::new_bound(py); /// # locals.set_item("a_valid_function", fun)?; /// # diff --git a/src/types/iterator.rs b/src/types/iterator.rs index e89cb864cd6..dccea94785f 100644 --- a/src/types/iterator.rs +++ b/src/types/iterator.rs @@ -295,7 +295,7 @@ def fibonacci(target): // Regression test for 2913 Python::with_gil(|py| { - let assert_iterator = crate::wrap_pyfunction_bound!(assert_iterator, py).unwrap(); + let assert_iterator = crate::wrap_pyfunction!(assert_iterator, py).unwrap(); crate::py_run!( py, assert_iterator, diff --git a/src/types/weakref/proxy.rs b/src/types/weakref/proxy.rs index 495cb1a5705..09054defe6f 100644 --- a/src/types/weakref/proxy.rs +++ b/src/types/weakref/proxy.rs @@ -127,7 +127,7 @@ impl PyWeakrefProxy { /// ); /// assert_eq!(py.eval_bound("counter", None, None)?.extract::()?, 0); /// - /// let weakref2 = PyWeakrefProxy::new_bound_with(&foo, wrap_pyfunction_bound!(callback, py)?)?; + /// let weakref2 = PyWeakrefProxy::new_bound_with(&foo, wrap_pyfunction!(callback, py)?)?; /// assert!(!weakref.is(&weakref2)); // Not the same weakref /// assert!(weakref.eq(&weakref2)?); // But Equal, since they point to the same object /// diff --git a/src/types/weakref/reference.rs b/src/types/weakref/reference.rs index a2da67149da..383754e33fb 100644 --- a/src/types/weakref/reference.rs +++ b/src/types/weakref/reference.rs @@ -134,7 +134,7 @@ impl PyWeakrefReference { /// ); /// assert_eq!(py.eval_bound("counter", None, None)?.extract::()?, 0); /// - /// let weakref2 = PyWeakrefReference::new_bound_with(&foo, wrap_pyfunction_bound!(callback, py)?)?; + /// let weakref2 = PyWeakrefReference::new_bound_with(&foo, wrap_pyfunction!(callback, py)?)?; /// assert!(!weakref.is(&weakref2)); // Not the same weakref /// assert!(weakref.eq(&weakref2)?); // But Equal, since they point to the same object /// diff --git a/tests/test_anyhow.rs b/tests/test_anyhow.rs index d6f5c036b74..3bd76bd637f 100644 --- a/tests/test_anyhow.rs +++ b/tests/test_anyhow.rs @@ -1,6 +1,6 @@ #![cfg(feature = "anyhow")] -use pyo3::wrap_pyfunction_bound; +use pyo3::wrap_pyfunction; #[test] fn test_anyhow_py_function_ok_result() { @@ -13,7 +13,7 @@ fn test_anyhow_py_function_ok_result() { } Python::with_gil(|py| { - let func = wrap_pyfunction_bound!(produce_ok_result)(py).unwrap(); + let func = wrap_pyfunction!(produce_ok_result)(py).unwrap(); py_run!( py, @@ -36,7 +36,7 @@ fn test_anyhow_py_function_err_result() { } Python::with_gil(|py| { - let func = wrap_pyfunction_bound!(produce_err_result)(py).unwrap(); + let func = wrap_pyfunction!(produce_err_result)(py).unwrap(); let locals = PyDict::new_bound(py); locals.set_item("func", func).unwrap(); diff --git a/tests/test_bytes.rs b/tests/test_bytes.rs index 5adca3f154a..26686a2def3 100644 --- a/tests/test_bytes.rs +++ b/tests/test_bytes.rs @@ -14,7 +14,7 @@ fn bytes_pybytes_conversion(bytes: &[u8]) -> &[u8] { #[test] fn test_pybytes_bytes_conversion() { Python::with_gil(|py| { - let f = wrap_pyfunction_bound!(bytes_pybytes_conversion)(py).unwrap(); + let f = wrap_pyfunction!(bytes_pybytes_conversion)(py).unwrap(); py_assert!(py, f, "f(b'Hello World') == b'Hello World'"); }); } @@ -27,7 +27,7 @@ fn bytes_vec_conversion(py: Python<'_>, bytes: Vec) -> Bound<'_, PyBytes> { #[test] fn test_pybytes_vec_conversion() { Python::with_gil(|py| { - let f = wrap_pyfunction_bound!(bytes_vec_conversion)(py).unwrap(); + let f = wrap_pyfunction!(bytes_vec_conversion)(py).unwrap(); py_assert!(py, f, "f(b'Hello World') == b'Hello World'"); }); } @@ -35,7 +35,7 @@ fn test_pybytes_vec_conversion() { #[test] fn test_bytearray_vec_conversion() { Python::with_gil(|py| { - let f = wrap_pyfunction_bound!(bytes_vec_conversion)(py).unwrap(); + let f = wrap_pyfunction!(bytes_vec_conversion)(py).unwrap(); py_assert!(py, f, "f(bytearray(b'Hello World')) == b'Hello World'"); }); } diff --git a/tests/test_coroutine.rs b/tests/test_coroutine.rs index 75b524edf78..887251a5084 100644 --- a/tests/test_coroutine.rs +++ b/tests/test_coroutine.rs @@ -33,7 +33,7 @@ fn noop_coroutine() { 42 } Python::with_gil(|gil| { - let noop = wrap_pyfunction_bound!(noop, gil).unwrap(); + let noop = wrap_pyfunction!(noop, gil).unwrap(); let test = "import asyncio; assert asyncio.run(noop()) == 42"; py_run!(gil, noop, &handle_windows(test)); }) @@ -71,10 +71,7 @@ fn test_coroutine_qualname() { let locals = [ ( "my_fn", - wrap_pyfunction_bound!(my_fn, gil) - .unwrap() - .as_borrowed() - .as_any(), + wrap_pyfunction!(my_fn, gil).unwrap().as_borrowed().as_any(), ), ("MyClass", gil.get_type_bound::().as_any()), ] @@ -99,7 +96,7 @@ fn sleep_0_like_coroutine() { .await } Python::with_gil(|gil| { - let sleep_0 = wrap_pyfunction_bound!(sleep_0, gil).unwrap(); + let sleep_0 = wrap_pyfunction!(sleep_0, gil).unwrap(); let test = "import asyncio; assert asyncio.run(sleep_0()) == 42"; py_run!(gil, sleep_0, &handle_windows(test)); }) @@ -118,7 +115,7 @@ async fn sleep(seconds: f64) -> usize { #[test] fn sleep_coroutine() { Python::with_gil(|gil| { - let sleep = wrap_pyfunction_bound!(sleep, gil).unwrap(); + let sleep = wrap_pyfunction!(sleep, gil).unwrap(); let test = r#"import asyncio; assert asyncio.run(sleep(0.1)) == 42"#; py_run!(gil, sleep, &handle_windows(test)); }) @@ -127,7 +124,7 @@ fn sleep_coroutine() { #[test] fn cancelled_coroutine() { Python::with_gil(|gil| { - let sleep = wrap_pyfunction_bound!(sleep, gil).unwrap(); + let sleep = wrap_pyfunction!(sleep, gil).unwrap(); let test = r#" import asyncio async def main(): @@ -166,7 +163,7 @@ fn coroutine_cancel_handle() { } } Python::with_gil(|gil| { - let cancellable_sleep = wrap_pyfunction_bound!(cancellable_sleep, gil).unwrap(); + let cancellable_sleep = wrap_pyfunction!(cancellable_sleep, gil).unwrap(); let test = r#" import asyncio; async def main(): @@ -198,7 +195,7 @@ fn coroutine_is_cancelled() { } } Python::with_gil(|gil| { - let sleep_loop = wrap_pyfunction_bound!(sleep_loop, gil).unwrap(); + let sleep_loop = wrap_pyfunction!(sleep_loop, gil).unwrap(); let test = r#" import asyncio; async def main(): @@ -226,7 +223,7 @@ fn coroutine_panic() { panic!("test panic"); } Python::with_gil(|gil| { - let panic = wrap_pyfunction_bound!(panic, gil).unwrap(); + let panic = wrap_pyfunction!(panic, gil).unwrap(); let test = r#" import asyncio coro = panic() diff --git a/tests/test_enum.rs b/tests/test_enum.rs index 96a07c3fe41..5b8d45a9e1d 100644 --- a/tests/test_enum.rs +++ b/tests/test_enum.rs @@ -30,7 +30,7 @@ fn return_enum() -> MyEnum { #[test] fn test_return_enum() { Python::with_gil(|py| { - let f = wrap_pyfunction_bound!(return_enum)(py).unwrap(); + let f = wrap_pyfunction!(return_enum)(py).unwrap(); let mynum = py.get_type_bound::(); py_run!(py, f mynum, "assert f() == mynum.Variant") @@ -45,7 +45,7 @@ fn enum_arg(e: MyEnum) { #[test] fn test_enum_arg() { Python::with_gil(|py| { - let f = wrap_pyfunction_bound!(enum_arg)(py).unwrap(); + let f = wrap_pyfunction!(enum_arg)(py).unwrap(); let mynum = py.get_type_bound::(); py_run!(py, f mynum, "f(mynum.OtherVariant)") diff --git a/tests/test_exceptions.rs b/tests/test_exceptions.rs index e85355fd40e..4cf866f9a0b 100644 --- a/tests/test_exceptions.rs +++ b/tests/test_exceptions.rs @@ -22,7 +22,7 @@ fn fail_to_open_file() -> PyResult<()> { #[cfg(not(target_os = "windows"))] fn test_filenotfounderror() { Python::with_gil(|py| { - let fail_to_open_file = wrap_pyfunction_bound!(fail_to_open_file)(py).unwrap(); + let fail_to_open_file = wrap_pyfunction!(fail_to_open_file)(py).unwrap(); py_run!( py, @@ -68,7 +68,7 @@ fn call_fail_with_custom_error() -> PyResult<()> { fn test_custom_error() { Python::with_gil(|py| { let call_fail_with_custom_error = - wrap_pyfunction_bound!(call_fail_with_custom_error)(py).unwrap(); + wrap_pyfunction!(call_fail_with_custom_error)(py).unwrap(); py_run!( py, diff --git a/tests/test_macros.rs b/tests/test_macros.rs index 6a50e5b36e4..0d2b125b870 100644 --- a/tests/test_macros.rs +++ b/tests/test_macros.rs @@ -76,7 +76,7 @@ fn test_macro_rules_interactions() { let my_base = py.get_type_bound::(); py_assert!(py, my_base, "my_base.__name__ == 'MyClass'"); - let my_func = wrap_pyfunction_bound!(my_function_in_macro, py).unwrap(); + let my_func = wrap_pyfunction!(my_function_in_macro, py).unwrap(); py_assert!( py, my_func, diff --git a/tests/test_methods.rs b/tests/test_methods.rs index 40893826f39..6888a9b0b14 100644 --- a/tests/test_methods.rs +++ b/tests/test_methods.rs @@ -1135,7 +1135,7 @@ fn test_option_pyclass_arg() { } Python::with_gil(|py| { - let f = wrap_pyfunction_bound!(option_class_arg, py).unwrap(); + let f = wrap_pyfunction!(option_class_arg, py).unwrap(); assert!(f.call0().unwrap().is_none()); let obj = Py::new(py, SomePyClass {}).unwrap(); assert!(f diff --git a/tests/test_pyfunction.rs b/tests/test_pyfunction.rs index f3a04a53a95..903db689527 100644 --- a/tests/test_pyfunction.rs +++ b/tests/test_pyfunction.rs @@ -21,7 +21,7 @@ fn struct_function() {} #[test] fn test_rust_keyword_name() { Python::with_gil(|py| { - let f = wrap_pyfunction_bound!(struct_function)(py).unwrap(); + let f = wrap_pyfunction!(struct_function)(py).unwrap(); py_assert!(py, f, "f.__name__ == 'struct'"); }); @@ -36,7 +36,7 @@ fn optional_bool(arg: Option) -> String { fn test_optional_bool() { // Regression test for issue #932 Python::with_gil(|py| { - let f = wrap_pyfunction_bound!(optional_bool)(py).unwrap(); + let f = wrap_pyfunction!(optional_bool)(py).unwrap(); py_assert!(py, f, "f() == 'Some(true)'"); py_assert!(py, f, "f(True) == 'Some(true)'"); @@ -60,7 +60,7 @@ fn buffer_inplace_add(py: Python<'_>, x: PyBuffer, y: PyBuffer) { #[test] fn test_buffer_add() { Python::with_gil(|py| { - let f = wrap_pyfunction_bound!(buffer_inplace_add)(py).unwrap(); + let f = wrap_pyfunction!(buffer_inplace_add)(py).unwrap(); py_expect_exception!( py, @@ -104,8 +104,8 @@ fn function_with_pycfunction_arg<'py>( #[test] fn test_functions_with_function_args() { Python::with_gil(|py| { - let py_cfunc_arg = wrap_pyfunction_bound!(function_with_pycfunction_arg)(py).unwrap(); - let bool_to_string = wrap_pyfunction_bound!(optional_bool)(py).unwrap(); + let py_cfunc_arg = wrap_pyfunction!(function_with_pycfunction_arg)(py).unwrap(); + let bool_to_string = wrap_pyfunction!(optional_bool)(py).unwrap(); pyo3::py_run!( py, @@ -118,7 +118,7 @@ fn test_functions_with_function_args() { #[cfg(not(any(Py_LIMITED_API, PyPy)))] { - let py_func_arg = wrap_pyfunction_bound!(function_with_pyfunction_arg)(py).unwrap(); + let py_func_arg = wrap_pyfunction!(function_with_pyfunction_arg)(py).unwrap(); pyo3::py_run!( py, @@ -152,7 +152,7 @@ fn function_with_custom_conversion( #[test] fn test_function_with_custom_conversion() { Python::with_gil(|py| { - let custom_conv_func = wrap_pyfunction_bound!(function_with_custom_conversion)(py).unwrap(); + let custom_conv_func = wrap_pyfunction!(function_with_custom_conversion)(py).unwrap(); pyo3::py_run!( py, @@ -171,7 +171,7 @@ fn test_function_with_custom_conversion() { #[test] fn test_function_with_custom_conversion_error() { Python::with_gil(|py| { - let custom_conv_func = wrap_pyfunction_bound!(function_with_custom_conversion)(py).unwrap(); + let custom_conv_func = wrap_pyfunction!(function_with_custom_conversion)(py).unwrap(); py_expect_exception!( py, @@ -208,13 +208,13 @@ fn test_from_py_with_defaults() { } Python::with_gil(|py| { - let f = wrap_pyfunction_bound!(from_py_with_option)(py).unwrap(); + let f = wrap_pyfunction!(from_py_with_option)(py).unwrap(); assert_eq!(f.call0().unwrap().extract::().unwrap(), 0); assert_eq!(f.call1((123,)).unwrap().extract::().unwrap(), 123); assert_eq!(f.call1((999,)).unwrap().extract::().unwrap(), 999); - let f2 = wrap_pyfunction_bound!(from_py_with_default)(py).unwrap(); + let f2 = wrap_pyfunction!(from_py_with_default)(py).unwrap(); assert_eq!(f2.call0().unwrap().extract::().unwrap(), 0); assert_eq!(f2.call1(("123",)).unwrap().extract::().unwrap(), 3); @@ -247,7 +247,7 @@ fn conversion_error( #[test] fn test_conversion_error() { Python::with_gil(|py| { - let conversion_error = wrap_pyfunction_bound!(conversion_error)(py).unwrap(); + let conversion_error = wrap_pyfunction!(conversion_error)(py).unwrap(); py_expect_exception!( py, conversion_error, @@ -497,12 +497,12 @@ fn use_pyfunction() { use function_in_module::foo; // check imported name can be wrapped - let f = wrap_pyfunction_bound!(foo, py).unwrap(); + let f = wrap_pyfunction!(foo, py).unwrap(); assert_eq!(f.call1((5,)).unwrap().extract::().unwrap(), 5); assert_eq!(f.call1((42,)).unwrap().extract::().unwrap(), 42); // check path import can be wrapped - let f2 = wrap_pyfunction_bound!(function_in_module::foo, py).unwrap(); + let f2 = wrap_pyfunction!(function_in_module::foo, py).unwrap(); assert_eq!(f2.call1((5,)).unwrap().extract::().unwrap(), 5); assert_eq!(f2.call1((42,)).unwrap().extract::().unwrap(), 42); }) @@ -530,7 +530,7 @@ fn return_value_borrows_from_arguments<'py>( #[test] fn test_return_value_borrows_from_arguments() { Python::with_gil(|py| { - let function = wrap_pyfunction_bound!(return_value_borrows_from_arguments, py).unwrap(); + let function = wrap_pyfunction!(return_value_borrows_from_arguments, py).unwrap(); let key = Py::new(py, Key("key".to_owned())).unwrap(); let value = Py::new(py, Value(42)).unwrap(); @@ -554,7 +554,7 @@ fn test_some_wrap_arguments() { } Python::with_gil(|py| { - let function = wrap_pyfunction_bound!(some_wrap_arguments, py).unwrap(); + let function = wrap_pyfunction!(some_wrap_arguments, py).unwrap(); py_assert!(py, function, "function() == [1, 2, None, None]"); }) } @@ -571,7 +571,7 @@ fn test_reference_to_bound_arguments() { } Python::with_gil(|py| { - let function = wrap_pyfunction_bound!(reference_args, py).unwrap(); + let function = wrap_pyfunction!(reference_args, py).unwrap(); py_assert!(py, function, "function(1) == 1"); py_assert!(py, function, "function(1, 2) == 3"); }) diff --git a/tests/test_string.rs b/tests/test_string.rs index d90c5a81b83..02bf2ecd4df 100644 --- a/tests/test_string.rs +++ b/tests/test_string.rs @@ -11,7 +11,7 @@ fn take_str(_s: &str) {} #[test] fn test_unicode_encode_error() { Python::with_gil(|py| { - let take_str = wrap_pyfunction_bound!(take_str)(py).unwrap(); + let take_str = wrap_pyfunction!(take_str)(py).unwrap(); py_expect_exception!( py, take_str, diff --git a/tests/test_text_signature.rs b/tests/test_text_signature.rs index 3899878bd56..d9fcb83d3bd 100644 --- a/tests/test_text_signature.rs +++ b/tests/test_text_signature.rs @@ -101,7 +101,7 @@ fn test_function() { } Python::with_gil(|py| { - let f = wrap_pyfunction_bound!(my_function)(py).unwrap(); + let f = wrap_pyfunction!(my_function)(py).unwrap(); py_assert!(py, f, "f.__text_signature__ == '(a, b=None, *, c=42)'"); }); @@ -148,42 +148,42 @@ fn test_auto_test_signature_function() { } Python::with_gil(|py| { - let f = wrap_pyfunction_bound!(my_function)(py).unwrap(); + let f = wrap_pyfunction!(my_function)(py).unwrap(); py_assert!( py, f, "f.__text_signature__ == '(a, b, c)', f.__text_signature__" ); - let f = wrap_pyfunction_bound!(my_function_2)(py).unwrap(); + let f = wrap_pyfunction!(my_function_2)(py).unwrap(); py_assert!( py, f, "f.__text_signature__ == '($module, a, b, c)', f.__text_signature__" ); - let f = wrap_pyfunction_bound!(my_function_3)(py).unwrap(); + let f = wrap_pyfunction!(my_function_3)(py).unwrap(); py_assert!( py, f, "f.__text_signature__ == '(a, /, b=None, *, c=5)', f.__text_signature__" ); - let f = wrap_pyfunction_bound!(my_function_4)(py).unwrap(); + let f = wrap_pyfunction!(my_function_4)(py).unwrap(); py_assert!( py, f, "f.__text_signature__ == '(a, /, b=None, *args, c, d=5, **kwargs)', f.__text_signature__" ); - let f = wrap_pyfunction_bound!(my_function_5)(py).unwrap(); + let f = wrap_pyfunction!(my_function_5)(py).unwrap(); py_assert!( py, f, "f.__text_signature__ == '(a=1, /, b=None, c=1.5, d=5, e=\"pyo3\", f=\\'f\\', h=True)', f.__text_signature__" ); - let f = wrap_pyfunction_bound!(my_function_6)(py).unwrap(); + let f = wrap_pyfunction!(my_function_6)(py).unwrap(); py_assert!( py, f, @@ -318,10 +318,10 @@ fn test_auto_test_signature_opt_out() { } Python::with_gil(|py| { - let f = wrap_pyfunction_bound!(my_function)(py).unwrap(); + let f = wrap_pyfunction!(my_function)(py).unwrap(); py_assert!(py, f, "f.__text_signature__ == None"); - let f = wrap_pyfunction_bound!(my_function_2)(py).unwrap(); + let f = wrap_pyfunction!(my_function_2)(py).unwrap(); py_assert!(py, f, "f.__text_signature__ == None"); let cls = py.get_type_bound::(); diff --git a/tests/test_various.rs b/tests/test_various.rs index dfc6498159c..64e49fd0a6e 100644 --- a/tests/test_various.rs +++ b/tests/test_various.rs @@ -56,7 +56,7 @@ fn return_custom_class() { assert_eq!(get_zero().value, 0); // Using from python - let get_zero = wrap_pyfunction_bound!(get_zero)(py).unwrap(); + let get_zero = wrap_pyfunction!(get_zero)(py).unwrap(); py_assert!(py, get_zero, "get_zero().value == 0"); }); } @@ -203,6 +203,6 @@ fn result_conversion_function() -> Result<(), MyError> { #[test] fn test_result_conversion() { Python::with_gil(|py| { - wrap_pyfunction_bound!(result_conversion_function)(py).unwrap(); + wrap_pyfunction!(result_conversion_function)(py).unwrap(); }); } diff --git a/tests/test_wrap_pyfunction_deduction.rs b/tests/test_wrap_pyfunction_deduction.rs deleted file mode 100644 index e205003113e..00000000000 --- a/tests/test_wrap_pyfunction_deduction.rs +++ /dev/null @@ -1,29 +0,0 @@ -#![cfg(feature = "macros")] - -use pyo3::{prelude::*, types::PyCFunction}; - -#[pyfunction] -fn f() {} - -#[cfg(feature = "gil-refs")] -pub fn add_wrapped(wrapper: &impl Fn(Python<'_>) -> PyResult<&PyCFunction>) { - let _ = wrapper; -} - -#[test] -fn wrap_pyfunction_deduction() { - #[allow(deprecated)] - #[cfg(feature = "gil-refs")] - add_wrapped(wrap_pyfunction!(f)); - #[cfg(not(feature = "gil-refs"))] - add_wrapped_bound(wrap_pyfunction!(f)); -} - -pub fn add_wrapped_bound(wrapper: &impl Fn(Python<'_>) -> PyResult>) { - let _ = wrapper; -} - -#[test] -fn wrap_pyfunction_deduction_bound() { - add_wrapped_bound(wrap_pyfunction_bound!(f)); -} diff --git a/tests/ui/invalid_result_conversion.rs b/tests/ui/invalid_result_conversion.rs index 373d3cacd9d..2679f3afda7 100644 --- a/tests/ui/invalid_result_conversion.rs +++ b/tests/ui/invalid_result_conversion.rs @@ -27,6 +27,6 @@ fn should_not_work() -> Result<(), MyError> { fn main() { Python::with_gil(|py| { - wrap_pyfunction_bound!(should_not_work)(py); + wrap_pyfunction!(should_not_work)(py); }); } From 1ca484dcddad6c521d2f532b8afd6982b0c4bcc6 Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Fri, 26 Jul 2024 18:40:40 +0200 Subject: [PATCH 175/495] fully remove the rest of the gil-refs (#4378) * fully remove the rest of the gil-refs * add newsfragment --- Cargo.toml | 3 - guide/src/SUMMARY.md | 1 - guide/src/class.md | 5 - guide/src/memory.md | 309 ------------ guide/src/migration.md | 2 +- guide/src/types.md | 182 ------- newsfragments/4378.removed.md | 1 + noxfile.py | 12 +- pyo3-macros-backend/Cargo.toml | 1 - pyo3-macros-backend/src/module.rs | 10 +- pyo3-macros-backend/src/pyclass.rs | 13 - pyo3-macros/Cargo.toml | 1 - src/conversion.rs | 146 ------ src/conversions/num_complex.rs | 10 - src/exceptions.rs | 7 - src/gil.rs | 218 +-------- src/impl_/extract_argument.rs | 2 +- src/impl_/not_send.rs | 3 - src/impl_/pyclass.rs | 7 - src/impl_/pymethods.rs | 29 -- src/instance.rs | 281 ----------- src/lib.rs | 9 - src/macros.rs | 4 +- src/marker.rs | 336 ------------- src/prelude.rs | 5 - src/pycell.rs | 484 ------------------- src/pycell/impl_.rs | 19 - src/pyclass.rs | 14 - src/type_object.rs | 130 ----- src/types/any.rs | 737 ----------------------------- src/types/datetime.rs | 10 - src/types/dict.rs | 11 - src/types/mod.rs | 81 ---- tests/test_compile_error.rs | 12 +- tests/test_super.rs | 1 - 35 files changed, 16 insertions(+), 3080 deletions(-) delete mode 100644 guide/src/memory.md create mode 100644 newsfragments/4378.removed.md diff --git a/Cargo.toml b/Cargo.toml index 38673913049..5aac73fced4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -103,9 +103,6 @@ generate-import-lib = ["pyo3-ffi/generate-import-lib"] # Changes `Python::with_gil` to automatically initialize the Python interpreter if needed. auto-initialize = [] -# Allows use of the deprecated "GIL Refs" APIs. -gil-refs = ["pyo3-macros/gil-refs"] - # Enables `Clone`ing references to Python objects `Py` which panics if the GIL is not held. py-clone = [] diff --git a/guide/src/SUMMARY.md b/guide/src/SUMMARY.md index 4c22c26f587..af43897c014 100644 --- a/guide/src/SUMMARY.md +++ b/guide/src/SUMMARY.md @@ -27,7 +27,6 @@ - [Parallelism](parallelism.md) - [Debugging](debugging.md) - [Features reference](features.md) -- [Memory management](memory.md) - [Performance](performance.md) - [Advanced topics](advanced.md) - [Building and distribution](building-and-distribution.md) diff --git a/guide/src/class.md b/guide/src/class.md index 94f3f333581..3a6f3f9f36c 100644 --- a/guide/src/class.md +++ b/guide/src/class.md @@ -1367,11 +1367,6 @@ struct MyClass { impl pyo3::types::DerefToPyAny for MyClass {} -# #[allow(deprecated)] -# #[cfg(feature = "gil-refs")] -unsafe impl pyo3::type_object::HasPyGilRef for MyClass { - type AsRefTarget = pyo3::PyCell; -} unsafe impl pyo3::type_object::PyTypeInfo for MyClass { const NAME: &'static str = "MyClass"; const MODULE: ::std::option::Option<&'static str> = ::std::option::Option::None; diff --git a/guide/src/memory.md b/guide/src/memory.md deleted file mode 100644 index 41454b03500..00000000000 --- a/guide/src/memory.md +++ /dev/null @@ -1,309 +0,0 @@ -# Memory management - -
- -⚠️ Warning: API update in progress 🛠️ - -PyO3 0.21 has introduced a significant new API, termed the "Bound" API after the new smart pointer `Bound`. - -This section on memory management is heavily weighted towards the now-deprecated "GIL Refs" API, which suffered from the drawbacks detailed here as well as CPU overheads. - -See [the smart pointer types](./types.md#pyo3s-smart-pointers) for description on the new, simplified, memory model of the Bound API, which is built as a thin wrapper on Python reference counting. -
- -Rust and Python have very different notions of memory management. Rust has -a strict memory model with concepts of ownership, borrowing, and lifetimes, -where memory is freed at predictable points in program execution. Python has -a looser memory model in which variables are reference-counted with shared, -mutable state by default. A global interpreter lock (GIL) is needed to prevent -race conditions, and a garbage collector is needed to break reference cycles. -Memory in Python is freed eventually by the garbage collector, but not usually -in a predictable way. - -PyO3 bridges the Rust and Python memory models with two different strategies for -accessing memory allocated on Python's heap from inside Rust. These are -GIL Refs such as `&'py PyAny`, and GIL-independent `Py` smart pointers. - -## GIL-bound memory - -PyO3's GIL Refs such as `&'py PyAny` make PyO3 more ergonomic to -use by ensuring that their lifetime can never be longer than the duration the -Python GIL is held. This means that most of PyO3's API can assume the GIL is -held. (If PyO3 could not assume this, every PyO3 API would need to take a -`Python` GIL token to prove that the GIL is held.) This allows us to write -very simple and easy-to-understand programs like this: - -```rust,ignore -# #![allow(unused_imports)] -# use pyo3::prelude::*; -# use pyo3::types::PyString; -# fn main() -> PyResult<()> { -# #[cfg(feature = "gil-refs")] -Python::with_gil(|py| -> PyResult<()> { - #[allow(deprecated)] // py.eval() is part of the GIL Refs API - let hello = py - .eval("\"Hello World!\"", None, None)? - .downcast::()?; - println!("Python says: {}", hello); - Ok(()) -})?; -# Ok(()) -# } -``` - -Internally, calling `Python::with_gil()` creates a `GILPool` which owns the -memory pointed to by the reference. In the example above, the lifetime of the -reference `hello` is bound to the `GILPool`. When the `with_gil()` closure ends -the `GILPool` is also dropped and the Python reference counts of the variables -it owns are decreased, releasing them to the Python garbage collector. Most -of the time we don't have to think about this, but consider the following: - -```rust,ignore -# #![allow(unused_imports)] -# use pyo3::prelude::*; -# use pyo3::types::PyString; -# fn main() -> PyResult<()> { -# #[cfg(feature = "gil-refs")] -Python::with_gil(|py| -> PyResult<()> { - for _ in 0..10 { - #[allow(deprecated)] // py.eval() is part of the GIL Refs API - let hello = py - .eval("\"Hello World!\"", None, None)? - .downcast::()?; - println!("Python says: {}", hello); - } - // There are 10 copies of `hello` on Python's heap here. - Ok(()) -})?; -# Ok(()) -# } -``` - -We might assume that the `hello` variable's memory is freed at the end of each -loop iteration, but in fact we create 10 copies of `hello` on Python's heap. -This may seem surprising at first, but it is completely consistent with Rust's -memory model. The `hello` variable is dropped at the end of each loop, but it -is only a reference to the memory owned by the `GILPool`, and its lifetime is -bound to the `GILPool`, not the for loop. The `GILPool` isn't dropped until -the end of the `with_gil()` closure, at which point the 10 copies of `hello` -are finally released to the Python garbage collector. - -
- -⚠️ Warning: `GILPool` is no longer the preferred way to manage memory with PyO3 🛠️ - -PyO3 0.21 has introduced a new API known as the Bound API, which doesn't have the same surprising results. Instead, each `Bound` smart pointer releases the Python reference immediately on drop. See [the smart pointer types](./types.md#pyo3s-smart-pointers) for more details. -
- - -In general we don't want unbounded memory growth during loops! One workaround -is to acquire and release the GIL with each iteration of the loop. - -```rust,ignore -# #![allow(unused_imports)] -# use pyo3::prelude::*; -# use pyo3::types::PyString; -# fn main() -> PyResult<()> { -# #[cfg(feature = "gil-refs")] -for _ in 0..10 { - Python::with_gil(|py| -> PyResult<()> { - #[allow(deprecated)] // py.eval() is part of the GIL Refs API - let hello = py - .eval("\"Hello World!\"", None, None)? - .downcast::()?; - println!("Python says: {}", hello); - Ok(()) - })?; // only one copy of `hello` at a time -} -# Ok(()) -# } -``` - -It might not be practical or performant to acquire and release the GIL so many -times. Another workaround is to work with the `GILPool` object directly, but -this is unsafe. - -```rust,ignore -# #![allow(unused_imports)] -# use pyo3::prelude::*; -# use pyo3::types::PyString; -# fn main() -> PyResult<()> { -# #[cfg(feature = "gil-refs")] -Python::with_gil(|py| -> PyResult<()> { - for _ in 0..10 { - #[allow(deprecated)] // `new_pool` is not needed in code not using the GIL Refs API - let pool = unsafe { py.new_pool() }; - let py = pool.python(); - #[allow(deprecated)] // py.eval() is part of the GIL Refs API - let hello = py - .eval("\"Hello World!\"", None, None)? - .downcast::()?; - println!("Python says: {}", hello); - } - Ok(()) -})?; -# Ok(()) -# } -``` - -The unsafe method `Python::new_pool` allows you to create a nested `GILPool` -from which you can retrieve a new `py: Python` GIL token. Variables created -with this new GIL token are bound to the nested `GILPool` and will be released -when the nested `GILPool` is dropped. Here, the nested `GILPool` is dropped -at the end of each loop iteration, before the `with_gil()` closure ends. - -When doing this, you must be very careful to ensure that once the `GILPool` is -dropped you do not retain access to any owned references created after the -`GILPool` was created. Read the documentation for `Python::new_pool()` -for more information on safety. - -This memory management can also be applicable when writing extension modules. -`#[pyfunction]` and `#[pymethods]` will create a `GILPool` which lasts the entire -function call, releasing objects when the function returns. Most functions only create -a few objects, meaning this doesn't have a significant impact. Occasionally functions -with long complex loops may need to use `Python::new_pool` as shown above. - -
- -⚠️ Warning: `GILPool` is no longer the preferred way to manage memory with PyO3 🛠️ - -PyO3 0.21 has introduced a new API known as the Bound API, which doesn't have the same surprising results. Instead, each `Bound` smart pointer releases the Python reference immediately on drop. See [the smart pointer types](./types.md#pyo3s-smart-pointers) for more details. -
- -## GIL-independent memory - -Sometimes we need a reference to memory on Python's heap that can outlive the -GIL. Python's `Py` is analogous to `Arc`, but for variables whose -memory is allocated on Python's heap. Cloning a `Py` increases its -internal reference count just like cloning `Arc`. The smart pointer can -outlive the "GIL is held" period in which it was created. It isn't magic, -though. We need to reacquire the GIL to access the memory pointed to by the -`Py`. - -What happens to the memory when the last `Py` is dropped and its -reference count reaches zero? It depends whether or not we are holding the GIL. - -```rust -# #![allow(unused_imports)] -# use pyo3::prelude::*; -# use pyo3::types::PyString; -# fn main() -> PyResult<()> { -# #[cfg(feature = "gil-refs")] -Python::with_gil(|py| -> PyResult<()> { - #[allow(deprecated)] // py.eval() is part of the GIL Refs API - let hello: Py = py.eval("\"Hello World!\"", None, None)?.extract()?; - #[allow(deprecated)] // as_ref is part of the GIL Refs API - { - println!("Python says: {}", hello.as_ref(py)); - } - Ok(()) -})?; -# Ok(()) -# } -``` - -At the end of the `Python::with_gil()` closure `hello` is dropped, and then the -GIL is dropped. Since `hello` is dropped while the GIL is still held by the -current thread, its memory is released to the Python garbage collector -immediately. - -This example wasn't very interesting. We could have just used a GIL-bound -`&PyString` reference. What happens when the last `Py` is dropped while -we are *not* holding the GIL? - -```rust -# #![allow(unused_imports, dead_code)] -# #[cfg(not(pyo3_disable_reference_pool))] { -# use pyo3::prelude::*; -# use pyo3::types::PyString; -# fn main() -> PyResult<()> { -# #[cfg(feature = "gil-refs")] -# { -let hello: Py = Python::with_gil(|py| { - #[allow(deprecated)] // py.eval() is part of the GIL Refs API - py.eval("\"Hello World!\"", None, None)?.extract() -})?; -// Do some stuff... -// Now sometime later in the program we want to access `hello`. -Python::with_gil(|py| { - #[allow(deprecated)] // as_ref is part of the deprecated "GIL Refs" API. - let hello = hello.as_ref(py); - println!("Python says: {}", hello); -}); -// Now we're done with `hello`. -drop(hello); // Memory *not* released here. -// Sometime later we need the GIL again for something... -Python::with_gil(|py| - // Memory for `hello` is released here. -# () -); -# } -# Ok(()) -# } -# } -``` - -When `hello` is dropped *nothing* happens to the pointed-to memory on Python's -heap because nothing _can_ happen if we're not holding the GIL. Fortunately, -the memory isn't leaked. If the `pyo3_disable_reference_pool` conditional compilation flag -is not enabled, PyO3 keeps track of the memory internally and will release it -the next time we acquire the GIL. - -We can avoid the delay in releasing memory if we are careful to drop the -`Py` while the GIL is held. - -```rust -# #![allow(unused_imports)] -# use pyo3::prelude::*; -# use pyo3::types::PyString; -# fn main() -> PyResult<()> { -# #[cfg(feature = "gil-refs")] -# { -#[allow(deprecated)] // py.eval() is part of the GIL Refs API -let hello: Py = - Python::with_gil(|py| py.eval("\"Hello World!\"", None, None)?.extract())?; -// Do some stuff... -// Now sometime later in the program: -Python::with_gil(|py| { - #[allow(deprecated)] // as_ref is part of the GIL Refs API - { - println!("Python says: {}", hello.as_ref(py)); - } - drop(hello); // Memory released here. -}); -# } -# Ok(()) -# } -``` - -We could also have used `Py::into_ref()`, which consumes `self`, instead of -`Py::as_ref()`. But note that in addition to being slower than `as_ref()`, -`into_ref()` binds the memory to the lifetime of the `GILPool`, which means -that rather than being released immediately, the memory will not be released -until the GIL is dropped. - -```rust -# #![allow(unused_imports)] -# use pyo3::prelude::*; -# use pyo3::types::PyString; -# fn main() -> PyResult<()> { -# #[cfg(feature = "gil-refs")] -# { -#[allow(deprecated)] // py.eval() is part of the GIL Refs API -let hello: Py = - Python::with_gil(|py| py.eval("\"Hello World!\"", None, None)?.extract())?; -// Do some stuff... -// Now sometime later in the program: -Python::with_gil(|py| { - #[allow(deprecated)] // into_ref is part of the GIL Refs API - { - println!("Python says: {}", hello.into_ref(py)); - } - // Memory not released yet. - // Do more stuff... - // Memory released here at end of `with_gil()` closure. -}); -# } -# Ok(()) -# } -``` diff --git a/guide/src/migration.md b/guide/src/migration.md index f45ba567291..113560ecaea 100644 --- a/guide/src/migration.md +++ b/guide/src/migration.md @@ -487,7 +487,7 @@ A key thing to note here is because extracting to these types now ties them to t Before: -```rust +```rust,ignore # #[cfg(feature = "gil-refs")] { # use pyo3::prelude::*; # use pyo3::types::{PyList, PyType}; diff --git a/guide/src/types.md b/guide/src/types.md index 564937124ba..ee46b97ebcd 100644 --- a/guide/src/types.md +++ b/guide/src/types.md @@ -6,8 +6,6 @@ The first set of types are the [smart pointers][smart-pointers] which all Python The second set of types are types which fill in the generic parameter `T` of the smart pointers. The most common is `PyAny`, which represents any Python object (similar to Python's `typing.Any`). There are also concrete types for many Python built-in types, such as `PyList`, `PyDict`, and `PyTuple`. User defined `#[pyclass]` types also fit this category. The [second section below](#concrete-python-types) expands on how to use these types. -Before PyO3 0.21, PyO3's main API to interact with Python objects was a deprecated API known as the "GIL Refs" API, containing reference types such as `&PyAny`, `&PyList`, and `&PyCell` for user-defined `#[pyclass]` types. The [third section below](#the-gil-refs-api) details this deprecated API. - ## PyO3's smart pointers PyO3's API offers three generic smart pointers: `Py`, `Bound<'py, T>` and `Borrowed<'a, 'py, T>`. For each of these the type parameter `T` will be filled by a [concrete Python type](#concrete-python-types). For example, a Python list object can be represented by `Py`, `Bound<'py, PyList>`, and `Borrowed<'a, 'py, PyList>`. @@ -310,186 +308,6 @@ assert_eq!((x, y, z), (1, 2, 3)); To avoid copying data, [`#[pyclass]`][pyclass] types can directly reference Rust data stored within the Python objects without needing to `.extract()`. See the [corresponding documentation in the class section of the guide](./class.md#bound-and-interior-mutability) for more detail. -## The GIL Refs API - -The GIL Refs API was PyO3's primary API prior to PyO3 0.21. The main difference was that instead of the `Bound<'py, PyAny>` smart pointer, the "GIL Reference" `&'py PyAny` was used. (This was similar for other Python types.) - -As of PyO3 0.21, the GIL Refs API is deprecated. See the [migration guide](./migration.md#from-020-to-021) for details on how to upgrade. - -The following sections note some historical detail about the GIL Refs API. - -### [`PyAny`][PyAny] - -**Represented:** a Python object of unspecified type. In the GIL Refs API, this was only accessed as the GIL Ref `&'py PyAny`. - -**Used:** `&'py PyAny` was used to refer to some Python object when the GIL lifetime was available for the whole duration access was needed. For example, intermediate values and arguments to `pyfunction`s or `pymethod`s implemented in Rust where any type is allowed. - -**Conversions:** - -For a `&PyAny` object reference `any` where the underlying object is a Python-native type such as -a list: - -```rust,ignore -# #![allow(unused_imports)] -# use pyo3::prelude::*; -# use pyo3::types::PyList; -# #[cfg(feature = "gil-refs")] -# Python::with_gil(|py| -> PyResult<()> { -#[allow(deprecated)] // PyList::empty is part of the deprecated "GIL Refs" API. -let obj: &PyAny = PyList::empty(py); - -// To &PyList with PyAny::downcast -let _: &PyList = obj.downcast()?; - -// To Py (aka PyObject) with .into() -let _: Py = obj.into(); - -// To Py with PyAny::extract -let _: Py = obj.extract()?; -# Ok(()) -# }).unwrap(); -``` - -For a `&PyAny` object reference `any` where the underlying object is a `#[pyclass]`: - -```rust,ignore -# #![allow(unused_imports)] -# use pyo3::prelude::*; -# #[pyclass] #[derive(Clone)] struct MyClass { } -# #[cfg(feature = "gil-refs")] -# Python::with_gil(|py| -> PyResult<()> { -#[allow(deprecated)] // into_ref is part of the deprecated GIL Refs API -let obj: &PyAny = Py::new(py, MyClass {})?.into_ref(py); - -// To &PyCell with PyAny::downcast -#[allow(deprecated)] // &PyCell is part of the deprecated GIL Refs API -let _: &PyCell = obj.downcast()?; - -// To Py (aka PyObject) with .into() -let _: Py = obj.into(); - -// To Py with PyAny::extract -let _: Py = obj.extract()?; - -// To MyClass with PyAny::extract, if MyClass: Clone -let _: MyClass = obj.extract()?; - -// To PyRef<'_, MyClass> or PyRefMut<'_, MyClass> with PyAny::extract -let _: PyRef<'_, MyClass> = obj.extract()?; -let _: PyRefMut<'_, MyClass> = obj.extract()?; -# Ok(()) -# }).unwrap(); -``` - -### `PyTuple`, `PyDict`, and many more - -**Represented:** a native Python object of known type. In the GIL Refs API, they were only accessed as the GIL Refs `&'py PyTuple`, `&'py PyDict`. - -**Used:** `&'py PyTuple` and similar were used to operate with native Python types while holding the GIL. Like `PyAny`, this is the most convenient form to use for function arguments and intermediate values. - -These GIL Refs implement `Deref`, so they all expose the same methods which can be found on `PyAny`. - -To see all Python types exposed by `PyO3` consult the [`pyo3::types`][pyo3::types] module. - -**Conversions:** - -```rust,ignore -# #![allow(unused_imports)] -# use pyo3::prelude::*; -# use pyo3::types::PyList; -# #[cfg(feature = "gil-refs")] -# Python::with_gil(|py| -> PyResult<()> { -#[allow(deprecated)] // PyList::empty is part of the deprecated "GIL Refs" API. -let list = PyList::empty(py); - -// Use methods from PyAny on all Python types with Deref implementation -let _ = list.repr()?; - -// To &PyAny automatically with Deref implementation -let _: &PyAny = list; - -// To &PyAny explicitly with .as_ref() -#[allow(deprecated)] // as_ref is part of the deprecated "GIL Refs" API. -let _: &PyAny = list.as_ref(); - -// To Py with .into() or Py::from() -let _: Py = list.into(); - -// To PyObject with .into() or .to_object(py) -let _: PyObject = list.into(); -# Ok(()) -# }).unwrap(); -``` - -### `Py` and `PyObject` - -**Represented:** a GIL-independent reference to a Python object. This can be a Python native type -(like `PyTuple`), or a `pyclass` type implemented in Rust. The most commonly-used variant, -`Py`, is also known as `PyObject`. - -**Used:** Whenever you want to carry around references to a Python object without caring about a -GIL lifetime. For example, storing Python object references in a Rust struct that outlives the -Python-Rust FFI boundary, or returning objects from functions implemented in Rust back to Python. - -Can be cloned using Python reference counts with `.clone()`. - -### `PyCell` - -**Represented:** a reference to a Rust object (instance of `PyClass`) wrapped in a Python object. The cell part is an analog to stdlib's [`RefCell`][RefCell] to allow access to `&mut` references. - -**Used:** for accessing pure-Rust API of the instance (members and functions taking `&SomeType` or `&mut SomeType`) while maintaining the aliasing rules of Rust references. - -Like PyO3's Python native types, the GIL Ref `&PyCell` implements `Deref`, so it also exposed all of the methods on `PyAny`. - -**Conversions:** - -`PyCell` was used to access `&T` and `&mut T` via `PyRef` and `PyRefMut` respectively. - -```rust -#![allow(unused_imports)] -# use pyo3::prelude::*; -# #[pyclass] struct MyClass { } -# #[cfg(feature = "gil-refs")] -# Python::with_gil(|py| -> PyResult<()> { -#[allow(deprecated)] // &PyCell is part of the deprecated GIL Refs API -let cell: &PyCell = PyCell::new(py, MyClass {})?; - -// To PyRef with .borrow() or .try_borrow() -let py_ref: PyRef<'_, MyClass> = cell.try_borrow()?; -let _: &MyClass = &*py_ref; -# drop(py_ref); - -// To PyRefMut with .borrow_mut() or .try_borrow_mut() -let mut py_ref_mut: PyRefMut<'_, MyClass> = cell.try_borrow_mut()?; -let _: &mut MyClass = &mut *py_ref_mut; -# Ok(()) -# }).unwrap(); -``` - -`PyCell` was also accessed like a Python-native type. - -```rust -#![allow(unused_imports)] -# use pyo3::prelude::*; -# #[pyclass] struct MyClass { } -# #[cfg(feature = "gil-refs")] -# Python::with_gil(|py| -> PyResult<()> { -#[allow(deprecated)] // &PyCell is part of the deprecate GIL Refs API -let cell: &PyCell = PyCell::new(py, MyClass {})?; - -// Use methods from PyAny on PyCell with Deref implementation -let _ = cell.repr()?; - -// To &PyAny automatically with Deref implementation -let _: &PyAny = cell; - -// To &PyAny explicitly with .as_ref() -#[allow(deprecated)] // as_ref is part of the deprecated "GIL Refs" API. -let _: &PyAny = cell.as_ref(); -# Ok(()) -# }).unwrap(); -``` - [Bound]: {{#PYO3_DOCS_URL}}/pyo3/struct.Bound.html [`Bound::unbind`]: {{#PYO3_DOCS_URL}}/pyo3/struct.Bound.html#method.unbind [Py]: {{#PYO3_DOCS_URL}}/pyo3/struct.Py.html diff --git a/newsfragments/4378.removed.md b/newsfragments/4378.removed.md new file mode 100644 index 00000000000..3c43d46478d --- /dev/null +++ b/newsfragments/4378.removed.md @@ -0,0 +1 @@ +removed deprecated `gil-refs` feature diff --git a/noxfile.py b/noxfile.py index 5d8123c7eb4..0267531cb7d 100644 --- a/noxfile.py +++ b/noxfile.py @@ -51,7 +51,6 @@ def test_rust(session: nox.Session): _run_cargo_test(session, features="abi3") if "skip-full" not in session.posargs: _run_cargo_test(session, features="full") - _run_cargo_test(session, features="full gil-refs") _run_cargo_test(session, features="abi3 full") @@ -673,7 +672,6 @@ def check_feature_powerset(session: nox.Session): EXCLUDED_FROM_FULL = { "nightly", - "gil-refs", "extension-module", "full", "default", @@ -715,7 +713,7 @@ def check_feature_powerset(session: nox.Session): session.error("no experimental features exist; please simplify the noxfile") features_to_skip = [ - *(EXCLUDED_FROM_FULL - {"gil-refs"}), + *(EXCLUDED_FROM_FULL), *abi3_version_features, ] @@ -793,8 +791,8 @@ def _get_feature_sets() -> Tuple[Tuple[str, ...], ...]: "--no-default-features", "--features=abi3", ), - ("--features=full gil-refs multiple-pymethods",), - ("--features=abi3 full gil-refs multiple-pymethods",), + ("--features=full multiple-pymethods",), + ("--features=abi3 full multiple-pymethods",), ) else: return ( @@ -803,8 +801,8 @@ def _get_feature_sets() -> Tuple[Tuple[str, ...], ...]: "--no-default-features", "--features=abi3", ), - ("--features=full gil-refs",), - ("--features=abi3 full gil-refs",), + ("--features=full",), + ("--features=abi3 full",), ) diff --git a/pyo3-macros-backend/Cargo.toml b/pyo3-macros-backend/Cargo.toml index b3775ef8518..337d19f5653 100644 --- a/pyo3-macros-backend/Cargo.toml +++ b/pyo3-macros-backend/Cargo.toml @@ -32,4 +32,3 @@ workspace = true [features] experimental-async = [] -gil-refs = [] diff --git a/pyo3-macros-backend/src/module.rs b/pyo3-macros-backend/src/module.rs index ea982f686b8..e0025fda6dd 100644 --- a/pyo3-macros-backend/src/module.rs +++ b/pyo3-macros-backend/src/module.rs @@ -462,11 +462,6 @@ fn process_functions_in_module(options: &PyModuleOptions, func: &mut syn::ItemFn let Ctx { pyo3_path, .. } = ctx; let mut stmts: Vec = Vec::new(); - #[cfg(feature = "gil-refs")] - let imports = quote!(use #pyo3_path::{PyNativeType, types::PyModuleMethods};); - #[cfg(not(feature = "gil-refs"))] - let imports = quote!(use #pyo3_path::types::PyModuleMethods;); - for mut stmt in func.block.stmts.drain(..) { if let syn::Stmt::Item(Item::Fn(func)) = &mut stmt { if let Some(pyfn_args) = get_pyfn_attr(&mut func.attrs)? { @@ -476,9 +471,8 @@ fn process_functions_in_module(options: &PyModuleOptions, func: &mut syn::ItemFn let statements: Vec = syn::parse_quote! { #wrapped_function { - #[allow(unknown_lints, unused_imports, redundant_imports)] - #imports - #module_name.as_borrowed().add_function(#pyo3_path::wrap_pyfunction!(#name, #module_name.as_borrowed())?)?; + use #pyo3_path::types::PyModuleMethods; + #module_name.add_function(#pyo3_path::wrap_pyfunction!(#name, #module_name.as_borrowed())?)?; } }; stmts.extend(statements); diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index 65fffc1655f..e9e5af6a8ac 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -1743,20 +1743,7 @@ fn impl_pytypeinfo(cls: &syn::Ident, attr: &PyClassArgs, ctx: &Ctx) -> TokenStre quote! { ::core::option::Option::None } }; - #[cfg(feature = "gil-refs")] - let has_py_gil_ref = quote! { - #[allow(deprecated)] - unsafe impl #pyo3_path::type_object::HasPyGilRef for #cls { - type AsRefTarget = #pyo3_path::PyCell; - } - }; - - #[cfg(not(feature = "gil-refs"))] - let has_py_gil_ref = TokenStream::new(); - quote! { - #has_py_gil_ref - unsafe impl #pyo3_path::type_object::PyTypeInfo for #cls { const NAME: &'static str = #cls_name; const MODULE: ::std::option::Option<&'static str> = #module; diff --git a/pyo3-macros/Cargo.toml b/pyo3-macros/Cargo.toml index 42d6d801c89..c86afd8e2d2 100644 --- a/pyo3-macros/Cargo.toml +++ b/pyo3-macros/Cargo.toml @@ -16,7 +16,6 @@ proc-macro = true [features] multiple-pymethods = [] experimental-async = ["pyo3-macros-backend/experimental-async"] -gil-refs = ["pyo3-macros-backend/gil-refs"] [dependencies] proc-macro2 = { version = "1.0.60", default-features = false } diff --git a/src/conversion.rs b/src/conversion.rs index 79f5f7f13cf..bbebf89c15c 100644 --- a/src/conversion.rs +++ b/src/conversion.rs @@ -6,11 +6,6 @@ use crate::pyclass::boolean_struct::False; use crate::types::any::PyAnyMethods; use crate::types::PyTuple; use crate::{ffi, Borrowed, Bound, Py, PyAny, PyClass, PyObject, PyRef, PyRefMut, Python}; -#[cfg(feature = "gil-refs")] -use { - crate::{err, gil, PyNativeType}, - std::ptr::NonNull, -}; /// Returns a borrowed pointer to a Python object. /// @@ -220,15 +215,6 @@ pub trait IntoPy: Sized { /// infinite recursion, implementors must implement at least one of these methods. The recommendation /// is to implement `extract_bound` and leave `extract` as the default implementation. pub trait FromPyObject<'py>: Sized { - /// Extracts `Self` from the source GIL Ref `obj`. - /// - /// Implementors are encouraged to implement `extract_bound` and leave this method as the - /// default implementation, which will forward calls to `extract_bound`. - #[cfg(feature = "gil-refs")] - fn extract(ob: &'py PyAny) -> PyResult { - Self::extract_bound(&ob.as_borrowed()) - } - /// Extracts `Self` from the bound smart pointer `obj`. /// /// Implementors are encouraged to implement this method and leave `extract` defaulted, as @@ -384,138 +370,6 @@ impl IntoPy> for () { } } -/// Raw level conversion between `*mut ffi::PyObject` and PyO3 types. -/// -/// # Safety -/// -/// See safety notes on individual functions. -#[cfg(feature = "gil-refs")] -#[deprecated(since = "0.21.0")] -pub unsafe trait FromPyPointer<'p>: Sized { - /// Convert from an arbitrary `PyObject`. - /// - /// # Safety - /// - /// Implementations must ensure the object does not get freed during `'p` - /// and ensure that `ptr` is of the correct type. - /// Note that it must be safe to decrement the reference count of `ptr`. - #[deprecated( - since = "0.21.0", - note = "use `Py::from_owned_ptr_or_opt(py, ptr)` or `Bound::from_owned_ptr_or_opt(py, ptr)` instead" - )] - unsafe fn from_owned_ptr_or_opt(py: Python<'p>, ptr: *mut ffi::PyObject) -> Option<&'p Self>; - /// Convert from an arbitrary `PyObject` or panic. - /// - /// # Safety - /// - /// Relies on [`from_owned_ptr_or_opt`](#method.from_owned_ptr_or_opt). - #[deprecated( - since = "0.21.0", - note = "use `Py::from_owned_ptr(py, ptr)` or `Bound::from_owned_ptr(py, ptr)` instead" - )] - unsafe fn from_owned_ptr_or_panic(py: Python<'p>, ptr: *mut ffi::PyObject) -> &'p Self { - #[allow(deprecated)] - Self::from_owned_ptr_or_opt(py, ptr).unwrap_or_else(|| err::panic_after_error(py)) - } - /// Convert from an arbitrary `PyObject` or panic. - /// - /// # Safety - /// - /// Relies on [`from_owned_ptr_or_opt`](#method.from_owned_ptr_or_opt). - #[deprecated( - since = "0.21.0", - note = "use `Py::from_owned_ptr(py, ptr)` or `Bound::from_owned_ptr(py, ptr)` instead" - )] - unsafe fn from_owned_ptr(py: Python<'p>, ptr: *mut ffi::PyObject) -> &'p Self { - #[allow(deprecated)] - Self::from_owned_ptr_or_panic(py, ptr) - } - /// Convert from an arbitrary `PyObject`. - /// - /// # Safety - /// - /// Relies on [`from_owned_ptr_or_opt`](#method.from_owned_ptr_or_opt). - #[deprecated( - since = "0.21.0", - note = "use `Py::from_owned_ptr_or_err(py, ptr)` or `Bound::from_owned_ptr_or_err(py, ptr)` instead" - )] - unsafe fn from_owned_ptr_or_err(py: Python<'p>, ptr: *mut ffi::PyObject) -> PyResult<&'p Self> { - #[allow(deprecated)] - Self::from_owned_ptr_or_opt(py, ptr).ok_or_else(|| err::PyErr::fetch(py)) - } - /// Convert from an arbitrary borrowed `PyObject`. - /// - /// # Safety - /// - /// Implementations must ensure the object does not get freed during `'p` and avoid type confusion. - #[deprecated( - since = "0.21.0", - note = "use `Py::from_borrowed_ptr_or_opt(py, ptr)` or `Bound::from_borrowed_ptr_or_opt(py, ptr)` instead" - )] - unsafe fn from_borrowed_ptr_or_opt(py: Python<'p>, ptr: *mut ffi::PyObject) - -> Option<&'p Self>; - /// Convert from an arbitrary borrowed `PyObject`. - /// - /// # Safety - /// - /// Relies on unsafe fn [`from_borrowed_ptr_or_opt`](#method.from_borrowed_ptr_or_opt). - #[deprecated( - since = "0.21.0", - note = "use `Py::from_borrowed_ptr(py, ptr)` or `Bound::from_borrowed_ptr(py, ptr)` instead" - )] - unsafe fn from_borrowed_ptr_or_panic(py: Python<'p>, ptr: *mut ffi::PyObject) -> &'p Self { - #[allow(deprecated)] - Self::from_borrowed_ptr_or_opt(py, ptr).unwrap_or_else(|| err::panic_after_error(py)) - } - /// Convert from an arbitrary borrowed `PyObject`. - /// - /// # Safety - /// - /// Relies on unsafe fn [`from_borrowed_ptr_or_opt`](#method.from_borrowed_ptr_or_opt). - #[deprecated( - since = "0.21.0", - note = "use `Py::from_borrowed_ptr(py, ptr)` or `Bound::from_borrowed_ptr(py, ptr)` instead" - )] - unsafe fn from_borrowed_ptr(py: Python<'p>, ptr: *mut ffi::PyObject) -> &'p Self { - #[allow(deprecated)] - Self::from_borrowed_ptr_or_panic(py, ptr) - } - /// Convert from an arbitrary borrowed `PyObject`. - /// - /// # Safety - /// - /// Relies on unsafe fn [`from_borrowed_ptr_or_opt`](#method.from_borrowed_ptr_or_opt). - #[deprecated( - since = "0.21.0", - note = "use `Py::from_borrowed_ptr_or_err(py, ptr)` or `Bound::from_borrowed_ptr_or_err(py, ptr)` instead" - )] - unsafe fn from_borrowed_ptr_or_err( - py: Python<'p>, - ptr: *mut ffi::PyObject, - ) -> PyResult<&'p Self> { - #[allow(deprecated)] - Self::from_borrowed_ptr_or_opt(py, ptr).ok_or_else(|| err::PyErr::fetch(py)) - } -} - -#[cfg(feature = "gil-refs")] -#[allow(deprecated)] -unsafe impl<'p, T> FromPyPointer<'p> for T -where - T: 'p + crate::PyNativeType, -{ - unsafe fn from_owned_ptr_or_opt(py: Python<'p>, ptr: *mut ffi::PyObject) -> Option<&'p Self> { - gil::register_owned(py, NonNull::new(ptr)?); - Some(&*(ptr as *mut Self)) - } - unsafe fn from_borrowed_ptr_or_opt( - _py: Python<'p>, - ptr: *mut ffi::PyObject, - ) -> Option<&'p Self> { - NonNull::new(ptr as *mut Self).map(|p| &*p.as_ptr()) - } -} - /// ```rust,compile_fail /// use pyo3::prelude::*; /// diff --git a/src/conversions/num_complex.rs b/src/conversions/num_complex.rs index 12f208aa8d1..172d1efd505 100644 --- a/src/conversions/num_complex.rs +++ b/src/conversions/num_complex.rs @@ -103,16 +103,6 @@ use num_complex::Complex; use std::os::raw::c_double; impl PyComplex { - /// Deprecated form of [`PyComplex::from_complex_bound`] - #[cfg(feature = "gil-refs")] - #[deprecated( - since = "0.21.0", - note = "`PyComplex::from_complex` will be replaced by `PyComplex::from_complex_bound` in a future PyO3 version" - )] - pub fn from_complex>(py: Python<'_>, complex: Complex) -> &PyComplex { - Self::from_complex_bound(py, complex).into_gil_ref() - } - /// Creates a new Python `PyComplex` object from `num_complex`'s [`Complex`]. pub fn from_complex_bound>( py: Python<'_>, diff --git a/src/exceptions.rs b/src/exceptions.rs index bca706c43d4..e9be7a96606 100644 --- a/src/exceptions.rs +++ b/src/exceptions.rs @@ -118,13 +118,6 @@ macro_rules! import_exception_bound { $crate::impl_exception_boilerplate_bound!($name); - // FIXME remove this: was necessary while `PyTypeInfo` requires `HasPyGilRef`, - // should change in 0.22. - #[cfg(feature = "gil-refs")] - unsafe impl $crate::type_object::HasPyGilRef for $name { - type AsRefTarget = $crate::PyAny; - } - $crate::pyobject_native_type_info!( $name, $name::type_object_raw, diff --git a/src/gil.rs b/src/gil.rs index ec20fc64c34..644fbe6e00a 100644 --- a/src/gil.rs +++ b/src/gil.rs @@ -1,17 +1,11 @@ //! Interaction with Python's global interpreter lock -#[cfg(feature = "gil-refs")] -use crate::impl_::not_send::{NotSend, NOT_SEND}; #[cfg(pyo3_disable_reference_pool)] use crate::impl_::panic::PanicTrap; use crate::{ffi, Python}; #[cfg(not(pyo3_disable_reference_pool))] use once_cell::sync::Lazy; use std::cell::Cell; -#[cfg(all(feature = "gil-refs", debug_assertions))] -use std::cell::RefCell; -#[cfg(all(feature = "gil-refs", not(debug_assertions)))] -use std::cell::UnsafeCell; use std::{mem, ptr::NonNull, sync}; static START: sync::Once = sync::Once::new(); @@ -27,12 +21,6 @@ std::thread_local! { /// Additionally, we sometimes need to prevent safe access to the GIL, /// e.g. when implementing `__traverse__`, which is represented by a negative value. static GIL_COUNT: Cell = const { Cell::new(0) }; - - /// Temporarily hold objects that will be released when the GILPool drops. - #[cfg(all(feature = "gil-refs", debug_assertions))] - static OWNED_OBJECTS: RefCell = const { RefCell::new(Vec::new()) }; - #[cfg(all(feature = "gil-refs", not(debug_assertions)))] - static OWNED_OBJECTS: UnsafeCell = const { UnsafeCell::new(Vec::new()) }; } const GIL_LOCKED_DURING_TRAVERSE: isize = -1; @@ -152,12 +140,7 @@ pub(crate) enum GILGuard { /// Indicates the GIL was already held with this GILGuard was acquired. Assumed, /// Indicates that we actually acquired the GIL when this GILGuard was acquired - Ensured { - gstate: ffi::PyGILState_STATE, - #[cfg(feature = "gil-refs")] - #[allow(deprecated)] - pool: mem::ManuallyDrop, - }, + Ensured { gstate: ffi::PyGILState_STATE }, } impl GILGuard { @@ -224,19 +207,11 @@ impl GILGuard { let gstate = ffi::PyGILState_Ensure(); // acquire GIL increment_gil_count(); - #[cfg(feature = "gil-refs")] - #[allow(deprecated)] - let pool = mem::ManuallyDrop::new(GILPool::new()); - #[cfg(not(pyo3_disable_reference_pool))] if let Some(pool) = Lazy::get(&POOL) { pool.update_counts(Python::assume_gil_acquired()); } - GILGuard::Ensured { - gstate, - #[cfg(feature = "gil-refs")] - pool, - } + GILGuard::Ensured { gstate } } /// Acquires the `GILGuard` while assuming that the GIL is already held. @@ -262,14 +237,8 @@ impl Drop for GILGuard { fn drop(&mut self) { match self { GILGuard::Assumed => {} - GILGuard::Ensured { - gstate, - #[cfg(feature = "gil-refs")] - pool, - } => unsafe { + GILGuard::Ensured { gstate } => unsafe { // Drop the objects in the pool before attempting to release the thread state - #[cfg(feature = "gil-refs")] - mem::ManuallyDrop::drop(pool); ffi::PyGILState_Release(*gstate); }, } @@ -386,92 +355,6 @@ impl Drop for LockGIL { } } -/// A RAII pool which PyO3 uses to store owned Python references. -/// -/// See the [Memory Management] chapter of the guide for more information about how PyO3 uses -/// [`GILPool`] to manage memory. - -/// -/// [Memory Management]: https://pyo3.rs/main/memory.html#gil-bound-memory -#[cfg(feature = "gil-refs")] -#[deprecated( - since = "0.21.0", - note = "`GILPool` has no function if PyO3's deprecated GIL Refs API is not used" -)] -pub struct GILPool { - /// Initial length of owned objects and anys. - /// `Option` is used since TSL can be broken when `new` is called from `atexit`. - start: Option, - _not_send: NotSend, -} - -#[cfg(feature = "gil-refs")] -#[allow(deprecated)] -impl GILPool { - /// Creates a new [`GILPool`]. This function should only ever be called with the GIL held. - /// - /// It is recommended not to use this API directly, but instead to use `Python::new_pool`, as - /// that guarantees the GIL is held. - /// - /// # Safety - /// - /// As well as requiring the GIL, see the safety notes on `Python::new_pool`. - #[inline] - pub unsafe fn new() -> GILPool { - // Update counts of PyObjects / Py that have been cloned or dropped since last acquisition - #[cfg(not(pyo3_disable_reference_pool))] - if let Some(pool) = Lazy::get(&POOL) { - pool.update_counts(Python::assume_gil_acquired()); - } - GILPool { - start: OWNED_OBJECTS - .try_with(|owned_objects| { - #[cfg(debug_assertions)] - let len = owned_objects.borrow().len(); - #[cfg(not(debug_assertions))] - // SAFETY: This is not re-entrant. - let len = unsafe { (*owned_objects.get()).len() }; - len - }) - .ok(), - _not_send: NOT_SEND, - } - } - - /// Gets the Python token associated with this [`GILPool`]. - #[inline] - pub fn python(&self) -> Python<'_> { - unsafe { Python::assume_gil_acquired() } - } -} - -#[cfg(feature = "gil-refs")] -#[allow(deprecated)] -impl Drop for GILPool { - fn drop(&mut self) { - if let Some(start) = self.start { - let owned_objects = OWNED_OBJECTS.with(|owned_objects| { - #[cfg(debug_assertions)] - let mut owned_objects = owned_objects.borrow_mut(); - #[cfg(not(debug_assertions))] - // SAFETY: `OWNED_OBJECTS` is released before calling Py_DECREF, - // or Py_DECREF may call `GILPool::drop` recursively, resulting in invalid borrowing. - let owned_objects = unsafe { &mut *owned_objects.get() }; - if start < owned_objects.len() { - owned_objects.split_off(start) - } else { - Vec::new() - } - }); - for obj in owned_objects { - unsafe { - ffi::Py_DECREF(obj.as_ptr()); - } - } - } - } -} - /// Increments the reference count of a Python object if the GIL is held. If /// the GIL is not held, this function will panic. /// @@ -513,25 +396,6 @@ pub unsafe fn register_decref(obj: NonNull) { } } -/// Registers an owned object inside the GILPool, to be released when the GILPool drops. -/// -/// # Safety -/// The object must be an owned Python reference. -#[cfg(feature = "gil-refs")] -pub unsafe fn register_owned(_py: Python<'_>, obj: NonNull) { - debug_assert!(gil_is_acquired()); - // Ignores the error in case this function called from `atexit`. - let _ = OWNED_OBJECTS.try_with(|owned_objects| { - #[cfg(debug_assertions)] - owned_objects.borrow_mut().push(obj); - #[cfg(not(debug_assertions))] - // SAFETY: This is not re-entrant. - unsafe { - (*owned_objects.get()).push(obj); - } - }); -} - /// Increments pyo3's internal GIL count - to be called whenever GILPool or GILGuard is created. #[inline(always)] fn increment_gil_count() { @@ -562,13 +426,8 @@ fn decrement_gil_count() { #[cfg(test)] mod tests { use super::GIL_COUNT; - #[cfg(feature = "gil-refs")] - #[allow(deprecated)] - use super::OWNED_OBJECTS; #[cfg(not(pyo3_disable_reference_pool))] use super::{gil_is_acquired, POOL}; - #[cfg(feature = "gil-refs")] - use crate::{ffi, gil}; use crate::{gil::GILGuard, types::any::PyAnyMethods}; use crate::{PyObject, Python}; use std::ptr::NonNull; @@ -577,15 +436,6 @@ mod tests { py.eval_bound("object()", None, None).unwrap().unbind() } - #[cfg(feature = "gil-refs")] - fn owned_object_count() -> usize { - #[cfg(debug_assertions)] - let len = OWNED_OBJECTS.with(|owned_objects| owned_objects.borrow().len()); - #[cfg(not(debug_assertions))] - let len = OWNED_OBJECTS.with(|owned_objects| unsafe { (*owned_objects.get()).len() }); - len - } - #[cfg(not(pyo3_disable_reference_pool))] fn pool_dec_refs_does_not_contain(obj: &PyObject) -> bool { !POOL @@ -603,68 +453,6 @@ mod tests { .contains(&unsafe { NonNull::new_unchecked(obj.as_ptr()) }) } - #[test] - #[cfg(feature = "gil-refs")] - #[allow(deprecated)] - fn test_owned() { - Python::with_gil(|py| { - let obj = get_object(py); - let obj_ptr = obj.as_ptr(); - // Ensure that obj does not get freed - let _ref = obj.clone_ref(py); - - unsafe { - { - let pool = py.new_pool(); - gil::register_owned(pool.python(), NonNull::new_unchecked(obj.into_ptr())); - - assert_eq!(owned_object_count(), 1); - assert_eq!(ffi::Py_REFCNT(obj_ptr), 2); - } - { - let _pool = py.new_pool(); - assert_eq!(owned_object_count(), 0); - assert_eq!(ffi::Py_REFCNT(obj_ptr), 1); - } - } - }) - } - - #[test] - #[cfg(feature = "gil-refs")] - #[allow(deprecated)] - fn test_owned_nested() { - Python::with_gil(|py| { - let obj = get_object(py); - // Ensure that obj does not get freed - let _ref = obj.clone_ref(py); - let obj_ptr = obj.as_ptr(); - - unsafe { - { - let _pool = py.new_pool(); - assert_eq!(owned_object_count(), 0); - - gil::register_owned(py, NonNull::new_unchecked(obj.into_ptr())); - - assert_eq!(owned_object_count(), 1); - assert_eq!(ffi::Py_REFCNT(obj_ptr), 2); - { - let _pool = py.new_pool(); - let obj = get_object(py); - gil::register_owned(py, NonNull::new_unchecked(obj.into_ptr())); - assert_eq!(owned_object_count(), 2); - } - assert_eq!(owned_object_count(), 1); - } - { - assert_eq!(owned_object_count(), 0); - assert_eq!(ffi::Py_REFCNT(obj_ptr), 1); - } - } - }); - } - #[test] fn test_pyobject_drop_with_gil_decreases_refcnt() { Python::with_gil(|py| { diff --git a/src/impl_/extract_argument.rs b/src/impl_/extract_argument.rs index ef62ef26e7b..4945d977dd9 100644 --- a/src/impl_/extract_argument.rs +++ b/src/impl_/extract_argument.rs @@ -65,7 +65,7 @@ where } } -#[cfg(all(Py_LIMITED_API, not(any(feature = "gil-refs", Py_3_10))))] +#[cfg(all(Py_LIMITED_API, not(Py_3_10)))] impl<'a> PyFunctionArgument<'a, '_> for &'a str { type Holder = Option>; diff --git a/src/impl_/not_send.rs b/src/impl_/not_send.rs index 382e07a14ee..3304d79a1ba 100644 --- a/src/impl_/not_send.rs +++ b/src/impl_/not_send.rs @@ -5,6 +5,3 @@ use crate::Python; /// A marker type that makes the type !Send. /// Workaround for lack of !Send on stable (). pub(crate) struct NotSend(PhantomData<*mut Python<'static>>); - -#[cfg(feature = "gil-refs")] -pub(crate) const NOT_SEND: NotSend = NotSend(PhantomData); diff --git a/src/impl_/pyclass.rs b/src/impl_/pyclass.rs index 391e96f5f43..7ad772f0496 100644 --- a/src/impl_/pyclass.rs +++ b/src/impl_/pyclass.rs @@ -1,5 +1,3 @@ -#[cfg(feature = "gil-refs")] -use crate::PyNativeType; use crate::{ exceptions::{PyAttributeError, PyNotImplementedError, PyRuntimeError, PyValueError}, ffi, @@ -177,11 +175,6 @@ pub trait PyClassImpl: Sized + 'static { /// The closest native ancestor. This is `PyAny` by default, and when you declare /// `#[pyclass(extends=PyDict)]`, it's `PyDict`. - #[cfg(feature = "gil-refs")] - type BaseNativeType: PyTypeInfo + PyNativeType; - /// The closest native ancestor. This is `PyAny` by default, and when you declare - /// `#[pyclass(extends=PyDict)]`, it's `PyDict`. - #[cfg(not(feature = "gil-refs"))] type BaseNativeType: PyTypeInfo; /// This handles following two situations: diff --git a/src/impl_/pymethods.rs b/src/impl_/pymethods.rs index 60b655e5647..77c6e6991ac 100644 --- a/src/impl_/pymethods.rs +++ b/src/impl_/pymethods.rs @@ -5,8 +5,6 @@ use crate::impl_::panic::PanicTrap; use crate::pycell::{PyBorrowError, PyBorrowMutError}; use crate::pyclass::boolean_struct::False; use crate::types::any::PyAnyMethods; -#[cfg(feature = "gil-refs")] -use crate::types::{PyModule, PyType}; use crate::{ ffi, Borrowed, Bound, DowncastError, Py, PyAny, PyClass, PyClassInitializer, PyErr, PyObject, PyRef, PyRefMut, PyResult, PyTraverseError, PyTypeCheck, PyVisit, Python, @@ -467,33 +465,6 @@ impl<'a, 'py> BoundRef<'a, 'py, PyAny> { } } -// GIL Ref implementations for &'a T ran into trouble with orphan rules, -// so explicit implementations are used instead for the two relevant types. -#[cfg(feature = "gil-refs")] -impl<'a> From> for &'a PyType { - #[inline] - fn from(bound: BoundRef<'a, 'a, PyType>) -> Self { - bound.0.as_gil_ref() - } -} - -#[cfg(feature = "gil-refs")] -impl<'a> From> for &'a PyModule { - #[inline] - fn from(bound: BoundRef<'a, 'a, PyModule>) -> Self { - bound.0.as_gil_ref() - } -} - -#[allow(deprecated)] -#[cfg(feature = "gil-refs")] -impl<'a, 'py, T: PyClass> From> for &'a crate::PyCell { - #[inline] - fn from(bound: BoundRef<'a, 'py, T>) -> Self { - bound.0.as_gil_ref() - } -} - impl<'a, 'py, T: PyClass> TryFrom> for PyRef<'py, T> { type Error = PyBorrowError; #[inline] diff --git a/src/instance.rs b/src/instance.rs index 3b8e2529a6e..51c4d62b2d5 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -3,8 +3,6 @@ use crate::impl_::pycell::PyClassObject; use crate::internal_tricks::ptr_from_ref; use crate::pycell::{PyBorrowError, PyBorrowMutError}; use crate::pyclass::boolean_struct::{False, True}; -#[cfg(feature = "gil-refs")] -use crate::type_object::HasPyGilRef; use crate::types::{any::PyAnyMethods, string::PyStringMethods, typeobject::PyTypeMethods}; use crate::types::{DerefToPyAny, PyDict, PyString, PyTuple}; use crate::{ @@ -17,54 +15,6 @@ use std::mem::ManuallyDrop; use std::ops::Deref; use std::ptr::NonNull; -/// Types that are built into the Python interpreter. -/// -/// PyO3 is designed in a way that all references to those types are bound -/// to the GIL, which is why you can get a token from all references of those -/// types. -/// -/// # Safety -/// -/// This trait must only be implemented for types which cannot be accessed without the GIL. -#[cfg(feature = "gil-refs")] -pub unsafe trait PyNativeType: Sized { - /// The form of this which is stored inside a `Py` smart pointer. - type AsRefSource: HasPyGilRef; - - /// Cast `&self` to a `Borrowed` smart pointer. - /// - /// `Borrowed` implements `Deref>`, so can also be used in locations - /// where `Bound` is expected. - /// - /// This is available as a migration tool to adjust code from the deprecated "GIL Refs" - /// API to the `Bound` smart pointer API. - #[inline] - fn as_borrowed(&self) -> Borrowed<'_, '_, Self::AsRefSource> { - // Safety: &'py Self is expected to be a Python pointer, - // so has the same layout as Borrowed<'py, 'py, T> - Borrowed( - unsafe { NonNull::new_unchecked(ptr_from_ref(self) as *mut _) }, - PhantomData, - self.py(), - ) - } - - /// Returns a GIL marker constrained to the lifetime of this type. - #[inline] - fn py(&self) -> Python<'_> { - unsafe { Python::assume_gil_acquired() } - } - /// Cast `&PyAny` to `&Self` without no type checking. - /// - /// # Safety - /// - /// `obj` must have the same layout as `*const ffi::PyObject` and must be - /// an instance of a type corresponding to `Self`. - unsafe fn unchecked_downcast(obj: &PyAny) -> &Self { - &*(obj.as_ptr() as *const Self) - } -} - /// A GIL-attached equivalent to [`Py`]. /// /// This type can be thought of as equivalent to the tuple `(Py, Python<'py>)`. By having the `'py` @@ -607,37 +557,6 @@ impl<'py, T> Bound<'py, T> { pub fn as_unbound(&self) -> &Py { &self.1 } - - /// Casts this `Bound` as the corresponding "GIL Ref" type. - /// - /// This is a helper to be used for migration from the deprecated "GIL Refs" API. - #[inline] - #[cfg(feature = "gil-refs")] - pub fn as_gil_ref(&'py self) -> &'py T::AsRefTarget - where - T: HasPyGilRef, - { - #[allow(deprecated)] - unsafe { - self.py().from_borrowed_ptr(self.as_ptr()) - } - } - - /// Casts this `Bound` as the corresponding "GIL Ref" type, registering the pointer on the - /// [release pool](Python::from_owned_ptr). - /// - /// This is a helper to be used for migration from the deprecated "GIL Refs" API. - #[inline] - #[cfg(feature = "gil-refs")] - pub fn into_gil_ref(self) -> &'py T::AsRefTarget - where - T: HasPyGilRef, - { - #[allow(deprecated)] - unsafe { - self.py().from_owned_ptr(self.into_ptr()) - } - } } unsafe impl AsPyPointer for Bound<'_, T> { @@ -1060,118 +979,6 @@ where } } -#[cfg(feature = "gil-refs")] -impl Py -where - T: HasPyGilRef, -{ - /// Borrows a GIL-bound reference to the contained `T`. - /// - /// By binding to the GIL lifetime, this allows the GIL-bound reference to not require - /// [`Python<'py>`](crate::Python) for any of its methods, which makes calling methods - /// on it more ergonomic. - /// - /// For native types, this reference is `&T`. For pyclasses, this is `&PyCell`. - /// - /// Note that the lifetime of the returned reference is the shortest of `&self` and - /// [`Python<'py>`](crate::Python). - /// Consider using [`Py::into_ref`] instead if this poses a problem. - /// - /// # Examples - /// - /// Get access to `&PyList` from `Py`: - /// - /// ```ignore - /// # use pyo3::prelude::*; - /// # use pyo3::types::PyList; - /// # - /// Python::with_gil(|py| { - /// let list: Py = PyList::empty_bound(py).into(); - /// # #[allow(deprecated)] - /// let list: &PyList = list.as_ref(py); - /// assert_eq!(list.len(), 0); - /// }); - /// ``` - /// - /// Get access to `&PyCell` from `Py`: - /// - /// ``` - /// # use pyo3::prelude::*; - /// # - /// #[pyclass] - /// struct MyClass {} - /// - /// Python::with_gil(|py| { - /// let my_class: Py = Py::new(py, MyClass {}).unwrap(); - /// # #[allow(deprecated)] - /// let my_class_cell: &PyCell = my_class.as_ref(py); - /// assert!(my_class_cell.try_borrow().is_ok()); - /// }); - /// ``` - #[deprecated( - since = "0.21.0", - note = "use `obj.bind(py)` instead of `obj.as_ref(py)`" - )] - pub fn as_ref<'py>(&'py self, _py: Python<'py>) -> &'py T::AsRefTarget { - let any = self.as_ptr() as *const PyAny; - unsafe { PyNativeType::unchecked_downcast(&*any) } - } - - /// Borrows a GIL-bound reference to the contained `T` independently of the lifetime of `T`. - /// - /// This method is similar to [`as_ref`](#method.as_ref) but consumes `self` and registers the - /// Python object reference in PyO3's object storage. The reference count for the Python - /// object will not be decreased until the GIL lifetime ends. - /// - /// You should prefer using [`as_ref`](#method.as_ref) if you can as it'll have less overhead. - /// - /// # Examples - /// - /// [`Py::as_ref`]'s lifetime limitation forbids creating a function that references a - /// variable created inside the function. - /// - /// ```rust,compile_fail - /// # use pyo3::prelude::*; - /// # - /// fn new_py_any<'py>(py: Python<'py>, value: impl IntoPy>) -> &'py PyAny { - /// let obj: Py = value.into_py(py); - /// - /// // The lifetime of the return value of this function is the shortest - /// // of `obj` and `py`. As `obj` is owned by the current function, - /// // Rust won't let the return value escape this function! - /// obj.as_ref(py) - /// } - /// ``` - /// - /// This can be solved by using [`Py::into_ref`] instead, which does not suffer from this issue. - /// Note that the lifetime of the [`Python<'py>`](crate::Python) token is transferred to - /// the returned reference. - /// - /// ```rust - /// # use pyo3::prelude::*; - /// # #[allow(dead_code)] // This is just to show it compiles. - /// fn new_py_any<'py>(py: Python<'py>, value: impl IntoPy>) -> &'py PyAny { - /// let obj: Py = value.into_py(py); - /// - /// // This reference's lifetime is determined by `py`'s lifetime. - /// // Because that originates from outside this function, - /// // this return value is allowed. - /// # #[allow(deprecated)] - /// obj.into_ref(py) - /// } - /// ``` - #[deprecated( - since = "0.21.0", - note = "use `obj.into_bound(py)` instead of `obj.into_ref(py)`" - )] - pub fn into_ref(self, py: Python<'_>) -> &T::AsRefTarget { - #[allow(deprecated)] - unsafe { - py.from_owned_ptr(self.into_ptr()) - } - } -} - impl Py { /// Returns the raw FFI pointer represented by self. /// @@ -1558,20 +1365,6 @@ impl Py { .setattr(attr_name, value.into_py(py).into_bound(py)) } - /// Deprecated form of [`call_bound`][Py::call_bound]. - #[cfg(feature = "gil-refs")] - #[deprecated( - since = "0.21.0", - note = "`call` will be replaced by `call_bound` in a future PyO3 version" - )] - #[inline] - pub fn call
(&self, py: Python<'_>, args: A, kwargs: Option<&PyDict>) -> PyResult - where - A: IntoPy>, - { - self.call_bound(py, args, kwargs.map(PyDict::as_borrowed).as_deref()) - } - /// Calls the object. /// /// This is equivalent to the Python expression `self(*args, **kwargs)`. @@ -1598,27 +1391,6 @@ impl Py { self.bind(py).as_any().call0().map(Bound::unbind) } - /// Deprecated form of [`call_method_bound`][Py::call_method_bound]. - #[cfg(feature = "gil-refs")] - #[deprecated( - since = "0.21.0", - note = "`call_method` will be replaced by `call_method_bound` in a future PyO3 version" - )] - #[inline] - pub fn call_method( - &self, - py: Python<'_>, - name: N, - args: A, - kwargs: Option<&PyDict>, - ) -> PyResult - where - N: IntoPy>, - A: IntoPy>, - { - self.call_method_bound(py, name, args, kwargs.map(PyDict::as_borrowed).as_deref()) - } - /// Calls a method on the object. /// /// This is equivalent to the Python expression `self.name(*args, **kwargs)`. @@ -1831,17 +1603,6 @@ unsafe impl crate::AsPyPointer for Py { } } -#[cfg(feature = "gil-refs")] -impl std::convert::From<&'_ T> for PyObject -where - T: PyNativeType, -{ - #[inline] - fn from(obj: &T) -> Self { - obj.as_borrowed().to_owned().into_any().unbind() - } -} - impl std::convert::From> for PyObject where T: AsRef, @@ -1870,18 +1631,6 @@ impl std::convert::From> for Py { } } -// `&PyCell` can be converted to `Py` -#[cfg(feature = "gil-refs")] -#[allow(deprecated)] -impl std::convert::From<&crate::PyCell> for Py -where - T: PyClass, -{ - fn from(cell: &crate::PyCell) -> Self { - cell.as_borrowed().to_owned().unbind() - } -} - impl<'a, T> std::convert::From> for Py where T: PyClass, @@ -1952,18 +1701,6 @@ where } } -/// `Py` can be used as an error when T is an Error. -/// -/// However for GIL lifetime reasons, cause() cannot be implemented for `Py`. -/// Use .as_ref() to get the GIL-scoped error if you need to inspect the cause. -#[cfg(feature = "gil-refs")] -impl std::error::Error for Py -where - T: std::error::Error + PyTypeInfo, - T::AsRefTarget: std::fmt::Display, -{ -} - impl std::fmt::Display for Py where T: PyTypeInfo, @@ -2049,24 +1786,6 @@ impl PyObject { self.bind(py).downcast() } - /// Deprecated form of [`PyObject::downcast_bound_unchecked`] - /// - /// # Safety - /// - /// Callers must ensure that the type is valid or risk type confusion. - #[cfg(feature = "gil-refs")] - #[deprecated( - since = "0.21.0", - note = "`PyObject::downcast_unchecked` will be replaced by `PyObject::downcast_bound_unchecked` in a future PyO3 version" - )] - #[inline] - pub unsafe fn downcast_unchecked<'py, T>(&'py self, py: Python<'py>) -> &T - where - T: HasPyGilRef, - { - self.downcast_bound_unchecked::(py).as_gil_ref() - } - /// Casts the PyObject to a concrete Python object type without checking validity. /// /// # Safety diff --git a/src/lib.rs b/src/lib.rs index f0854d547b8..fdba3f842e6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -318,18 +318,10 @@ pub use crate::class::*; pub use crate::conversion::{AsPyPointer, FromPyObject, IntoPy, ToPyObject}; pub use crate::err::{DowncastError, DowncastIntoError, PyErr, PyErrArguments, PyResult, ToPyErr}; -#[cfg(feature = "gil-refs")] -#[allow(deprecated)] -pub use crate::gil::GILPool; #[cfg(not(any(PyPy, GraalPy)))] pub use crate::gil::{prepare_freethreaded_python, with_embedded_python_interpreter}; -#[cfg(feature = "gil-refs")] -pub use crate::instance::PyNativeType; pub use crate::instance::{Borrowed, Bound, Py, PyObject}; pub use crate::marker::Python; -#[cfg(feature = "gil-refs")] -#[allow(deprecated)] -pub use crate::pycell::PyCell; pub use crate::pycell::{PyRef, PyRefMut}; pub use crate::pyclass::PyClass; pub use crate::pyclass_init::PyClassInitializer; @@ -502,7 +494,6 @@ pub mod doc_test { "guide/src/function.md" => guide_function_md, "guide/src/function/error-handling.md" => guide_function_error_handling_md, "guide/src/function/signature.md" => guide_function_signature_md, - "guide/src/memory.md" => guide_memory_md, "guide/src/migration.md" => guide_migration_md, "guide/src/module.md" => guide_module_md, "guide/src/parallelism.md" => guide_parallelism_md, diff --git a/src/macros.rs b/src/macros.rs index 4de52185eda..79b65c17b45 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -105,9 +105,7 @@ macro_rules! py_run_impl { ($py:expr, *$dict:expr, $code:expr) => {{ use ::std::option::Option::*; #[allow(unused_imports)] - #[cfg(feature = "gil-refs")] - use $crate::PyNativeType; - if let ::std::result::Result::Err(e) = $py.run_bound($code, None, Some(&$dict.as_borrowed())) { + if let ::std::result::Result::Err(e) = $py.run_bound($code, None, Some(&$dict)) { e.print($py); // So when this c api function the last line called printed the error to stderr, // the output is only written into a buffer which is never flushed because we diff --git a/src/marker.rs b/src/marker.rs index 58843cf29aa..bc0e22e37e7 100644 --- a/src/marker.rs +++ b/src/marker.rs @@ -126,9 +126,6 @@ use crate::types::{ PyAny, PyDict, PyEllipsis, PyModule, PyNone, PyNotImplemented, PyString, PyType, }; use crate::version::PythonVersionInfo; -#[allow(deprecated)] -#[cfg(feature = "gil-refs")] -use crate::{conversion::FromPyPointer, gil::GILPool, PyNativeType}; use crate::{ffi, Bound, IntoPy, Py, PyObject, PyTypeInfo}; use std::ffi::{CStr, CString}; use std::marker::PhantomData; @@ -279,10 +276,6 @@ mod nightly { // This means that PyString, PyList, etc all inherit !Ungil from this. impl !Ungil for crate::PyAny {} - // All the borrowing wrappers - #[allow(deprecated)] - #[cfg(feature = "gil-refs")] - impl !Ungil for crate::PyCell {} impl !Ungil for crate::PyRef<'_, T> {} impl !Ungil for crate::PyRefMut<'_, T> {} @@ -522,26 +515,6 @@ impl<'py> Python<'py> { f() } - /// Deprecated version of [`Python::eval_bound`] - #[cfg(feature = "gil-refs")] - #[deprecated( - since = "0.21.0", - note = "`Python::eval` will be replaced by `Python::eval_bound` in a future PyO3 version" - )] - pub fn eval( - self, - code: &str, - globals: Option<&'py PyDict>, - locals: Option<&'py PyDict>, - ) -> PyResult<&'py PyAny> { - self.eval_bound( - code, - globals.map(PyNativeType::as_borrowed).as_deref(), - locals.map(PyNativeType::as_borrowed).as_deref(), - ) - .map(Bound::into_gil_ref) - } - /// Evaluates a Python expression in the given context and returns the result. /// /// If `globals` is `None`, it defaults to Python module `__main__`. @@ -569,25 +542,6 @@ impl<'py> Python<'py> { self.run_code(code, ffi::Py_eval_input, globals, locals) } - /// Deprecated version of [`Python::run_bound`] - #[cfg(feature = "gil-refs")] - #[deprecated( - since = "0.21.0", - note = "`Python::run` will be replaced by `Python::run_bound` in a future PyO3 version" - )] - pub fn run( - self, - code: &str, - globals: Option<&PyDict>, - locals: Option<&PyDict>, - ) -> PyResult<()> { - self.run_bound( - code, - globals.map(PyNativeType::as_borrowed).as_deref(), - locals.map(PyNativeType::as_borrowed).as_deref(), - ) - } - /// Executes one or more Python statements in the given context. /// /// If `globals` is `None`, it defaults to Python module `__main__`. @@ -695,20 +649,6 @@ impl<'py> Python<'py> { } } - /// Gets the Python type object for type `T`. - #[cfg(feature = "gil-refs")] - #[deprecated( - since = "0.21.0", - note = "`Python::get_type` will be replaced by `Python::get_type_bound` in a future PyO3 version" - )] - #[inline] - pub fn get_type(self) -> &'py PyType - where - T: PyTypeInfo, - { - self.get_type_bound::().into_gil_ref() - } - /// Gets the Python type object for type `T`. #[inline] pub fn get_type_bound(self) -> Bound<'py, PyType> @@ -718,19 +658,6 @@ impl<'py> Python<'py> { T::type_object_bound(self) } - /// Deprecated form of [`Python::import_bound`] - #[cfg(feature = "gil-refs")] - #[deprecated( - since = "0.21.0", - note = "`Python::import` will be replaced by `Python::import_bound` in a future PyO3 version" - )] - pub fn import(self, name: N) -> PyResult<&'py PyModule> - where - N: IntoPy>, - { - Self::import_bound(self, name).map(Bound::into_gil_ref) - } - /// Imports the Python module with the specified name. pub fn import_bound(self, name: N) -> PyResult> where @@ -801,127 +728,6 @@ impl<'py> Python<'py> { PythonVersionInfo::from_str(version_number_str).unwrap() } - /// Registers the object pointer in the release pool, - /// and does an unchecked downcast to the specific type. - /// - /// # Safety - /// - /// Callers must ensure that ensure that the cast is valid. - #[allow(clippy::wrong_self_convention, deprecated)] - #[cfg(feature = "gil-refs")] - #[deprecated( - since = "0.21.0", - note = "use `Py::from_owned_ptr(py, ptr)` or `Bound::from_owned_ptr(py, ptr)` instead" - )] - pub unsafe fn from_owned_ptr(self, ptr: *mut ffi::PyObject) -> &'py T - where - T: FromPyPointer<'py>, - { - FromPyPointer::from_owned_ptr(self, ptr) - } - - /// Registers the owned object pointer in the release pool. - /// - /// Returns `Err(PyErr)` if the pointer is NULL. - /// Does an unchecked downcast to the specific type. - /// - /// # Safety - /// - /// Callers must ensure that ensure that the cast is valid. - #[allow(clippy::wrong_self_convention, deprecated)] - #[cfg(feature = "gil-refs")] - #[deprecated( - since = "0.21.0", - note = "use `Py::from_owned_ptr_or_err(py, ptr)` or `Bound::from_owned_ptr_or_err(py, ptr)` instead" - )] - pub unsafe fn from_owned_ptr_or_err(self, ptr: *mut ffi::PyObject) -> PyResult<&'py T> - where - T: FromPyPointer<'py>, - { - FromPyPointer::from_owned_ptr_or_err(self, ptr) - } - - /// Registers the owned object pointer in release pool. - /// - /// Returns `None` if the pointer is NULL. - /// Does an unchecked downcast to the specific type. - /// - /// # Safety - /// - /// Callers must ensure that ensure that the cast is valid. - #[allow(clippy::wrong_self_convention, deprecated)] - #[cfg(feature = "gil-refs")] - #[deprecated( - since = "0.21.0", - note = "use `Py::from_owned_ptr_or_opt(py, ptr)` or `Bound::from_owned_ptr_or_opt(py, ptr)` instead" - )] - pub unsafe fn from_owned_ptr_or_opt(self, ptr: *mut ffi::PyObject) -> Option<&'py T> - where - T: FromPyPointer<'py>, - { - FromPyPointer::from_owned_ptr_or_opt(self, ptr) - } - - /// Does an unchecked downcast to the specific type. - /// - /// Panics if the pointer is NULL. - /// - /// # Safety - /// - /// Callers must ensure that ensure that the cast is valid. - #[allow(clippy::wrong_self_convention, deprecated)] - #[cfg(feature = "gil-refs")] - #[deprecated( - since = "0.21.0", - note = "use `Py::from_borrowed_ptr(py, ptr)` or `Bound::from_borrowed_ptr(py, ptr)` instead" - )] - pub unsafe fn from_borrowed_ptr(self, ptr: *mut ffi::PyObject) -> &'py T - where - T: FromPyPointer<'py>, - { - FromPyPointer::from_borrowed_ptr(self, ptr) - } - - /// Does an unchecked downcast to the specific type. - /// - /// Returns `Err(PyErr)` if the pointer is NULL. - /// - /// # Safety - /// - /// Callers must ensure that ensure that the cast is valid. - #[allow(clippy::wrong_self_convention, deprecated)] - #[cfg(feature = "gil-refs")] - #[deprecated( - since = "0.21.0", - note = "use `Py::from_borrowed_ptr_or_err(py, ptr)` or `Bound::from_borrowed_ptr_or_err(py, ptr)` instead" - )] - pub unsafe fn from_borrowed_ptr_or_err(self, ptr: *mut ffi::PyObject) -> PyResult<&'py T> - where - T: FromPyPointer<'py>, - { - FromPyPointer::from_borrowed_ptr_or_err(self, ptr) - } - - /// Does an unchecked downcast to the specific type. - /// - /// Returns `None` if the pointer is NULL. - /// - /// # Safety - /// - /// Callers must ensure that ensure that the cast is valid. - #[allow(clippy::wrong_self_convention, deprecated)] - #[cfg(feature = "gil-refs")] - #[deprecated( - since = "0.21.0", - note = "use `Py::from_borrowed_ptr_or_opt(py, ptr)` or `Bound::from_borrowed_ptr_or_opt(py, ptr)` instead" - )] - pub unsafe fn from_borrowed_ptr_or_opt(self, ptr: *mut ffi::PyObject) -> Option<&'py T> - where - T: FromPyPointer<'py>, - { - FromPyPointer::from_borrowed_ptr_or_opt(self, ptr) - } - /// Lets the Python interpreter check and handle any pending signals. This will invoke the /// corresponding signal handlers registered in Python (if any). /// @@ -967,148 +773,6 @@ impl<'py> Python<'py> { pub fn check_signals(self) -> PyResult<()> { err::error_on_minusone(self, unsafe { ffi::PyErr_CheckSignals() }) } - - /// Create a new pool for managing PyO3's GIL Refs. This has no functional - /// use for code which does not use the deprecated GIL Refs API. - /// - /// When this `GILPool` is dropped, all GIL Refs created after this `GILPool` will - /// all have their Python reference counts decremented, potentially allowing Python to drop - /// the corresponding Python objects. - /// - /// Typical usage of PyO3 will not need this API, as [`Python::with_gil`] automatically creates - /// a `GILPool` where appropriate. - /// - /// Advanced uses of PyO3 which perform long-running tasks which never free the GIL may need - /// to use this API to clear memory, as PyO3 usually does not clear memory until the GIL is - /// released. - /// - /// # Examples - /// - /// ```rust - /// # use pyo3::prelude::*; - /// Python::with_gil(|py| { - /// // Some long-running process like a webserver, which never releases the GIL. - /// loop { - /// // Create a new pool, so that PyO3 can clear memory at the end of the loop. - /// #[allow(deprecated)] // `new_pool` is not needed in code not using the GIL Refs API - /// let pool = unsafe { py.new_pool() }; - /// - /// // It is recommended to *always* immediately set py to the pool's Python, to help - /// // avoid creating references with invalid lifetimes. - /// let py = pool.python(); - /// - /// // do stuff... - /// # break; // Exit the loop so that doctest terminates! - /// } - /// }); - /// ``` - /// - /// # Safety - /// - /// Extreme care must be taken when using this API, as misuse can lead to accessing invalid - /// memory. In addition, the caller is responsible for guaranteeing that the GIL remains held - /// for the entire lifetime of the returned `GILPool`. - /// - /// Two best practices are required when using this API: - /// - From the moment `new_pool()` is called, only the `Python` token from the returned - /// `GILPool` (accessible using [`.python()`]) should be used in PyO3 APIs. All other older - /// `Python` tokens with longer lifetimes are unsafe to use until the `GILPool` is dropped, - /// because they can be used to create PyO3 owned references which have lifetimes which - /// outlive the `GILPool`. - /// - Similarly, methods on existing owned references will implicitly refer back to the - /// `Python` token which that reference was originally created with. If the returned values - /// from these methods are owned references they will inherit the same lifetime. As a result, - /// Rust's lifetime rules may allow them to outlive the `GILPool`, even though this is not - /// safe for reasons discussed above. Care must be taken to never access these return values - /// after the `GILPool` is dropped, unless they are converted to `Py` *before* the pool - /// is dropped. - /// - /// [`.python()`]: crate::GILPool::python - #[inline] - #[cfg(feature = "gil-refs")] - #[deprecated( - since = "0.21.0", - note = "code not using the GIL Refs API can safely remove use of `Python::new_pool`" - )] - #[allow(deprecated)] - pub unsafe fn new_pool(self) -> GILPool { - GILPool::new() - } -} - -impl Python<'_> { - /// Creates a scope using a new pool for managing PyO3's GIL Refs. This has no functional - /// use for code which does not use the deprecated GIL Refs API. - /// - /// This is a safe alterantive to [`new_pool`][Self::new_pool] as - /// it limits the closure to using the new GIL token at the cost of - /// being unable to capture existing GIL-bound references. - /// - /// Note that on stable Rust, this API suffers from the same the `SendWrapper` loophole - /// as [`allow_threads`][Self::allow_threads], c.f. the documentation of the [`Ungil`] trait, - /// - /// # Examples - /// - /// ```rust - /// # use pyo3::prelude::*; - /// Python::with_gil(|py| { - /// // Some long-running process like a webserver, which never releases the GIL. - /// loop { - /// // Create a new scope, so that PyO3 can clear memory at the end of the loop. - /// #[allow(deprecated)] // `with_pool` is not needed in code not using the GIL Refs API - /// py.with_pool(|py| { - /// // do stuff... - /// }); - /// # break; // Exit the loop so that doctest terminates! - /// } - /// }); - /// ``` - /// - /// The `Ungil` bound on the closure does prevent hanging on to existing GIL-bound references - /// - /// ```compile_fail - /// # #![allow(deprecated)] - /// # use pyo3::prelude::*; - /// # use pyo3::types::PyString; - /// - /// Python::with_gil(|py| { - /// let old_str = PyString::new(py, "a message from the past"); - /// - /// py.with_pool(|_py| { - /// print!("{:?}", old_str); - /// }); - /// }); - /// ``` - /// - /// or continuing to use the old GIL token - /// - /// ```compile_fail - /// # use pyo3::prelude::*; - /// - /// Python::with_gil(|old_py| { - /// old_py.with_pool(|_new_py| { - /// let _none = old_py.None(); - /// }); - /// }); - /// ``` - #[inline] - #[cfg(feature = "gil-refs")] - #[deprecated( - since = "0.21.0", - note = "code not using the GIL Refs API can safely remove use of `Python::with_pool`" - )] - #[allow(deprecated)] - pub fn with_pool(&self, f: F) -> R - where - F: for<'py> FnOnce(Python<'py>) -> R + Ungil, - { - // SAFETY: The closure is `Ungil`, - // i.e. it does not capture any GIL-bound references - // and accesses only the newly created GIL token. - let pool = unsafe { GILPool::new() }; - - f(pool.python()) - } } impl<'unbound> Python<'unbound> { diff --git a/src/prelude.rs b/src/prelude.rs index eac04d0e048..6182b21c2d1 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -12,14 +12,9 @@ pub use crate::conversion::{FromPyObject, IntoPy, ToPyObject}; pub use crate::err::{PyErr, PyResult}; pub use crate::instance::{Borrowed, Bound, Py, PyObject}; pub use crate::marker::Python; -#[cfg(feature = "gil-refs")] -#[allow(deprecated)] -pub use crate::pycell::PyCell; pub use crate::pycell::{PyRef, PyRefMut}; pub use crate::pyclass_init::PyClassInitializer; pub use crate::types::{PyAny, PyModule}; -#[cfg(feature = "gil-refs")] -pub use crate::PyNativeType; #[cfg(feature = "macros")] pub use pyo3_macros::{pyclass, pyfunction, pymethods, pymodule, FromPyObject}; diff --git a/src/pycell.rs b/src/pycell.rs index 7dadb8361d5..6b501ad0401 100644 --- a/src/pycell.rs +++ b/src/pycell.rs @@ -199,376 +199,14 @@ use crate::ffi_ptr_ext::FfiPtrExt; use crate::internal_tricks::{ptr_from_mut, ptr_from_ref}; use crate::pyclass::{boolean_struct::False, PyClass}; use crate::types::any::PyAnyMethods; -#[cfg(feature = "gil-refs")] -use crate::{ - conversion::ToPyObject, - impl_::pyclass::PyClassImpl, - pyclass::boolean_struct::True, - pyclass_init::PyClassInitializer, - type_object::{PyLayout, PySizedLayout}, - types::PyAny, - PyNativeType, PyResult, PyTypeCheck, -}; use crate::{ffi, Bound, IntoPy, PyErr, PyObject, Python}; use std::fmt; use std::mem::ManuallyDrop; use std::ops::{Deref, DerefMut}; pub(crate) mod impl_; -#[cfg(feature = "gil-refs")] -use self::impl_::PyClassObject; use impl_::{PyClassBorrowChecker, PyClassObjectLayout}; -/// A container type for (mutably) accessing [`PyClass`] values -/// -/// `PyCell` autodereferences to [`PyAny`], so you can call `PyAny`'s methods on a `PyCell`. -/// -/// # Examples -/// -/// This example demonstrates getting a mutable reference of the contained `PyClass`. -/// ```rust -/// use pyo3::prelude::*; -/// -/// #[pyclass] -/// struct Number { -/// inner: u32, -/// } -/// -/// #[pymethods] -/// impl Number { -/// fn increment(&mut self) { -/// self.inner += 1; -/// } -/// } -/// -/// # fn main() -> PyResult<()> { -/// Python::with_gil(|py| { -/// # #[allow(deprecated)] -/// let n = PyCell::new(py, Number { inner: 0 })?; -/// -/// let n_mutable: &mut Number = &mut n.borrow_mut(); -/// n_mutable.increment(); -/// -/// Ok(()) -/// }) -/// # } -/// ``` -/// For more information on how, when and why (not) to use `PyCell` please see the -/// [module-level documentation](self). -#[cfg(feature = "gil-refs")] -#[deprecated( - since = "0.21.0", - note = "`PyCell` was merged into `Bound`, use that instead; see the migration guide for more info" -)] -#[repr(transparent)] -pub struct PyCell(PyClassObject); - -#[cfg(feature = "gil-refs")] -#[allow(deprecated)] -unsafe impl PyNativeType for PyCell { - type AsRefSource = T; -} - -#[cfg(feature = "gil-refs")] -#[allow(deprecated)] -impl PyCell { - /// Makes a new `PyCell` on the Python heap and return the reference to it. - /// - /// In cases where the value in the cell does not need to be accessed immediately after - /// creation, consider [`Py::new`](crate::Py::new) as a more efficient alternative. - #[deprecated( - since = "0.21.0", - note = "use `Bound::new(py, value)` or `Py::new(py, value)` instead of `PyCell::new(py, value)`" - )] - pub fn new(py: Python<'_>, value: impl Into>) -> PyResult<&Self> { - Bound::new(py, value).map(Bound::into_gil_ref) - } - - /// Immutably borrows the value `T`. This borrow lasts as long as the returned `PyRef` exists. - /// - /// For frozen classes, the simpler [`get`][Self::get] is available. - /// - /// # Panics - /// - /// Panics if the value is currently mutably borrowed. For a non-panicking variant, use - /// [`try_borrow`](#method.try_borrow). - pub fn borrow(&self) -> PyRef<'_, T> { - PyRef::borrow(&self.as_borrowed()) - } - - /// Mutably borrows the value `T`. This borrow lasts as long as the returned `PyRefMut` exists. - /// - /// # Panics - /// - /// Panics if the value is currently borrowed. For a non-panicking variant, use - /// [`try_borrow_mut`](#method.try_borrow_mut). - pub fn borrow_mut(&self) -> PyRefMut<'_, T> - where - T: PyClass, - { - PyRefMut::borrow(&self.as_borrowed()) - } - - /// Immutably borrows the value `T`, returning an error if the value is currently - /// mutably borrowed. This borrow lasts as long as the returned `PyRef` exists. - /// - /// This is the non-panicking variant of [`borrow`](#method.borrow). - /// - /// For frozen classes, the simpler [`get`][Self::get] is available. - /// - /// # Examples - /// - /// ``` - /// # use pyo3::prelude::*; - /// #[pyclass] - /// struct Class {} - /// - /// Python::with_gil(|py| { - /// # #[allow(deprecated)] - /// let c = PyCell::new(py, Class {}).unwrap(); - /// { - /// let m = c.borrow_mut(); - /// assert!(c.try_borrow().is_err()); - /// } - /// - /// { - /// let m = c.borrow(); - /// assert!(c.try_borrow().is_ok()); - /// } - /// }); - /// ``` - pub fn try_borrow(&self) -> Result, PyBorrowError> { - PyRef::try_borrow(&self.as_borrowed()) - } - - /// Mutably borrows the value `T`, returning an error if the value is currently borrowed. - /// This borrow lasts as long as the returned `PyRefMut` exists. - /// - /// This is the non-panicking variant of [`borrow_mut`](#method.borrow_mut). - /// - /// # Examples - /// - /// ``` - /// # use pyo3::prelude::*; - /// #[pyclass] - /// struct Class {} - /// Python::with_gil(|py| { - /// # #[allow(deprecated)] - /// let c = PyCell::new(py, Class {}).unwrap(); - /// { - /// let m = c.borrow(); - /// assert!(c.try_borrow_mut().is_err()); - /// } - /// - /// assert!(c.try_borrow_mut().is_ok()); - /// }); - /// ``` - pub fn try_borrow_mut(&self) -> Result, PyBorrowMutError> - where - T: PyClass, - { - PyRefMut::try_borrow(&self.as_borrowed()) - } - - /// Immutably borrows the value `T`, returning an error if the value is - /// currently mutably borrowed. - /// - /// # Safety - /// - /// This method is unsafe because it does not return a `PyRef`, - /// thus leaving the borrow flag untouched. Mutably borrowing the `PyCell` - /// while the reference returned by this method is alive is undefined behaviour. - /// - /// # Examples - /// - /// ``` - /// # use pyo3::prelude::*; - /// #[pyclass] - /// struct Class {} - /// Python::with_gil(|py| { - /// # #[allow(deprecated)] - /// let c = PyCell::new(py, Class {}).unwrap(); - /// - /// { - /// let m = c.borrow_mut(); - /// assert!(unsafe { c.try_borrow_unguarded() }.is_err()); - /// } - /// - /// { - /// let m = c.borrow(); - /// assert!(unsafe { c.try_borrow_unguarded() }.is_ok()); - /// } - /// }); - /// ``` - pub unsafe fn try_borrow_unguarded(&self) -> Result<&T, PyBorrowError> { - self.0.ensure_threadsafe(); - self.0 - .borrow_checker() - .try_borrow_unguarded() - .map(|_: ()| &*self.0.get_ptr()) - } - - /// Provide an immutable borrow of the value `T` without acquiring the GIL. - /// - /// This is available if the class is [`frozen`][macro@crate::pyclass] and [`Sync`]. - /// - /// While the GIL is usually required to get access to `&PyCell`, - /// compared to [`borrow`][Self::borrow] or [`try_borrow`][Self::try_borrow] - /// this avoids any thread or borrow checking overhead at runtime. - /// - /// # Examples - /// - /// ``` - /// use std::sync::atomic::{AtomicUsize, Ordering}; - /// # use pyo3::prelude::*; - /// - /// #[pyclass(frozen)] - /// struct FrozenCounter { - /// value: AtomicUsize, - /// } - /// - /// Python::with_gil(|py| { - /// let counter = FrozenCounter { value: AtomicUsize::new(0) }; - /// - /// # #[allow(deprecated)] - /// let cell = PyCell::new(py, counter).unwrap(); - /// - /// cell.get().value.fetch_add(1, Ordering::Relaxed); - /// }); - /// ``` - pub fn get(&self) -> &T - where - T: PyClass + Sync, - { - // SAFETY: The class itself is frozen and `Sync` and we do not access anything but `self.contents.value`. - unsafe { &*self.get_ptr() } - } - - /// Replaces the wrapped value with a new one, returning the old value. - /// - /// # Panics - /// - /// Panics if the value is currently borrowed. - #[inline] - pub fn replace(&self, t: T) -> T - where - T: PyClass, - { - std::mem::replace(&mut *self.borrow_mut(), t) - } - - /// Replaces the wrapped value with a new one computed from `f`, returning the old value. - /// - /// # Panics - /// - /// Panics if the value is currently borrowed. - pub fn replace_with T>(&self, f: F) -> T - where - T: PyClass, - { - let mut_borrow = &mut *self.borrow_mut(); - let replacement = f(mut_borrow); - std::mem::replace(mut_borrow, replacement) - } - - /// Swaps the wrapped value of `self` with the wrapped value of `other`. - /// - /// # Panics - /// - /// Panics if the value in either `PyCell` is currently borrowed. - #[inline] - pub fn swap(&self, other: &Self) - where - T: PyClass, - { - std::mem::swap(&mut *self.borrow_mut(), &mut *other.borrow_mut()) - } - - pub(crate) fn get_ptr(&self) -> *mut T { - self.0.get_ptr() - } -} - -#[cfg(feature = "gil-refs")] -#[allow(deprecated)] -unsafe impl PyLayout for PyCell {} -#[cfg(feature = "gil-refs")] -#[allow(deprecated)] -impl PySizedLayout for PyCell {} - -#[cfg(feature = "gil-refs")] -#[allow(deprecated)] -impl PyTypeCheck for PyCell -where - T: PyClass, -{ - const NAME: &'static str = ::NAME; - - fn type_check(object: &Bound<'_, PyAny>) -> bool { - ::type_check(object) - } -} -#[cfg(feature = "gil-refs")] -#[allow(deprecated)] -unsafe impl AsPyPointer for PyCell { - fn as_ptr(&self) -> *mut ffi::PyObject { - ptr_from_ref(self) as *mut _ - } -} - -#[cfg(feature = "gil-refs")] -#[allow(deprecated)] -impl ToPyObject for &PyCell { - fn to_object(&self, py: Python<'_>) -> PyObject { - unsafe { PyObject::from_borrowed_ptr(py, self.as_ptr()) } - } -} - -#[cfg(feature = "gil-refs")] -#[allow(deprecated)] -impl AsRef for PyCell { - fn as_ref(&self) -> &PyAny { - #[allow(deprecated)] - unsafe { - self.py().from_borrowed_ptr(self.as_ptr()) - } - } -} - -#[cfg(feature = "gil-refs")] -#[allow(deprecated)] -impl Deref for PyCell { - type Target = PyAny; - - fn deref(&self) -> &PyAny { - #[allow(deprecated)] - unsafe { - self.py().from_borrowed_ptr(self.as_ptr()) - } - } -} - -#[cfg(feature = "gil-refs")] -#[allow(deprecated)] -impl fmt::Debug for PyCell { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self.try_borrow() { - Ok(borrow) => f.debug_struct("RefCell").field("value", &borrow).finish(), - Err(_) => { - struct BorrowedPlaceholder; - impl fmt::Debug for BorrowedPlaceholder { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str("") - } - } - f.debug_struct("RefCell") - .field("value", &BorrowedPlaceholder) - .finish() - } - } - } -} - /// A wrapper type for an immutably borrowed value from a [`Bound<'py, T>`]. /// /// See the [`Bound`] documentation for more information. @@ -828,15 +466,6 @@ impl IntoPy for &'_ PyRef<'_, T> { } } -#[cfg(feature = "gil-refs")] -#[allow(deprecated)] -impl<'a, T: PyClass> std::convert::TryFrom<&'a PyCell> for crate::PyRef<'a, T> { - type Error = PyBorrowError; - fn try_from(cell: &'a crate::PyCell) -> Result { - cell.try_borrow() - } -} - unsafe impl<'a, T: PyClass> AsPyPointer for PyRef<'a, T> { fn as_ptr(&self) -> *mut ffi::PyObject { self.inner.as_ptr() @@ -1012,17 +641,6 @@ unsafe impl<'a, T: PyClass> AsPyPointer for PyRefMut<'a, T> { } } -#[cfg(feature = "gil-refs")] -#[allow(deprecated)] -impl<'a, T: PyClass> std::convert::TryFrom<&'a PyCell> - for crate::PyRefMut<'a, T> -{ - type Error = PyBorrowMutError; - fn try_from(cell: &'a crate::PyCell) -> Result { - cell.try_borrow_mut() - } -} - impl + fmt::Debug> fmt::Debug for PyRefMut<'_, T> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fmt::Debug::fmt(self.deref(), f) @@ -1090,108 +708,6 @@ mod tests { #[derive(Copy, Clone, PartialEq, Eq, Debug)] struct SomeClass(i32); - #[cfg(feature = "gil-refs")] - mod deprecated { - use super::*; - - #[test] - fn pycell_replace() { - Python::with_gil(|py| { - #[allow(deprecated)] - let cell = PyCell::new(py, SomeClass(0)).unwrap(); - assert_eq!(*cell.borrow(), SomeClass(0)); - - let previous = cell.replace(SomeClass(123)); - assert_eq!(previous, SomeClass(0)); - assert_eq!(*cell.borrow(), SomeClass(123)); - }) - } - - #[test] - #[should_panic(expected = "Already borrowed: PyBorrowMutError")] - fn pycell_replace_panic() { - Python::with_gil(|py| { - #[allow(deprecated)] - let cell = PyCell::new(py, SomeClass(0)).unwrap(); - let _guard = cell.borrow(); - - cell.replace(SomeClass(123)); - }) - } - - #[test] - fn pycell_replace_with() { - Python::with_gil(|py| { - #[allow(deprecated)] - let cell = PyCell::new(py, SomeClass(0)).unwrap(); - assert_eq!(*cell.borrow(), SomeClass(0)); - - let previous = cell.replace_with(|value| { - *value = SomeClass(2); - SomeClass(123) - }); - assert_eq!(previous, SomeClass(2)); - assert_eq!(*cell.borrow(), SomeClass(123)); - }) - } - - #[test] - #[should_panic(expected = "Already borrowed: PyBorrowMutError")] - fn pycell_replace_with_panic() { - Python::with_gil(|py| { - #[allow(deprecated)] - let cell = PyCell::new(py, SomeClass(0)).unwrap(); - let _guard = cell.borrow(); - - cell.replace_with(|_| SomeClass(123)); - }) - } - - #[test] - fn pycell_swap() { - Python::with_gil(|py| { - #[allow(deprecated)] - let cell = PyCell::new(py, SomeClass(0)).unwrap(); - #[allow(deprecated)] - let cell2 = PyCell::new(py, SomeClass(123)).unwrap(); - assert_eq!(*cell.borrow(), SomeClass(0)); - assert_eq!(*cell2.borrow(), SomeClass(123)); - - cell.swap(cell2); - assert_eq!(*cell.borrow(), SomeClass(123)); - assert_eq!(*cell2.borrow(), SomeClass(0)); - }) - } - - #[test] - #[should_panic(expected = "Already borrowed: PyBorrowMutError")] - fn pycell_swap_panic() { - Python::with_gil(|py| { - #[allow(deprecated)] - let cell = PyCell::new(py, SomeClass(0)).unwrap(); - #[allow(deprecated)] - let cell2 = PyCell::new(py, SomeClass(123)).unwrap(); - - let _guard = cell.borrow(); - cell.swap(cell2); - }) - } - - #[test] - #[should_panic(expected = "Already borrowed: PyBorrowMutError")] - fn pycell_swap_panic_other_borrowed() { - Python::with_gil(|py| { - #[allow(deprecated)] - let cell = PyCell::new(py, SomeClass(0)).unwrap(); - #[allow(deprecated)] - let cell2 = PyCell::new(py, SomeClass(123)).unwrap(); - - let _guard = cell2.borrow(); - cell.swap(cell2); - }) - } - } - #[test] fn test_as_ptr() { Python::with_gil(|py| { diff --git a/src/pycell/impl_.rs b/src/pycell/impl_.rs index efc057e74f7..1b5ce774379 100644 --- a/src/pycell/impl_.rs +++ b/src/pycell/impl_.rs @@ -74,9 +74,6 @@ pub trait PyClassBorrowChecker { /// Increments immutable borrow count, if possible fn try_borrow(&self) -> Result<(), PyBorrowError>; - #[cfg(feature = "gil-refs")] - fn try_borrow_unguarded(&self) -> Result<(), PyBorrowError>; - /// Decrements immutable borrow count fn release_borrow(&self); /// Increments mutable borrow count, if possible @@ -96,12 +93,6 @@ impl PyClassBorrowChecker for EmptySlot { Ok(()) } - #[inline] - #[cfg(feature = "gil-refs")] - fn try_borrow_unguarded(&self) -> Result<(), PyBorrowError> { - Ok(()) - } - #[inline] fn release_borrow(&self) {} @@ -132,16 +123,6 @@ impl PyClassBorrowChecker for BorrowChecker { } } - #[cfg(feature = "gil-refs")] - fn try_borrow_unguarded(&self) -> Result<(), PyBorrowError> { - let flag = self.0.get(); - if flag != BorrowFlag::HAS_MUTABLE_BORROW { - Ok(()) - } else { - Err(PyBorrowError { _private: () }) - } - } - fn release_borrow(&self) { let flag = self.0.get(); self.0.set(flag.decrement()) diff --git a/src/pyclass.rs b/src/pyclass.rs index 91510e11151..4e409566af5 100644 --- a/src/pyclass.rs +++ b/src/pyclass.rs @@ -12,20 +12,6 @@ pub use self::gc::{PyTraverseError, PyVisit}; /// /// The `#[pyclass]` attribute implements this trait for your Rust struct - /// you shouldn't implement this trait directly. -#[allow(deprecated)] -#[cfg(feature = "gil-refs")] -pub trait PyClass: PyTypeInfo> + PyClassImpl { - /// Whether the pyclass is frozen. - /// - /// This can be enabled via `#[pyclass(frozen)]`. - type Frozen: Frozen; -} - -/// Types that can be used as Python classes. -/// -/// The `#[pyclass]` attribute implements this trait for your Rust struct - -/// you shouldn't implement this trait directly. -#[cfg(not(feature = "gil-refs"))] pub trait PyClass: PyTypeInfo + PyClassImpl { /// Whether the pyclass is frozen. /// diff --git a/src/type_object.rs b/src/type_object.rs index 871e8366865..ed3cbf52de4 100644 --- a/src/type_object.rs +++ b/src/type_object.rs @@ -3,8 +3,6 @@ use crate::ffi_ptr_ext::FfiPtrExt; use crate::types::any::PyAnyMethods; use crate::types::{PyAny, PyType}; -#[cfg(feature = "gil-refs")] -use crate::PyNativeType; use crate::{ffi, Bound, Python}; /// `T: PyLayout` represents that `T` is a concrete representation of `U` in the Python heap. @@ -23,120 +21,6 @@ pub unsafe trait PyLayout {} /// In addition, that `T` is a concrete representation of `U`. pub trait PySizedLayout: PyLayout + Sized {} -/// Specifies that this type has a "GIL-bound Reference" form. -/// -/// This is expected to be deprecated in the near future, see -/// -/// # Safety -/// -/// - `Py::as_ref` will hand out references to `Self::AsRefTarget`. -/// - `Self::AsRefTarget` must have the same layout as `UnsafeCell`. -#[cfg(feature = "gil-refs")] -pub unsafe trait HasPyGilRef { - /// Utility type to make Py::as_ref work. - type AsRefTarget: PyNativeType; -} - -#[cfg(feature = "gil-refs")] -unsafe impl HasPyGilRef for T -where - T: PyNativeType, -{ - type AsRefTarget = Self; -} - -/// Python type information. -/// All Python native types (e.g., `PyDict`) and `#[pyclass]` structs implement this trait. -/// -/// This trait is marked unsafe because: -/// - specifying the incorrect layout can lead to memory errors -/// - the return value of type_object must always point to the same PyTypeObject instance -/// -/// It is safely implemented by the `pyclass` macro. -/// -/// # Safety -/// -/// Implementations must provide an implementation for `type_object_raw` which infallibly produces a -/// non-null pointer to the corresponding Python type object. -#[cfg(feature = "gil-refs")] -pub unsafe trait PyTypeInfo: Sized + HasPyGilRef { - /// Class name. - const NAME: &'static str; - - /// Module name, if any. - const MODULE: Option<&'static str>; - - /// Returns the PyTypeObject instance for this type. - fn type_object_raw(py: Python<'_>) -> *mut ffi::PyTypeObject; - - /// Returns the safe abstraction over the type object. - #[inline] - #[cfg(feature = "gil-refs")] - #[deprecated( - since = "0.21.0", - note = "`PyTypeInfo::type_object` will be replaced by `PyTypeInfo::type_object_bound` in a future PyO3 version" - )] - fn type_object(py: Python<'_>) -> &PyType { - // This isn't implemented in terms of `type_object_bound` because this just borrowed the - // object, for legacy reasons. - #[allow(deprecated)] - unsafe { - py.from_borrowed_ptr(Self::type_object_raw(py) as _) - } - } - - /// Returns the safe abstraction over the type object. - #[inline] - fn type_object_bound(py: Python<'_>) -> Bound<'_, PyType> { - // Making the borrowed object `Bound` is necessary for soundness reasons. It's an extreme - // edge case, but arbitrary Python code _could_ change the __class__ of an object and cause - // the type object to be freed. - // - // By making `Bound` we assume ownership which is then safe against races. - unsafe { - Self::type_object_raw(py) - .cast::() - .assume_borrowed_unchecked(py) - .to_owned() - .downcast_into_unchecked() - } - } - - /// Checks if `object` is an instance of this type or a subclass of this type. - #[inline] - #[cfg(feature = "gil-refs")] - #[deprecated( - since = "0.21.0", - note = "`PyTypeInfo::is_type_of` will be replaced by `PyTypeInfo::is_type_of_bound` in a future PyO3 version" - )] - fn is_type_of(object: &PyAny) -> bool { - Self::is_type_of_bound(&object.as_borrowed()) - } - - /// Checks if `object` is an instance of this type or a subclass of this type. - #[inline] - fn is_type_of_bound(object: &Bound<'_, PyAny>) -> bool { - unsafe { ffi::PyObject_TypeCheck(object.as_ptr(), Self::type_object_raw(object.py())) != 0 } - } - - /// Checks if `object` is an instance of this type. - #[inline] - #[cfg(feature = "gil-refs")] - #[deprecated( - since = "0.21.0", - note = "`PyTypeInfo::is_exact_type_of` will be replaced by `PyTypeInfo::is_exact_type_of_bound` in a future PyO3 version" - )] - fn is_exact_type_of(object: &PyAny) -> bool { - Self::is_exact_type_of_bound(&object.as_borrowed()) - } - - /// Checks if `object` is an instance of this type. - #[inline] - fn is_exact_type_of_bound(object: &Bound<'_, PyAny>) -> bool { - unsafe { ffi::Py_TYPE(object.as_ptr()) == Self::type_object_raw(object.py()) } - } -} - /// Python type information. /// All Python native types (e.g., `PyDict`) and `#[pyclass]` structs implement this trait. /// @@ -150,7 +34,6 @@ pub unsafe trait PyTypeInfo: Sized + HasPyGilRef { /// /// Implementations must provide an implementation for `type_object_raw` which infallibly produces a /// non-null pointer to the corresponding Python type object. -#[cfg(not(feature = "gil-refs"))] pub unsafe trait PyTypeInfo: Sized { /// Class name. const NAME: &'static str; @@ -192,19 +75,6 @@ pub unsafe trait PyTypeInfo: Sized { } /// Implemented by types which can be used as a concrete Python type inside `Py` smart pointers. -#[cfg(feature = "gil-refs")] -pub trait PyTypeCheck: HasPyGilRef { - /// Name of self. This is used in error messages, for example. - const NAME: &'static str; - - /// Checks if `object` is an instance of `Self`, which may include a subtype. - /// - /// This should be equivalent to the Python expression `isinstance(object, Self)`. - fn type_check(object: &Bound<'_, PyAny>) -> bool; -} - -/// Implemented by types which can be used as a concrete Python type inside `Py` smart pointers. -#[cfg(not(feature = "gil-refs"))] pub trait PyTypeCheck { /// Name of self. This is used in error messages, for example. const NAME: &'static str; diff --git a/src/types/any.rs b/src/types/any.rs index 33ac04df763..626feba0856 100644 --- a/src/types/any.rs +++ b/src/types/any.rs @@ -10,8 +10,6 @@ use crate::type_object::{PyTypeCheck, PyTypeInfo}; #[cfg(not(any(PyPy, GraalPy)))] use crate::types::PySuper; use crate::types::{PyDict, PyIterator, PyList, PyString, PyTuple, PyType}; -#[cfg(feature = "gil-refs")] -use crate::PyNativeType; use crate::{err, ffi, Py, Python}; use std::cell::UnsafeCell; use std::cmp::Ordering; @@ -45,8 +43,6 @@ fn PyObject_Check(_: *mut ffi::PyObject) -> c_int { 1 } -pyobject_native_type_base!(PyAny); - pyobject_native_type_info!( PyAny, pyobject_native_static_type_object!(ffi::PyBaseObject_Type), @@ -56,739 +52,6 @@ pyobject_native_type_info!( pyobject_native_type_sized!(PyAny, ffi::PyObject); -#[cfg(feature = "gil-refs")] -impl PyAny { - /// Returns whether `self` and `other` point to the same object. To compare - /// the equality of two objects (the `==` operator), use [`eq`](PyAny::eq). - /// - /// This is equivalent to the Python expression `self is other`. - #[inline] - pub fn is(&self, other: &T) -> bool { - self.as_borrowed().is(other) - } - - /// Determines whether this object has the given attribute. - /// - /// This is equivalent to the Python expression `hasattr(self, attr_name)`. - /// - /// To avoid repeated temporary allocations of Python strings, the [`intern!`] macro can be used - /// to intern `attr_name`. - /// - /// # Example: `intern!`ing the attribute name - /// - /// ``` - /// # use pyo3::{prelude::*, intern}; - /// # - /// #[pyfunction] - /// fn has_version(sys: &Bound<'_, PyModule>) -> PyResult { - /// sys.hasattr(intern!(sys.py(), "version")) - /// } - /// # - /// # Python::with_gil(|py| { - /// # let sys = py.import_bound("sys").unwrap(); - /// # has_version(&sys).unwrap(); - /// # }); - /// ``` - pub fn hasattr(&self, attr_name: N) -> PyResult - where - N: IntoPy>, - { - self.as_borrowed().hasattr(attr_name) - } - - /// Retrieves an attribute value. - /// - /// This is equivalent to the Python expression `self.attr_name`. - /// - /// To avoid repeated temporary allocations of Python strings, the [`intern!`] macro can be used - /// to intern `attr_name`. - /// - /// # Example: `intern!`ing the attribute name - /// - /// ``` - /// # use pyo3::{prelude::*, intern}; - /// # - /// #[pyfunction] - /// fn version<'py>(sys: &Bound<'py, PyModule>) -> PyResult> { - /// sys.getattr(intern!(sys.py(), "version")) - /// } - /// # - /// # Python::with_gil(|py| { - /// # let sys = py.import_bound("sys").unwrap(); - /// # version(&sys).unwrap(); - /// # }); - /// ``` - pub fn getattr(&self, attr_name: N) -> PyResult<&PyAny> - where - N: IntoPy>, - { - self.as_borrowed() - .getattr(attr_name) - .map(Bound::into_gil_ref) - } - - /// Sets an attribute value. - /// - /// This is equivalent to the Python expression `self.attr_name = value`. - /// - /// To avoid repeated temporary allocations of Python strings, the [`intern!`] macro can be used - /// to intern `name`. - /// - /// # Example: `intern!`ing the attribute name - /// - /// ``` - /// # use pyo3::{prelude::*, intern}; - /// # - /// #[pyfunction] - /// fn set_answer(ob: &Bound<'_, PyAny>) -> PyResult<()> { - /// ob.setattr(intern!(ob.py(), "answer"), 42) - /// } - /// # - /// # Python::with_gil(|py| { - /// # let ob = PyModule::new_bound(py, "empty").unwrap(); - /// # set_answer(&ob).unwrap(); - /// # }); - /// ``` - pub fn setattr(&self, attr_name: N, value: V) -> PyResult<()> - where - N: IntoPy>, - V: ToPyObject, - { - self.as_borrowed().setattr(attr_name, value) - } - - /// Deletes an attribute. - /// - /// This is equivalent to the Python statement `del self.attr_name`. - /// - /// To avoid repeated temporary allocations of Python strings, the [`intern!`] macro can be used - /// to intern `attr_name`. - pub fn delattr(&self, attr_name: N) -> PyResult<()> - where - N: IntoPy>, - { - self.as_borrowed().delattr(attr_name) - } - - /// Returns an [`Ordering`] between `self` and `other`. - /// - /// This is equivalent to the following Python code: - /// ```python - /// if self == other: - /// return Equal - /// elif a < b: - /// return Less - /// elif a > b: - /// return Greater - /// else: - /// raise TypeError("PyAny::compare(): All comparisons returned false") - /// ``` - /// - /// # Examples - /// - /// ```rust - /// use pyo3::prelude::*; - /// use pyo3::types::PyFloat; - /// use std::cmp::Ordering; - /// - /// # fn main() -> PyResult<()> { - /// Python::with_gil(|py| -> PyResult<()> { - /// let a = PyFloat::new_bound(py, 0_f64); - /// let b = PyFloat::new_bound(py, 42_f64); - /// assert_eq!(a.compare(b)?, Ordering::Less); - /// Ok(()) - /// })?; - /// # Ok(())} - /// ``` - /// - /// It will return `PyErr` for values that cannot be compared: - /// - /// ```rust - /// use pyo3::prelude::*; - /// use pyo3::types::{PyFloat, PyString}; - /// - /// # fn main() -> PyResult<()> { - /// Python::with_gil(|py| -> PyResult<()> { - /// let a = PyFloat::new_bound(py, 0_f64); - /// let b = PyString::new_bound(py, "zero"); - /// assert!(a.compare(b).is_err()); - /// Ok(()) - /// })?; - /// # Ok(())} - /// ``` - pub fn compare(&self, other: O) -> PyResult - where - O: ToPyObject, - { - self.as_borrowed().compare(other) - } - - /// Tests whether two Python objects obey a given [`CompareOp`]. - /// - /// [`lt`](Self::lt), [`le`](Self::le), [`eq`](Self::eq), [`ne`](Self::ne), - /// [`gt`](Self::gt) and [`ge`](Self::ge) are the specialized versions - /// of this function. - /// - /// Depending on the value of `compare_op`, this is equivalent to one of the - /// following Python expressions: - /// - /// | `compare_op` | Python expression | - /// | :---: | :----: | - /// | [`CompareOp::Eq`] | `self == other` | - /// | [`CompareOp::Ne`] | `self != other` | - /// | [`CompareOp::Lt`] | `self < other` | - /// | [`CompareOp::Le`] | `self <= other` | - /// | [`CompareOp::Gt`] | `self > other` | - /// | [`CompareOp::Ge`] | `self >= other` | - /// - /// # Examples - /// - /// ```rust - /// use pyo3::class::basic::CompareOp; - /// use pyo3::prelude::*; - /// use pyo3::types::PyInt; - /// - /// # fn main() -> PyResult<()> { - /// Python::with_gil(|py| -> PyResult<()> { - /// let a: Bound<'_, PyInt> = 0_u8.into_py(py).into_bound(py).downcast_into()?; - /// let b: Bound<'_, PyInt> = 42_u8.into_py(py).into_bound(py).downcast_into()?; - /// assert!(a.rich_compare(b, CompareOp::Le)?.is_truthy()?); - /// Ok(()) - /// })?; - /// # Ok(())} - /// ``` - pub fn rich_compare(&self, other: O, compare_op: CompareOp) -> PyResult<&PyAny> - where - O: ToPyObject, - { - self.as_borrowed() - .rich_compare(other, compare_op) - .map(Bound::into_gil_ref) - } - - /// Tests whether this object is less than another. - /// - /// This is equivalent to the Python expression `self < other`. - pub fn lt(&self, other: O) -> PyResult - where - O: ToPyObject, - { - self.as_borrowed().lt(other) - } - - /// Tests whether this object is less than or equal to another. - /// - /// This is equivalent to the Python expression `self <= other`. - pub fn le(&self, other: O) -> PyResult - where - O: ToPyObject, - { - self.as_borrowed().le(other) - } - - /// Tests whether this object is equal to another. - /// - /// This is equivalent to the Python expression `self == other`. - pub fn eq(&self, other: O) -> PyResult - where - O: ToPyObject, - { - self.as_borrowed().eq(other) - } - - /// Tests whether this object is not equal to another. - /// - /// This is equivalent to the Python expression `self != other`. - pub fn ne(&self, other: O) -> PyResult - where - O: ToPyObject, - { - self.as_borrowed().ne(other) - } - - /// Tests whether this object is greater than another. - /// - /// This is equivalent to the Python expression `self > other`. - pub fn gt(&self, other: O) -> PyResult - where - O: ToPyObject, - { - self.as_borrowed().gt(other) - } - - /// Tests whether this object is greater than or equal to another. - /// - /// This is equivalent to the Python expression `self >= other`. - pub fn ge(&self, other: O) -> PyResult - where - O: ToPyObject, - { - self.as_borrowed().ge(other) - } - - /// Determines whether this object appears callable. - /// - /// This is equivalent to Python's [`callable()`][1] function. - /// - /// # Examples - /// - /// ```rust - /// use pyo3::prelude::*; - /// - /// # fn main() -> PyResult<()> { - /// Python::with_gil(|py| -> PyResult<()> { - /// let builtins = PyModule::import_bound(py, "builtins")?; - /// let print = builtins.getattr("print")?; - /// assert!(print.is_callable()); - /// Ok(()) - /// })?; - /// # Ok(())} - /// ``` - /// - /// This is equivalent to the Python statement `assert callable(print)`. - /// - /// Note that unless an API needs to distinguish between callable and - /// non-callable objects, there is no point in checking for callability. - /// Instead, it is better to just do the call and handle potential - /// exceptions. - /// - /// [1]: https://docs.python.org/3/library/functions.html#callable - pub fn is_callable(&self) -> bool { - self.as_borrowed().is_callable() - } - - /// Calls the object. - /// - /// This is equivalent to the Python expression `self(*args, **kwargs)`. - /// - /// # Examples - /// - /// ```rust - /// use pyo3::prelude::*; - /// use pyo3::types::PyDict; - /// - /// const CODE: &str = r#" - /// def function(*args, **kwargs): - /// assert args == ("hello",) - /// assert kwargs == {"cruel": "world"} - /// return "called with args and kwargs" - /// "#; - /// - /// # fn main() -> PyResult<()> { - /// Python::with_gil(|py| { - /// let module = PyModule::from_code_bound(py, CODE, "", "")?; - /// let fun = module.getattr("function")?; - /// let args = ("hello",); - /// let kwargs = PyDict::new_bound(py); - /// kwargs.set_item("cruel", "world")?; - /// let result = fun.call(args, Some(&kwargs))?; - /// assert_eq!(result.extract::()?, "called with args and kwargs"); - /// Ok(()) - /// }) - /// # } - /// ``` - pub fn call( - &self, - args: impl IntoPy>, - kwargs: Option<&PyDict>, - ) -> PyResult<&PyAny> { - self.as_borrowed() - .call(args, kwargs.map(PyDict::as_borrowed).as_deref()) - .map(Bound::into_gil_ref) - } - - /// Calls the object without arguments. - /// - /// This is equivalent to the Python expression `self()`. - /// - /// # Examples - /// - /// ```no_run - /// use pyo3::prelude::*; - /// - /// # fn main() -> PyResult<()> { - /// Python::with_gil(|py| -> PyResult<()> { - /// let module = PyModule::import_bound(py, "builtins")?; - /// let help = module.getattr("help")?; - /// help.call0()?; - /// Ok(()) - /// })?; - /// # Ok(())} - /// ``` - /// - /// This is equivalent to the Python expression `help()`. - pub fn call0(&self) -> PyResult<&PyAny> { - self.as_borrowed().call0().map(Bound::into_gil_ref) - } - - /// Calls the object with only positional arguments. - /// - /// This is equivalent to the Python expression `self(*args)`. - /// - /// # Examples - /// - /// ```rust - /// use pyo3::prelude::*; - /// - /// const CODE: &str = r#" - /// def function(*args, **kwargs): - /// assert args == ("hello",) - /// assert kwargs == {} - /// return "called with args" - /// "#; - /// - /// # fn main() -> PyResult<()> { - /// Python::with_gil(|py| { - /// let module = PyModule::from_code_bound(py, CODE, "", "")?; - /// let fun = module.getattr("function")?; - /// let args = ("hello",); - /// let result = fun.call1(args)?; - /// assert_eq!(result.extract::()?, "called with args"); - /// Ok(()) - /// }) - /// # } - /// ``` - pub fn call1(&self, args: impl IntoPy>) -> PyResult<&PyAny> { - self.as_borrowed().call1(args).map(Bound::into_gil_ref) - } - - /// Calls a method on the object. - /// - /// This is equivalent to the Python expression `self.name(*args, **kwargs)`. - /// - /// To avoid repeated temporary allocations of Python strings, the [`intern!`] macro can be used - /// to intern `name`. - /// - /// # Examples - /// - /// ```rust - /// use pyo3::prelude::*; - /// use pyo3::types::PyDict; - /// - /// const CODE: &str = r#" - /// class A: - /// def method(self, *args, **kwargs): - /// assert args == ("hello",) - /// assert kwargs == {"cruel": "world"} - /// return "called with args and kwargs" - /// a = A() - /// "#; - /// - /// # fn main() -> PyResult<()> { - /// Python::with_gil(|py| { - /// let module = PyModule::from_code_bound(py, CODE, "", "")?; - /// let instance = module.getattr("a")?; - /// let args = ("hello",); - /// let kwargs = PyDict::new_bound(py); - /// kwargs.set_item("cruel", "world")?; - /// let result = instance.call_method("method", args, Some(&kwargs))?; - /// assert_eq!(result.extract::()?, "called with args and kwargs"); - /// Ok(()) - /// }) - /// # } - /// ``` - pub fn call_method(&self, name: N, args: A, kwargs: Option<&PyDict>) -> PyResult<&PyAny> - where - N: IntoPy>, - A: IntoPy>, - { - self.as_borrowed() - .call_method(name, args, kwargs.map(PyDict::as_borrowed).as_deref()) - .map(Bound::into_gil_ref) - } - - /// Calls a method on the object without arguments. - /// - /// This is equivalent to the Python expression `self.name()`. - /// - /// To avoid repeated temporary allocations of Python strings, the [`intern!`] macro can be used - /// to intern `name`. - /// - /// # Examples - /// - /// ```rust - /// use pyo3::prelude::*; - /// - /// const CODE: &str = r#" - /// class A: - /// def method(self, *args, **kwargs): - /// assert args == () - /// assert kwargs == {} - /// return "called with no arguments" - /// a = A() - /// "#; - /// - /// # fn main() -> PyResult<()> { - /// Python::with_gil(|py| { - /// let module = PyModule::from_code_bound(py, CODE, "", "")?; - /// let instance = module.getattr("a")?; - /// let result = instance.call_method0("method")?; - /// assert_eq!(result.extract::()?, "called with no arguments"); - /// Ok(()) - /// }) - /// # } - /// ``` - pub fn call_method0(&self, name: N) -> PyResult<&PyAny> - where - N: IntoPy>, - { - self.as_borrowed() - .call_method0(name) - .map(Bound::into_gil_ref) - } - - /// Calls a method on the object with only positional arguments. - /// - /// This is equivalent to the Python expression `self.name(*args)`. - /// - /// To avoid repeated temporary allocations of Python strings, the [`intern!`] macro can be used - /// to intern `name`. - /// - /// # Examples - /// - /// ```rust - /// use pyo3::prelude::*; - /// - /// const CODE: &str = r#" - /// class A: - /// def method(self, *args, **kwargs): - /// assert args == ("hello",) - /// assert kwargs == {} - /// return "called with args" - /// a = A() - /// "#; - /// - /// # fn main() -> PyResult<()> { - /// Python::with_gil(|py| { - /// let module = PyModule::from_code_bound(py, CODE, "", "")?; - /// let instance = module.getattr("a")?; - /// let args = ("hello",); - /// let result = instance.call_method1("method", args)?; - /// assert_eq!(result.extract::()?, "called with args"); - /// Ok(()) - /// }) - /// # } - /// ``` - pub fn call_method1(&self, name: N, args: A) -> PyResult<&PyAny> - where - N: IntoPy>, - A: IntoPy>, - { - self.as_borrowed() - .call_method1(name, args) - .map(Bound::into_gil_ref) - } - - /// Returns whether the object is considered to be true. - /// - /// This applies truth value testing equivalent to the Python expression `bool(self)`. - pub fn is_truthy(&self) -> PyResult { - self.as_borrowed().is_truthy() - } - - /// Returns whether the object is considered to be None. - /// - /// This is equivalent to the Python expression `self is None`. - #[inline] - pub fn is_none(&self) -> bool { - self.as_borrowed().is_none() - } - - /// Returns true if the sequence or mapping has a length of 0. - /// - /// This is equivalent to the Python expression `len(self) == 0`. - pub fn is_empty(&self) -> PyResult { - self.as_borrowed().is_empty() - } - - /// Gets an item from the collection. - /// - /// This is equivalent to the Python expression `self[key]`. - pub fn get_item(&self, key: K) -> PyResult<&PyAny> - where - K: ToPyObject, - { - self.as_borrowed().get_item(key).map(Bound::into_gil_ref) - } - - /// Sets a collection item value. - /// - /// This is equivalent to the Python expression `self[key] = value`. - pub fn set_item(&self, key: K, value: V) -> PyResult<()> - where - K: ToPyObject, - V: ToPyObject, - { - self.as_borrowed().set_item(key, value) - } - - /// Deletes an item from the collection. - /// - /// This is equivalent to the Python expression `del self[key]`. - pub fn del_item(&self, key: K) -> PyResult<()> - where - K: ToPyObject, - { - self.as_borrowed().del_item(key) - } - - /// Takes an object and returns an iterator for it. - /// - /// This is typically a new iterator but if the argument is an iterator, - /// this returns itself. - pub fn iter(&self) -> PyResult<&PyIterator> { - self.as_borrowed().iter().map(Bound::into_gil_ref) - } - - /// Returns the Python type object for this object's type. - pub fn get_type(&self) -> &PyType { - self.as_borrowed().get_type().into_gil_ref() - } - - /// Returns the Python type pointer for this object. - #[inline] - pub fn get_type_ptr(&self) -> *mut ffi::PyTypeObject { - self.as_borrowed().get_type_ptr() - } - - /// Extracts some type from the Python object. - /// - /// This is a wrapper function around - /// [`FromPyObject::extract()`](crate::FromPyObject::extract). - #[inline] - pub fn extract<'py, D>(&'py self) -> PyResult - where - D: FromPyObjectBound<'py, 'py>, - { - FromPyObjectBound::from_py_object_bound(self.as_borrowed()) - } - - /// Returns the reference count for the Python object. - pub fn get_refcnt(&self) -> isize { - self.as_borrowed().get_refcnt() - } - - /// Computes the "repr" representation of self. - /// - /// This is equivalent to the Python expression `repr(self)`. - pub fn repr(&self) -> PyResult<&PyString> { - self.as_borrowed().repr().map(Bound::into_gil_ref) - } - - /// Computes the "str" representation of self. - /// - /// This is equivalent to the Python expression `str(self)`. - pub fn str(&self) -> PyResult<&PyString> { - self.as_borrowed().str().map(Bound::into_gil_ref) - } - - /// Retrieves the hash code of self. - /// - /// This is equivalent to the Python expression `hash(self)`. - pub fn hash(&self) -> PyResult { - self.as_borrowed().hash() - } - - /// Returns the length of the sequence or mapping. - /// - /// This is equivalent to the Python expression `len(self)`. - pub fn len(&self) -> PyResult { - self.as_borrowed().len() - } - - /// Returns the list of attributes of this object. - /// - /// This is equivalent to the Python expression `dir(self)`. - pub fn dir(&self) -> PyResult<&PyList> { - self.as_borrowed().dir().map(Bound::into_gil_ref) - } - - /// Checks whether this object is an instance of type `ty`. - /// - /// This is equivalent to the Python expression `isinstance(self, ty)`. - #[inline] - pub fn is_instance(&self, ty: &PyAny) -> PyResult { - self.as_borrowed().is_instance(&ty.as_borrowed()) - } - - /// Checks whether this object is an instance of exactly type `ty` (not a subclass). - /// - /// This is equivalent to the Python expression `type(self) is ty`. - #[inline] - pub fn is_exact_instance(&self, ty: &PyAny) -> bool { - self.as_borrowed().is_exact_instance(&ty.as_borrowed()) - } - - /// Checks whether this object is an instance of type `T`. - /// - /// This is equivalent to the Python expression `isinstance(self, T)`, - /// if the type `T` is known at compile time. - #[inline] - pub fn is_instance_of(&self) -> bool { - self.as_borrowed().is_instance_of::() - } - - /// Checks whether this object is an instance of exactly type `T`. - /// - /// This is equivalent to the Python expression `type(self) is T`, - /// if the type `T` is known at compile time. - #[inline] - pub fn is_exact_instance_of(&self) -> bool { - self.as_borrowed().is_exact_instance_of::() - } - - /// Determines if self contains `value`. - /// - /// This is equivalent to the Python expression `value in self`. - pub fn contains(&self, value: V) -> PyResult - where - V: ToPyObject, - { - self.as_borrowed().contains(value) - } - - /// Returns a GIL marker constrained to the lifetime of this type. - #[inline] - pub fn py(&self) -> Python<'_> { - PyNativeType::py(self) - } - - /// Returns the raw FFI pointer represented by self. - /// - /// # Safety - /// - /// Callers are responsible for ensuring that the pointer does not outlive self. - /// - /// The reference is borrowed; callers should not decrease the reference count - /// when they are finished with the pointer. - #[inline] - pub fn as_ptr(&self) -> *mut ffi::PyObject { - ptr_from_ref(self) as *mut ffi::PyObject - } - - /// Returns an owned raw FFI pointer represented by self. - /// - /// # Safety - /// - /// The reference is owned; when finished the caller should either transfer ownership - /// of the pointer or decrease the reference count (e.g. with [`pyo3::ffi::Py_DecRef`](crate::ffi::Py_DecRef)). - #[inline] - pub fn into_ptr(&self) -> *mut ffi::PyObject { - // Safety: self.as_ptr() returns a valid non-null pointer - let ptr = self.as_ptr(); - unsafe { ffi::Py_INCREF(ptr) }; - ptr - } - - /// Return a proxy object that delegates method calls to a parent or sibling class of type. - /// - /// This is equivalent to the Python expression `super()` - #[cfg(not(any(PyPy, GraalPy)))] - pub fn py_super(&self) -> PyResult<&PySuper> { - self.as_borrowed().py_super().map(Bound::into_gil_ref) - } -} - /// This trait represents the Python APIs which are usable on all Python objects. /// /// It is recommended you import this trait via `use pyo3::prelude::*` rather than diff --git a/src/types/datetime.rs b/src/types/datetime.rs index b8a344a60b1..72b3bf886f2 100644 --- a/src/types/datetime.rs +++ b/src/types/datetime.rs @@ -171,16 +171,6 @@ pub trait PyTimeAccess { /// Trait for accessing the components of a struct containing a tzinfo. pub trait PyTzInfoAccess<'py> { - /// Deprecated form of `get_tzinfo_bound`. - #[cfg(feature = "gil-refs")] - #[deprecated( - since = "0.21.0", - note = "`get_tzinfo` will be replaced by `get_tzinfo_bound` in a future PyO3 version" - )] - fn get_tzinfo(&self) -> Option<&'py PyTzInfo> { - self.get_tzinfo_bound().map(Bound::into_gil_ref) - } - /// Returns the tzinfo (which may be None). /// /// Implementations should conform to the upstream documentation: diff --git a/src/types/dict.rs b/src/types/dict.rs index 3c731d909e0..1c86b291e2d 100644 --- a/src/types/dict.rs +++ b/src/types/dict.rs @@ -517,17 +517,6 @@ pub(crate) use borrowed_iter::BorrowedDictIter; /// Conversion trait that allows a sequence of tuples to be converted into `PyDict` /// Primary use case for this trait is `call` and `call_method` methods as keywords argument. pub trait IntoPyDict: Sized { - /// Converts self into a `PyDict` object pointer. Whether pointer owned or borrowed - /// depends on implementation. - #[cfg(feature = "gil-refs")] - #[deprecated( - since = "0.21.0", - note = "`IntoPyDict::into_py_dict` will be replaced by `IntoPyDict::into_py_dict_bound` in a future PyO3 version" - )] - fn into_py_dict(self, py: Python<'_>) -> &PyDict { - Self::into_py_dict_bound(self, py).into_gil_ref() - } - /// Converts self into a `PyDict` object pointer. Whether pointer owned or borrowed /// depends on implementation. fn into_py_dict_bound(self, py: Python<'_>) -> Bound<'_, PyDict>; diff --git a/src/types/mod.rs b/src/types/mod.rs index 11d409edfa3..1acd19ed74f 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -110,63 +110,12 @@ pub trait DerefToPyAny { // Empty. } -// Implementations core to all native types -#[doc(hidden)] -#[macro_export] -macro_rules! pyobject_native_type_base( - ($name:ty $(;$generics:ident)* ) => { - #[cfg(feature = "gil-refs")] - unsafe impl<$($generics,)*> $crate::PyNativeType for $name { - type AsRefSource = Self; - } - - #[cfg(feature = "gil-refs")] - impl<$($generics,)*> ::std::fmt::Debug for $name { - fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) - -> ::std::result::Result<(), ::std::fmt::Error> - { - use $crate::{PyNativeType, types::{PyAnyMethods, PyStringMethods}}; - let s = self.as_borrowed().repr().or(::std::result::Result::Err(::std::fmt::Error))?; - f.write_str(&s.to_string_lossy()) - } - } - - #[cfg(feature = "gil-refs")] - impl<$($generics,)*> ::std::fmt::Display for $name { - fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) - -> ::std::result::Result<(), ::std::fmt::Error> - { - use $crate::{PyNativeType, types::{PyAnyMethods, PyStringMethods, PyTypeMethods}}; - match self.as_borrowed().str() { - ::std::result::Result::Ok(s) => return f.write_str(&s.to_string_lossy()), - ::std::result::Result::Err(err) => err.write_unraisable_bound(self.py(), ::std::option::Option::Some(&self.as_borrowed())), - } - - match self.as_borrowed().get_type().name() { - ::std::result::Result::Ok(name) => ::std::write!(f, "", name), - ::std::result::Result::Err(_err) => f.write_str(""), - } - } - } - - #[cfg(feature = "gil-refs")] - impl<$($generics,)*> $crate::ToPyObject for $name - { - #[inline] - fn to_object(&self, py: $crate::Python<'_>) -> $crate::PyObject { - unsafe { $crate::PyObject::from_borrowed_ptr(py, self.as_ptr()) } - } - } - }; -); - // Implementations core to all native types except for PyAny (because they don't // make sense on PyAny / have different implementations). #[doc(hidden)] #[macro_export] macro_rules! pyobject_native_type_named ( ($name:ty $(;$generics:ident)*) => { - $crate::pyobject_native_type_base!($name $(;$generics)*); impl<$($generics,)*> ::std::convert::AsRef<$crate::PyAny> for $name { #[inline] @@ -192,36 +141,6 @@ macro_rules! pyobject_native_type_named ( } } - // FIXME https://github.com/PyO3/pyo3/issues/3903 - #[allow(unknown_lints, non_local_definitions)] - #[cfg(feature = "gil-refs")] - impl<$($generics,)*> $crate::IntoPy<$crate::Py<$name>> for &'_ $name { - #[inline] - fn into_py(self, py: $crate::Python<'_>) -> $crate::Py<$name> { - unsafe { $crate::Py::from_borrowed_ptr(py, self.as_ptr()) } - } - } - - // FIXME https://github.com/PyO3/pyo3/issues/3903 - #[allow(unknown_lints, non_local_definitions)] - #[cfg(feature = "gil-refs")] - impl<$($generics,)*> ::std::convert::From<&'_ $name> for $crate::Py<$name> { - #[inline] - fn from(other: &$name) -> Self { - use $crate::PyNativeType; - unsafe { $crate::Py::from_borrowed_ptr(other.py(), other.as_ptr()) } - } - } - - // FIXME https://github.com/PyO3/pyo3/issues/3903 - #[allow(unknown_lints, non_local_definitions)] - #[cfg(feature = "gil-refs")] - impl<'a, $($generics,)*> ::std::convert::From<&'a $name> for &'a $crate::PyAny { - fn from(ob: &'a $name) -> Self { - unsafe{&*(ob as *const $name as *const $crate::PyAny)} - } - } - impl $crate::types::DerefToPyAny for $name {} }; ); diff --git a/tests/test_compile_error.rs b/tests/test_compile_error.rs index eb9934af949..c2d8d3287c1 100644 --- a/tests/test_compile_error.rs +++ b/tests/test_compile_error.rs @@ -15,7 +15,7 @@ fn test_compile_errors() { #[cfg(any(not(Py_LIMITED_API), Py_3_11))] t.compile_fail("tests/ui/invalid_pymethods_buffer.rs"); // The output is not stable across abi3 / not abi3 and features - #[cfg(all(not(Py_LIMITED_API), feature = "full", not(feature = "gil-refs")))] + #[cfg(all(not(Py_LIMITED_API), feature = "full"))] t.compile_fail("tests/ui/invalid_pymethods_duplicates.rs"); t.compile_fail("tests/ui/invalid_pymethod_enum.rs"); t.compile_fail("tests/ui/invalid_pymethod_names.rs"); @@ -27,26 +27,18 @@ fn test_compile_errors() { t.compile_fail("tests/ui/invalid_argument_attributes.rs"); t.compile_fail("tests/ui/invalid_frompy_derive.rs"); t.compile_fail("tests/ui/static_ref.rs"); - #[cfg(not(feature = "gil-refs"))] t.compile_fail("tests/ui/wrong_aspyref_lifetimes.rs"); t.compile_fail("tests/ui/invalid_pyfunctions.rs"); t.compile_fail("tests/ui/invalid_pymethods.rs"); // output changes with async feature #[cfg(all(Py_LIMITED_API, feature = "experimental-async"))] t.compile_fail("tests/ui/abi3_nativetype_inheritance.rs"); - #[cfg(not(feature = "gil-refs"))] t.compile_fail("tests/ui/invalid_intern_arg.rs"); t.compile_fail("tests/ui/invalid_frozen_pyclass_borrow.rs"); t.compile_fail("tests/ui/invalid_pymethod_receiver.rs"); t.compile_fail("tests/ui/missing_intopy.rs"); // adding extra error conversion impls changes the output - #[cfg(not(any( - windows, - feature = "eyre", - feature = "anyhow", - feature = "gil-refs", - Py_LIMITED_API - )))] + #[cfg(not(any(windows, feature = "eyre", feature = "anyhow", Py_LIMITED_API)))] t.compile_fail("tests/ui/invalid_result_conversion.rs"); t.compile_fail("tests/ui/not_send.rs"); t.compile_fail("tests/ui/not_send2.rs"); diff --git a/tests/test_super.rs b/tests/test_super.rs index 3647e7d5b23..3362ad57814 100644 --- a/tests/test_super.rs +++ b/tests/test_super.rs @@ -35,7 +35,6 @@ impl SubClass { } fn method_super_new<'py>(self_: &Bound<'py, Self>) -> PyResult> { - #[cfg_attr(not(feature = "gil-refs"), allow(deprecated))] let super_ = PySuper::new_bound(&self_.get_type(), self_)?; super_.call_method("method", (), None) } From 6ef4ca34ebfac9d9293d0ad2be6f082f9a640ddb Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Sat, 27 Jul 2024 20:08:50 +0200 Subject: [PATCH 176/495] fix nightly ci (#4385) --- guide/src/class.md | 2 ++ guide/src/class/object.md | 10 ++++++++++ guide/src/faq.md | 1 + src/conversion.rs | 1 + src/types/mod.rs | 5 ++++- tests/test_declarative_module.rs | 2 +- tests/test_macros.rs | 1 + 7 files changed, 20 insertions(+), 2 deletions(-) diff --git a/guide/src/class.md b/guide/src/class.md index 3a6f3f9f36c..216838f779d 100644 --- a/guide/src/class.md +++ b/guide/src/class.md @@ -549,6 +549,7 @@ For simple cases where a member variable is just read and written with no side e ```rust # use pyo3::prelude::*; +# #[allow(dead_code)] #[pyclass] struct MyClass { #[pyo3(get, set)] @@ -1360,6 +1361,7 @@ The `#[pyclass]` macro expands to roughly the code seen below. The `PyClassImplC # #[cfg(not(feature = "multiple-pymethods"))] { # use pyo3::prelude::*; // Note: the implementation differs slightly with the `multiple-pymethods` feature enabled. +# #[allow(dead_code)] struct MyClass { # #[allow(dead_code)] num: i32, diff --git a/guide/src/class/object.md b/guide/src/class/object.md index e2565427838..07f445aac60 100644 --- a/guide/src/class/object.md +++ b/guide/src/class/object.md @@ -76,6 +76,7 @@ To automatically generate the `__str__` implementation using a `Display` trait i # use std::fmt::{Display, Formatter}; # use pyo3::prelude::*; # +# #[allow(dead_code)] # #[pyclass(str)] # struct Coordinate { x: i32, @@ -102,6 +103,7 @@ For convenience, a shorthand format string can be passed to `str` as `str="
{ - |_py| unsafe { ::std::ptr::addr_of_mut!($typeobject) } + |_py| { + #[allow(unused_unsafe)] // https://github.com/rust-lang/rust/pull/125834 + unsafe { ::std::ptr::addr_of_mut!($typeobject) } + } }; ); diff --git a/tests/test_declarative_module.rs b/tests/test_declarative_module.rs index 060da188aaa..9d3250d79ef 100644 --- a/tests/test_declarative_module.rs +++ b/tests/test_declarative_module.rs @@ -35,7 +35,7 @@ impl ValueClass { } #[pyclass(module = "module")] -struct LocatedClass {} +pub struct LocatedClass {} #[pyfunction] fn double(x: usize) -> usize { diff --git a/tests/test_macros.rs b/tests/test_macros.rs index 0d2b125b870..4bf2807f93a 100644 --- a/tests/test_macros.rs +++ b/tests/test_macros.rs @@ -22,6 +22,7 @@ make_struct_using_macro!(MyBaseClass, "MyClass"); macro_rules! set_extends_via_macro { ($class_name:ident, $base_class:path) => { // Try and pass a variable into the extends parameter + #[allow(dead_code)] #[pyclass(extends=$base_class)] struct $class_name {} }; From 9e2a945163e5141c15022593d961f77cd788c239 Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Sat, 27 Jul 2024 21:27:31 +0200 Subject: [PATCH 177/495] fix incorrect spans on `ret` and `py` local varianbles in emitted code (#4382) * fix incorrect spans on `ret` and `py` local varianbles in emitted code * add newsfragment --- newsfragments/4382.fixed.md | 1 + pyo3-macros-backend/src/method.rs | 7 ++++--- pyo3-macros-backend/src/pyclass.rs | 2 +- pyo3-macros-backend/src/quotes.rs | 3 ++- src/tests/hygiene/misc.rs | 19 +++++++++++++++++++ 5 files changed, 27 insertions(+), 5 deletions(-) create mode 100644 newsfragments/4382.fixed.md diff --git a/newsfragments/4382.fixed.md b/newsfragments/4382.fixed.md new file mode 100644 index 00000000000..974ae23d3bf --- /dev/null +++ b/newsfragments/4382.fixed.md @@ -0,0 +1 @@ +Fixed hygiene/span issues of emitted code which affected expansion in `macro_rules` context. \ No newline at end of file diff --git a/pyo3-macros-backend/src/method.rs b/pyo3-macros-backend/src/method.rs index d5e26f694f3..1b8f5f5c66b 100644 --- a/pyo3-macros-backend/src/method.rs +++ b/pyo3-macros-backend/src/method.rs @@ -720,9 +720,10 @@ impl<'a> FnSpec<'a> { // We must assign the output_span to the return value of the call, // but *not* of the call itself otherwise the spans get really weird - let ret_expr = quote! { let ret = #call; }; - let ret_var = quote_spanned! {*output_span=> ret }; - let return_conversion = quotes::map_result_into_ptr(quotes::ok_wrap(ret_var, ctx), ctx); + let ret_ident = Ident::new("ret", *output_span); + let ret_expr = quote! { let #ret_ident = #call; }; + let return_conversion = + quotes::map_result_into_ptr(quotes::ok_wrap(ret_ident.to_token_stream(), ctx), ctx); quote! { { #ret_expr diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index e9e5af6a8ac..6fba5b7e23e 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -2081,7 +2081,7 @@ impl<'a> PyClassImplsBuilder<'a> { if attr.options.extends.is_none() { quote! { impl #pyo3_path::IntoPy<#pyo3_path::PyObject> for #cls { - fn into_py(self, py: #pyo3_path::Python) -> #pyo3_path::PyObject { + fn into_py(self, py: #pyo3_path::Python<'_>) -> #pyo3_path::PyObject { #pyo3_path::IntoPy::into_py(#pyo3_path::Py::new(py, self).unwrap(), py) } } diff --git a/pyo3-macros-backend/src/quotes.rs b/pyo3-macros-backend/src/quotes.rs index 6f6f64dad20..fd7a59991cb 100644 --- a/pyo3-macros-backend/src/quotes.rs +++ b/pyo3-macros-backend/src/quotes.rs @@ -27,5 +27,6 @@ pub(crate) fn map_result_into_ptr(result: TokenStream, ctx: &Ctx) -> TokenStream output_span, } = ctx; let pyo3_path = pyo3_path.to_tokens_spanned(*output_span); - quote_spanned! {*output_span=> #pyo3_path::impl_::wrap::map_result_into_ptr(py, #result) } + let py = syn::Ident::new("py", proc_macro2::Span::call_site()); + quote_spanned! {*output_span=> #pyo3_path::impl_::wrap::map_result_into_ptr(#py, #result) } } diff --git a/src/tests/hygiene/misc.rs b/src/tests/hygiene/misc.rs index e10a3b46f38..3e1cd51422a 100644 --- a/src/tests/hygiene/misc.rs +++ b/src/tests/hygiene/misc.rs @@ -37,3 +37,22 @@ fn append_to_inittab() { crate::append_to_inittab!(module_for_inittab); } + +macro_rules! macro_rules_hygiene { + ($name_a:ident, $name_b:ident) => { + #[crate::pyclass(crate = "crate")] + struct $name_a {} + + #[crate::pymethods(crate = "crate")] + impl $name_a { + fn finalize(&mut self) -> $name_b { + $name_b {} + } + } + + #[crate::pyclass(crate = "crate")] + struct $name_b {} + }; +} + +macro_rules_hygiene!(MyClass1, MyClass2); From 6970cf5a41d84f955df1fc7150695aaff9ba554d Mon Sep 17 00:00:00 2001 From: William Takeshi Pereira Date: Sat, 27 Jul 2024 17:56:42 -0300 Subject: [PATCH 178/495] Impl partialeq for pylong (#4317) * add partialeq for types u8,...,u64 and i8,...,i64 with PyLong * implement partialeq for pylong and i128 and u128 * add changelog * remove constructors, improve partialeq implementation * revert adding sealed to pylong * add overflow tests Co-authored-by: David Hewitt * fix test_partial_eq * cargo fmt * fix clippy --------- Co-authored-by: David Hewitt --- newsfragments/4317.added.md | 1 + src/types/num.rs | 114 ++++++++++++++++++++++++++++++++- tests/test_class_attributes.rs | 2 +- 3 files changed, 115 insertions(+), 2 deletions(-) create mode 100644 newsfragments/4317.added.md diff --git a/newsfragments/4317.added.md b/newsfragments/4317.added.md new file mode 100644 index 00000000000..99849236101 --- /dev/null +++ b/newsfragments/4317.added.md @@ -0,0 +1 @@ +Implement `PartialEq` for `Bound<'py, PyInt>` with `u8`, `u16`, `u32`, `u64`, `u128`, `usize`, `i8`, `i16`, `i32`, `i64`, `i128` and `isize`. diff --git a/src/types/num.rs b/src/types/num.rs index 517e769742b..f33d85f4a1c 100644 --- a/src/types/num.rs +++ b/src/types/num.rs @@ -1,4 +1,6 @@ -use crate::{ffi, PyAny}; +use super::any::PyAnyMethods; + +use crate::{ffi, instance::Bound, PyAny}; /// Represents a Python `int` object. /// @@ -17,3 +19,113 @@ pyobject_native_type_core!(PyInt, pyobject_native_static_type_object!(ffi::PyLon /// Deprecated alias for [`PyInt`]. #[deprecated(since = "0.23.0", note = "use `PyInt` instead")] pub type PyLong = PyInt; + +macro_rules! int_compare { + ($rust_type: ty) => { + impl PartialEq<$rust_type> for Bound<'_, PyInt> { + #[inline] + fn eq(&self, other: &$rust_type) -> bool { + if let Ok(value) = self.extract::<$rust_type>() { + value == *other + } else { + false + } + } + } + impl PartialEq> for $rust_type { + #[inline] + fn eq(&self, other: &Bound<'_, PyInt>) -> bool { + if let Ok(value) = other.extract::<$rust_type>() { + value == *self + } else { + false + } + } + } + }; +} + +int_compare!(i8); +int_compare!(u8); +int_compare!(i16); +int_compare!(u16); +int_compare!(i32); +int_compare!(u32); +int_compare!(i64); +int_compare!(u64); +int_compare!(i128); +int_compare!(u128); +int_compare!(isize); +int_compare!(usize); + +#[cfg(test)] +mod tests { + use super::PyInt; + use crate::{types::PyAnyMethods, IntoPy, Python}; + + #[test] + fn test_partial_eq() { + Python::with_gil(|py| { + let v_i8 = 123i8; + let v_u8 = 123i8; + let v_i16 = 123i16; + let v_u16 = 123u16; + let v_i32 = 123i32; + let v_u32 = 123u32; + let v_i64 = 123i64; + let v_u64 = 123u64; + let v_i128 = 123i128; + let v_u128 = 123u128; + let v_isize = 123isize; + let v_usize = 123usize; + let obj = 123_i64.into_py(py).downcast_bound(py).unwrap().clone(); + assert_eq!(v_i8, obj); + assert_eq!(obj, v_i8); + + assert_eq!(v_u8, obj); + assert_eq!(obj, v_u8); + + assert_eq!(v_i16, obj); + assert_eq!(obj, v_i16); + + assert_eq!(v_u16, obj); + assert_eq!(obj, v_u16); + + assert_eq!(v_i32, obj); + assert_eq!(obj, v_i32); + + assert_eq!(v_u32, obj); + assert_eq!(obj, v_u32); + + assert_eq!(v_i64, obj); + assert_eq!(obj, v_i64); + + assert_eq!(v_u64, obj); + assert_eq!(obj, v_u64); + + assert_eq!(v_i128, obj); + assert_eq!(obj, v_i128); + + assert_eq!(v_u128, obj); + assert_eq!(obj, v_u128); + + assert_eq!(v_isize, obj); + assert_eq!(obj, v_isize); + + assert_eq!(v_usize, obj); + assert_eq!(obj, v_usize); + + let big_num = (u8::MAX as u16) + 1; + let big_obj = big_num + .into_py(py) + .into_bound(py) + .downcast_into::() + .unwrap(); + + for x in 0u8..=u8::MAX { + assert_ne!(x, big_obj); + assert_ne!(big_obj, x); + } + }); + } +} diff --git a/tests/test_class_attributes.rs b/tests/test_class_attributes.rs index 9e544211c3c..a2e099549c0 100644 --- a/tests/test_class_attributes.rs +++ b/tests/test_class_attributes.rs @@ -224,7 +224,7 @@ macro_rules! test_case { let struct_obj = struct_class.call0().unwrap(); assert!(struct_obj.setattr($renamed_field_name, 2).is_ok()); let attr = struct_obj.getattr($renamed_field_name).unwrap(); - assert_eq!(2, attr.extract().unwrap()); + assert_eq!(2, attr.extract::().unwrap()); }); } }; From 7683c4acf888205d47ae0adf0b6af00ca8936f70 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Sun, 28 Jul 2024 01:13:05 +0100 Subject: [PATCH 179/495] add back `PyTuple::new` (#4380) * add back `PyTuple::new` * Update guide/src/migration.md Co-authored-by: Icxolu <10486322+Icxolu@users.noreply.github.com> --------- Co-authored-by: Icxolu <10486322+Icxolu@users.noreply.github.com> --- guide/src/conversions/traits.md | 4 +- guide/src/migration.md | 38 +++++++++++++ guide/src/python-from-rust/function-calls.md | 2 +- guide/src/types.md | 6 +-- pytests/src/datetime.rs | 12 ++--- src/conversion.rs | 2 +- src/impl_/extract_argument.rs | 8 +-- src/impl_/pymodule.rs | 2 +- src/instance.rs | 2 +- src/types/datetime.rs | 2 +- src/types/list.rs | 2 +- src/types/sequence.rs | 8 +-- src/types/tuple.rs | 56 ++++++++++++++------ src/types/typeobject.rs | 6 +-- tests/test_frompyobject.rs | 8 +-- tests/test_various.rs | 6 +-- 16 files changed, 110 insertions(+), 54 deletions(-) diff --git a/guide/src/conversions/traits.md b/guide/src/conversions/traits.md index 95d16faaaa6..fde2e354012 100644 --- a/guide/src/conversions/traits.md +++ b/guide/src/conversions/traits.md @@ -181,7 +181,7 @@ struct RustyTuple(String, String); # use pyo3::types::PyTuple; # fn main() -> PyResult<()> { # Python::with_gil(|py| -> PyResult<()> { -# let tuple = PyTuple::new_bound(py, vec!["test", "test2"]); +# let tuple = PyTuple::new(py, vec!["test", "test2"]); # # let rustytuple: RustyTuple = tuple.extract()?; # assert_eq!(rustytuple.0, "test"); @@ -204,7 +204,7 @@ struct RustyTuple((String,)); # use pyo3::types::PyTuple; # fn main() -> PyResult<()> { # Python::with_gil(|py| -> PyResult<()> { -# let tuple = PyTuple::new_bound(py, vec!["test"]); +# let tuple = PyTuple::new(py, vec!["test"]); # # let rustytuple: RustyTuple = tuple.extract()?; # assert_eq!((rustytuple.0).0, "test"); diff --git a/guide/src/migration.md b/guide/src/migration.md index 113560ecaea..671a3333990 100644 --- a/guide/src/migration.md +++ b/guide/src/migration.md @@ -3,6 +3,44 @@ This guide can help you upgrade code through breaking changes from one PyO3 version to the next. For a detailed list of all changes, see the [CHANGELOG](changelog.md). +## from 0.22.* to 0.23 + +### `gil-refs` feature removed +
+Click to expand + +PyO3 0.23 completes the removal of the "GIL Refs" API in favour of the new "Bound" API introduced in PyO3 0.21. + +With the removal of the old API, many "Bound" API functions which had been introduced with `_bound` suffixes no longer need the suffixes as these names has been freed up. For example, `PyTuple::new_bound` is now just `PyTuple::new` (the existing name remains but is deprecated). + +Before: + +```rust +# #![allow(deprecated)] +# use pyo3::prelude::*; +# use pyo3::types::PyTuple; +# fn main() { +# Python::with_gil(|py| { +// For example, for PyTuple. Many such APIs have been changed. +let tup = PyTuple::new_bound(py, [1, 2, 3]); +# }) +# } +``` + +After: + +```rust +# use pyo3::prelude::*; +# use pyo3::types::PyTuple; +# fn main() { +# Python::with_gil(|py| { +// For example, for PyTuple. Many such APIs have been changed. +let tup = PyTuple::new(py, [1, 2, 3]); +# }) +# } +``` +
+ ## from 0.21.* to 0.22 ### Deprecation of `gil-refs` feature continues diff --git a/guide/src/python-from-rust/function-calls.md b/guide/src/python-from-rust/function-calls.md index c19d6fafabc..fa9c047609a 100644 --- a/guide/src/python-from-rust/function-calls.md +++ b/guide/src/python-from-rust/function-calls.md @@ -49,7 +49,7 @@ fn main() -> PyResult<()> { fun.call1(py, args)?; // call object with Python tuple of positional arguments - let args = PyTuple::new_bound(py, &[arg1, arg2, arg3]); + let args = PyTuple::new(py, &[arg1, arg2, arg3]); fun.call1(py, args)?; Ok(()) }) diff --git a/guide/src/types.md b/guide/src/types.md index ee46b97ebcd..5a011ddbe5d 100644 --- a/guide/src/types.md +++ b/guide/src/types.md @@ -145,7 +145,7 @@ use pyo3::types::PyTuple; # fn example<'py>(py: Python<'py>) -> PyResult<()> { // Create a new tuple with the elements (0, 1, 2) -let t = PyTuple::new_bound(py, [0, 1, 2]); +let t = PyTuple::new(py, [0, 1, 2]); for i in 0..=2 { let entry: Borrowed<'_, 'py, PyAny> = t.get_borrowed_item(i)?; // `PyAnyMethods::extract` is available on `Borrowed` @@ -250,7 +250,7 @@ For example, the following snippet shows how to cast `Bound<'py, PyAny>` to `Bou # use pyo3::types::PyTuple; # fn example<'py>(py: Python<'py>) -> PyResult<()> { // create a new Python `tuple`, and use `.into_any()` to erase the type -let obj: Bound<'py, PyAny> = PyTuple::empty_bound(py).into_any(); +let obj: Bound<'py, PyAny> = PyTuple::empty(py).into_any(); // use `.downcast()` to cast to `PyTuple` without transferring ownership let _: &Bound<'py, PyTuple> = obj.downcast()?; @@ -295,7 +295,7 @@ For example, the following snippet extracts a Rust tuple of integers from a Pyth # use pyo3::types::PyTuple; # fn example<'py>(py: Python<'py>) -> PyResult<()> { // create a new Python `tuple`, and use `.into_any()` to erase the type -let obj: Bound<'py, PyAny> = PyTuple::new_bound(py, [1, 2, 3]).into_any(); +let obj: Bound<'py, PyAny> = PyTuple::new(py, [1, 2, 3]).into_any(); // extracting the Python `tuple` to a rust `(i32, i32, i32)` tuple let (x, y, z) = obj.extract::<(i32, i32, i32)>()?; diff --git a/pytests/src/datetime.rs b/pytests/src/datetime.rs index e26782d04f7..4e505caa0f8 100644 --- a/pytests/src/datetime.rs +++ b/pytests/src/datetime.rs @@ -13,7 +13,7 @@ fn make_date(py: Python<'_>, year: i32, month: u8, day: u8) -> PyResult(d: &Bound<'py, PyDate>) -> Bound<'py, PyTuple> { - PyTuple::new_bound( + PyTuple::new( d.py(), [d.get_year(), d.get_month() as i32, d.get_day() as i32], ) @@ -53,7 +53,7 @@ fn time_with_fold<'py>( #[pyfunction] fn get_time_tuple<'py>(dt: &Bound<'py, PyTime>) -> Bound<'py, PyTuple> { - PyTuple::new_bound( + PyTuple::new( dt.py(), [ dt.get_hour() as u32, @@ -66,7 +66,7 @@ fn get_time_tuple<'py>(dt: &Bound<'py, PyTime>) -> Bound<'py, PyTuple> { #[pyfunction] fn get_time_tuple_fold<'py>(dt: &Bound<'py, PyTime>) -> Bound<'py, PyTuple> { - PyTuple::new_bound( + PyTuple::new( dt.py(), [ dt.get_hour() as u32, @@ -90,7 +90,7 @@ fn make_delta( #[pyfunction] fn get_delta_tuple<'py>(delta: &Bound<'py, PyDelta>) -> Bound<'py, PyTuple> { - PyTuple::new_bound( + PyTuple::new( delta.py(), [ delta.get_days(), @@ -129,7 +129,7 @@ fn make_datetime<'py>( #[pyfunction] fn get_datetime_tuple<'py>(dt: &Bound<'py, PyDateTime>) -> Bound<'py, PyTuple> { - PyTuple::new_bound( + PyTuple::new( dt.py(), [ dt.get_year(), @@ -145,7 +145,7 @@ fn get_datetime_tuple<'py>(dt: &Bound<'py, PyDateTime>) -> Bound<'py, PyTuple> { #[pyfunction] fn get_datetime_tuple_fold<'py>(dt: &Bound<'py, PyDateTime>) -> Bound<'py, PyTuple> { - PyTuple::new_bound( + PyTuple::new( dt.py(), [ dt.get_year(), diff --git a/src/conversion.rs b/src/conversion.rs index 596c9647431..d50d8f45ae9 100644 --- a/src/conversion.rs +++ b/src/conversion.rs @@ -367,7 +367,7 @@ where /// Converts `()` to an empty Python tuple. impl IntoPy> for () { fn into_py(self, py: Python<'_>) -> Py { - PyTuple::empty_bound(py).unbind() + PyTuple::empty(py).unbind() } } diff --git a/src/impl_/extract_argument.rs b/src/impl_/extract_argument.rs index 4945d977dd9..42b936a5ec7 100644 --- a/src/impl_/extract_argument.rs +++ b/src/impl_/extract_argument.rs @@ -709,7 +709,7 @@ impl<'py> VarargsHandler<'py> for TupleVarargs { varargs: &[Option>], _function_description: &FunctionDescription, ) -> PyResult { - Ok(PyTuple::new_bound(py, varargs)) + Ok(PyTuple::new(py, varargs)) } #[inline] @@ -807,7 +807,7 @@ mod tests { }; Python::with_gil(|py| { - let args = PyTuple::empty_bound(py); + let args = PyTuple::empty(py); let kwargs = [("foo", 0u8)].into_py_dict_bound(py); let err = unsafe { function_description @@ -838,7 +838,7 @@ mod tests { }; Python::with_gil(|py| { - let args = PyTuple::empty_bound(py); + let args = PyTuple::empty(py); let kwargs = [(1u8, 1u8)].into_py_dict_bound(py); let err = unsafe { function_description @@ -869,7 +869,7 @@ mod tests { }; Python::with_gil(|py| { - let args = PyTuple::empty_bound(py); + let args = PyTuple::empty(py); let mut output = [None, None]; let err = unsafe { function_description.extract_arguments_tuple_dict::( diff --git a/src/impl_/pymodule.rs b/src/impl_/pymodule.rs index b05652bced8..2cdb1bff8f8 100644 --- a/src/impl_/pymodule.rs +++ b/src/impl_/pymodule.rs @@ -95,7 +95,7 @@ impl ModuleDef { .import_bound("sys")? .getattr("implementation")? .getattr("version")?; - if version.lt(crate::types::PyTuple::new_bound(py, PYPY_GOOD_VERSION))? { + if version.lt(crate::types::PyTuple::new(py, PYPY_GOOD_VERSION))? { let warn = py.import_bound("warnings")?.getattr("warn")?; warn.call1(( "PyPy 3.7 versions older than 7.3.8 are known to have binary \ diff --git a/src/instance.rs b/src/instance.rs index 51c4d62b2d5..2287f5d3700 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -585,7 +585,7 @@ impl<'py, T> Borrowed<'_, 'py, T> { /// /// # fn main() -> PyResult<()> { /// Python::with_gil(|py| -> PyResult<()> { - /// let tuple = PyTuple::new_bound(py, [1, 2, 3]); + /// let tuple = PyTuple::new(py, [1, 2, 3]); /// /// // borrows from `tuple`, so can only be /// // used while `tuple` stays alive diff --git a/src/types/datetime.rs b/src/types/datetime.rs index 72b3bf886f2..a70cb6c885e 100644 --- a/src/types/datetime.rs +++ b/src/types/datetime.rs @@ -208,7 +208,7 @@ impl PyDate { /// /// This is equivalent to `datetime.date.fromtimestamp` pub fn from_timestamp_bound(py: Python<'_>, timestamp: i64) -> PyResult> { - let time_tuple = PyTuple::new_bound(py, [timestamp]); + let time_tuple = PyTuple::new(py, [timestamp]); // safety ensure that the API is loaded let _api = ensure_datetime_api(py)?; diff --git a/src/types/list.rs b/src/types/list.rs index fa1eb876353..ae08798f946 100644 --- a/src/types/list.rs +++ b/src/types/list.rs @@ -1046,7 +1046,7 @@ mod tests { Python::with_gil(|py| { let list = PyList::new_bound(py, vec![1, 2, 3]); let tuple = list.to_tuple(); - let tuple_expected = PyTuple::new_bound(py, vec![1, 2, 3]); + let tuple_expected = PyTuple::new(py, vec![1, 2, 3]); assert!(tuple.eq(tuple_expected).unwrap()); }) } diff --git a/src/types/sequence.rs b/src/types/sequence.rs index d2a6b6b3806..1b71a186b57 100644 --- a/src/types/sequence.rs +++ b/src/types/sequence.rs @@ -735,7 +735,7 @@ mod tests { assert!(seq .to_tuple() .unwrap() - .eq(PyTuple::new_bound(py, ["foo", "bar"])) + .eq(PyTuple::new(py, ["foo", "bar"])) .unwrap()); }); } @@ -746,11 +746,7 @@ mod tests { let v = vec!["foo", "bar"]; let ob = v.to_object(py); let seq = ob.downcast_bound::(py).unwrap(); - assert!(seq - .to_tuple() - .unwrap() - .eq(PyTuple::new_bound(py, &v)) - .unwrap()); + assert!(seq.to_tuple().unwrap().eq(PyTuple::new(py, &v)).unwrap()); }); } diff --git a/src/types/tuple.rs b/src/types/tuple.rs index ed99d40c22f..3d67a062cde 100644 --- a/src/types/tuple.rs +++ b/src/types/tuple.rs @@ -76,7 +76,7 @@ impl PyTuple { /// # fn main() { /// Python::with_gil(|py| { /// let elements: Vec = vec![0, 1, 2, 3, 4, 5]; - /// let tuple = PyTuple::new_bound(py, elements); + /// let tuple = PyTuple::new(py, elements); /// assert_eq!(format!("{:?}", tuple), "(0, 1, 2, 3, 4, 5)"); /// }); /// # } @@ -88,7 +88,7 @@ impl PyTuple { /// All standard library structures implement this trait correctly, if they do, so calling this /// function using [`Vec`]`` or `&[T]` will always succeed. #[track_caller] - pub fn new_bound( + pub fn new( py: Python<'_>, elements: impl IntoIterator, ) -> Bound<'_, PyTuple> @@ -100,14 +100,36 @@ impl PyTuple { new_from_iter(py, &mut elements) } + /// Deprecated name for [`PyTuple::new`]. + #[deprecated(since = "0.23.0", note = "renamed to `PyTuple::new`")] + #[track_caller] + #[inline] + pub fn new_bound( + py: Python<'_>, + elements: impl IntoIterator, + ) -> Bound<'_, PyTuple> + where + T: ToPyObject, + U: ExactSizeIterator, + { + PyTuple::new(py, elements) + } + /// Constructs an empty tuple (on the Python side, a singleton object). - pub fn empty_bound(py: Python<'_>) -> Bound<'_, PyTuple> { + pub fn empty(py: Python<'_>) -> Bound<'_, PyTuple> { unsafe { ffi::PyTuple_New(0) .assume_owned(py) .downcast_into_unchecked() } } + + /// Deprecated name for [`PyTuple::empty`]. + #[deprecated(since = "0.23.0", note = "renamed to `PyTuple::empty`")] + #[inline] + pub fn empty_bound(py: Python<'_>) -> Bound<'_, PyTuple> { + PyTuple::empty(py) + } } /// Implementation of functionality for [`PyTuple`]. @@ -664,7 +686,7 @@ mod tests { #[test] fn test_new() { Python::with_gil(|py| { - let ob = PyTuple::new_bound(py, [1, 2, 3]); + let ob = PyTuple::new(py, [1, 2, 3]); assert_eq!(3, ob.len()); let ob = ob.as_any(); assert_eq!((1, 2, 3), ob.extract().unwrap()); @@ -672,7 +694,7 @@ mod tests { let mut map = HashSet::new(); map.insert(1); map.insert(2); - PyTuple::new_bound(py, map); + PyTuple::new(py, map); }); } @@ -691,7 +713,7 @@ mod tests { #[test] fn test_empty() { Python::with_gil(|py| { - let tuple = PyTuple::empty_bound(py); + let tuple = PyTuple::empty(py); assert!(tuple.is_empty()); assert_eq!(0, tuple.len()); }); @@ -700,7 +722,7 @@ mod tests { #[test] fn test_slice() { Python::with_gil(|py| { - let tup = PyTuple::new_bound(py, [2, 3, 5, 7]); + let tup = PyTuple::new(py, [2, 3, 5, 7]); let slice = tup.get_slice(1, 3); assert_eq!(2, slice.len()); let slice = tup.get_slice(1, 7); @@ -759,7 +781,7 @@ mod tests { #[test] fn test_bound_iter() { Python::with_gil(|py| { - let tuple = PyTuple::new_bound(py, [1, 2, 3]); + let tuple = PyTuple::new(py, [1, 2, 3]); assert_eq!(3, tuple.len()); let mut iter = tuple.iter(); @@ -782,7 +804,7 @@ mod tests { #[test] fn test_bound_iter_rev() { Python::with_gil(|py| { - let tuple = PyTuple::new_bound(py, [1, 2, 3]); + let tuple = PyTuple::new(py, [1, 2, 3]); assert_eq!(3, tuple.len()); let mut iter = tuple.iter().rev(); @@ -1005,7 +1027,7 @@ mod tests { fn too_long_iterator() { Python::with_gil(|py| { let iter = FaultyIter(0..usize::MAX, 73); - let _tuple = PyTuple::new_bound(py, iter); + let _tuple = PyTuple::new(py, iter); }) } @@ -1016,7 +1038,7 @@ mod tests { fn too_short_iterator() { Python::with_gil(|py| { let iter = FaultyIter(0..35, 73); - let _tuple = PyTuple::new_bound(py, iter); + let _tuple = PyTuple::new(py, iter); }) } @@ -1028,7 +1050,7 @@ mod tests { Python::with_gil(|py| { let iter = FaultyIter(0..0, usize::MAX); - let _tuple = PyTuple::new_bound(py, iter); + let _tuple = PyTuple::new(py, iter); }) } @@ -1088,7 +1110,7 @@ mod tests { Python::with_gil(|py| { std::panic::catch_unwind(|| { let iter = FaultyIter(0..50, 50); - let _tuple = PyTuple::new_bound(py, iter); + let _tuple = PyTuple::new(py, iter); }) .unwrap_err(); }); @@ -1154,7 +1176,7 @@ mod tests { #[test] fn test_tuple_to_list() { Python::with_gil(|py| { - let tuple = PyTuple::new_bound(py, vec![1, 2, 3]); + let tuple = PyTuple::new(py, vec![1, 2, 3]); let list = tuple.to_list(); let list_expected = PyList::new_bound(py, vec![1, 2, 3]); assert!(list.eq(list_expected).unwrap()); @@ -1164,7 +1186,7 @@ mod tests { #[test] fn test_tuple_as_sequence() { Python::with_gil(|py| { - let tuple = PyTuple::new_bound(py, vec![1, 2, 3]); + let tuple = PyTuple::new(py, vec![1, 2, 3]); let sequence = tuple.as_sequence(); assert!(tuple.get_item(0).unwrap().eq(1).unwrap()); assert!(sequence.get_item(0).unwrap().eq(1).unwrap()); @@ -1177,7 +1199,7 @@ mod tests { #[test] fn test_tuple_into_sequence() { Python::with_gil(|py| { - let tuple = PyTuple::new_bound(py, vec![1, 2, 3]); + let tuple = PyTuple::new(py, vec![1, 2, 3]); let sequence = tuple.into_sequence(); assert!(sequence.get_item(0).unwrap().eq(1).unwrap()); assert_eq!(sequence.len().unwrap(), 3); @@ -1187,7 +1209,7 @@ mod tests { #[test] fn test_bound_tuple_get_item() { Python::with_gil(|py| { - let tuple = PyTuple::new_bound(py, vec![1, 2, 3, 4]); + let tuple = PyTuple::new(py, vec![1, 2, 3, 4]); assert_eq!(tuple.len(), 4); assert_eq!(tuple.get_item(0).unwrap().extract::().unwrap(), 1); diff --git a/src/types/typeobject.rs b/src/types/typeobject.rs index 9c2d8c17334..609e12089ce 100644 --- a/src/types/typeobject.rs +++ b/src/types/typeobject.rs @@ -272,7 +272,7 @@ mod tests { assert!(py .get_type_bound::() .mro() - .eq(PyTuple::new_bound( + .eq(PyTuple::new( py, [ py.get_type_bound::(), @@ -290,7 +290,7 @@ mod tests { assert!(py .get_type_bound::() .bases() - .eq(PyTuple::new_bound(py, [py.get_type_bound::()])) + .eq(PyTuple::new(py, [py.get_type_bound::()])) .unwrap()); }); } @@ -301,7 +301,7 @@ mod tests { assert!(py .get_type_bound::() .bases() - .eq(PyTuple::empty_bound(py)) + .eq(PyTuple::empty(py)) .unwrap()); }); } diff --git a/tests/test_frompyobject.rs b/tests/test_frompyobject.rs index 5c57a954023..9afe22141d2 100644 --- a/tests/test_frompyobject.rs +++ b/tests/test_frompyobject.rs @@ -168,10 +168,10 @@ pub struct Tuple(String, usize); #[test] fn test_tuple_struct() { Python::with_gil(|py| { - let tup = PyTuple::new_bound(py, &[1.into_py(py), "test".into_py(py)]); + let tup = PyTuple::new(py, &[1.into_py(py), "test".into_py(py)]); let tup = tup.extract::(); assert!(tup.is_err()); - let tup = PyTuple::new_bound(py, &["test".into_py(py), 1.into_py(py)]); + let tup = PyTuple::new(py, &["test".into_py(py), 1.into_py(py)]); let tup = tup .extract::() .expect("Failed to extract Tuple from PyTuple"); @@ -333,7 +333,7 @@ pub struct PyBool { #[test] fn test_enum() { Python::with_gil(|py| { - let tup = PyTuple::new_bound(py, &[1.into_py(py), "test".into_py(py)]); + let tup = PyTuple::new(py, &[1.into_py(py), "test".into_py(py)]); let f = tup .extract::>() .expect("Failed to extract Foo from tuple"); @@ -424,7 +424,7 @@ TypeError: failed to extract enum Foo ('TupleVar | StructVar | TransparentTuple - variant StructWithGetItemArg (StructWithGetItemArg): KeyError: 'foo'" ); - let tup = PyTuple::empty_bound(py); + let tup = PyTuple::empty(py); let err = tup.extract::>().unwrap_err(); assert_eq!( err.to_string(), diff --git a/tests/test_various.rs b/tests/test_various.rs index 64e49fd0a6e..b7c1ea70cfa 100644 --- a/tests/test_various.rs +++ b/tests/test_various.rs @@ -91,7 +91,7 @@ fn intopytuple_pyclass() { #[test] fn pytuple_primitive_iter() { Python::with_gil(|py| { - let tup = PyTuple::new_bound(py, [1u32, 2, 3].iter()); + let tup = PyTuple::new(py, [1u32, 2, 3].iter()); py_assert!(py, tup, "tup == (1, 2, 3)"); }); } @@ -99,7 +99,7 @@ fn pytuple_primitive_iter() { #[test] fn pytuple_pyclass_iter() { Python::with_gil(|py| { - let tup = PyTuple::new_bound( + let tup = PyTuple::new( py, [ Py::new(py, SimplePyClass {}).unwrap(), @@ -134,7 +134,7 @@ fn test_pickle() { ) -> PyResult<(PyObject, Bound<'py, PyTuple>, PyObject)> { let cls = slf.to_object(py).getattr(py, "__class__")?; let dict = slf.to_object(py).getattr(py, "__dict__")?; - Ok((cls, PyTuple::empty_bound(py), dict)) + Ok((cls, PyTuple::empty(py), dict)) } } From 5fc5df4c32be719998e0ffcb0aa77b7ac0a81235 Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Sun, 28 Jul 2024 15:15:18 +0200 Subject: [PATCH 180/495] reintroduce `PyList` constructors (#4386) --- guide/src/conversions/traits.md | 2 +- guide/src/exception.md | 2 +- guide/src/performance.md | 4 +- guide/src/trait-bounds.md | 10 +-- guide/src/types.md | 6 +- pyo3-benches/benches/bench_frompyobject.rs | 4 +- pyo3-benches/benches/bench_list.rs | 10 +-- pyo3-benches/benches/bench_tuple.rs | 2 +- src/conversions/smallvec.rs | 6 +- src/lib.rs | 2 +- src/macros.rs | 2 +- src/marker.rs | 4 +- src/sync.rs | 2 +- src/tests/hygiene/pymethods.rs | 2 +- src/types/any.rs | 4 +- src/types/dict.rs | 4 +- src/types/iterator.rs | 2 +- src/types/list.rs | 96 +++++++++++++--------- src/types/module.rs | 2 +- src/types/sequence.rs | 10 +-- src/types/tuple.rs | 2 +- tests/test_frompyobject.rs | 2 +- tests/test_getter_setter.rs | 2 +- tests/test_methods.rs | 2 +- tests/test_proto_methods.rs | 4 +- 25 files changed, 103 insertions(+), 85 deletions(-) diff --git a/guide/src/conversions/traits.md b/guide/src/conversions/traits.md index fde2e354012..c622bae58d2 100644 --- a/guide/src/conversions/traits.md +++ b/guide/src/conversions/traits.md @@ -13,7 +13,7 @@ fails, so usually you will use something like # use pyo3::types::PyList; # fn main() -> PyResult<()> { # Python::with_gil(|py| { -# let list = PyList::new_bound(py, b"foo"); +# let list = PyList::new(py, b"foo"); let v: Vec = list.extract()?; # assert_eq!(&v, &[102, 111, 111]); # Ok(()) diff --git a/guide/src/exception.md b/guide/src/exception.md index 1a68e24086f..bf979237f01 100644 --- a/guide/src/exception.md +++ b/guide/src/exception.md @@ -80,7 +80,7 @@ use pyo3::types::{PyBool, PyList}; Python::with_gil(|py| { assert!(PyBool::new_bound(py, true).is_instance_of::()); - let list = PyList::new_bound(py, &[1, 2, 3, 4]); + let list = PyList::new(py, &[1, 2, 3, 4]); assert!(!list.is_instance_of::()); assert!(list.is_instance_of::()); }); diff --git a/guide/src/performance.md b/guide/src/performance.md index b3d160fe6b1..fb2288dd566 100644 --- a/guide/src/performance.md +++ b/guide/src/performance.md @@ -108,7 +108,7 @@ This limitation is important to keep in mind when this setting is used, especial ```rust,ignore # use pyo3::prelude::*; # use pyo3::types::PyList; -let numbers: Py = Python::with_gil(|py| PyList::empty_bound(py).unbind()); +let numbers: Py = Python::with_gil(|py| PyList::empty(py).unbind()); Python::with_gil(|py| { numbers.bind(py).append(23).unwrap(); @@ -124,7 +124,7 @@ will abort if the list not explicitly disposed via ```rust # use pyo3::prelude::*; # use pyo3::types::PyList; -let numbers: Py = Python::with_gil(|py| PyList::empty_bound(py).unbind()); +let numbers: Py = Python::with_gil(|py| PyList::empty(py).unbind()); Python::with_gil(|py| { numbers.bind(py).append(23).unwrap(); diff --git a/guide/src/trait-bounds.md b/guide/src/trait-bounds.md index eb67bc42413..434ac7dd968 100644 --- a/guide/src/trait-bounds.md +++ b/guide/src/trait-bounds.md @@ -84,7 +84,7 @@ impl Model for UserModel { Python::with_gil(|py| { self.model .bind(py) - .call_method("set_variables", (PyList::new_bound(py, var),), None) + .call_method("set_variables", (PyList::new(py, var),), None) .unwrap(); }) } @@ -182,7 +182,7 @@ This wrapper will also perform the type conversions between Python and Rust. # println!("Rust calling Python to set the variables"); # Python::with_gil(|py| { # self.model.bind(py) -# .call_method("set_variables", (PyList::new_bound(py, var),), None) +# .call_method("set_variables", (PyList::new(py, var),), None) # .unwrap(); # }) # } @@ -360,7 +360,7 @@ impl Model for UserModel { # println!("Rust calling Python to set the variables"); # Python::with_gil(|py| { # self.model.bind(py) -# .call_method("set_variables", (PyList::new_bound(py, var),), None) +# .call_method("set_variables", (PyList::new(py, var),), None) # .unwrap(); # }) # } @@ -419,7 +419,7 @@ impl Model for UserModel { # println!("Rust calling Python to set the variables"); # Python::with_gil(|py| { # let py_model = self.model.bind(py) -# .call_method("set_variables", (PyList::new_bound(py, var),), None) +# .call_method("set_variables", (PyList::new(py, var),), None) # .unwrap(); # }) # } @@ -517,7 +517,7 @@ impl Model for UserModel { Python::with_gil(|py| { self.model .bind(py) - .call_method("set_variables", (PyList::new_bound(py, var),), None) + .call_method("set_variables", (PyList::new(py, var),), None) .unwrap(); }) } diff --git a/guide/src/types.md b/guide/src/types.md index 5a011ddbe5d..131cb0ed119 100644 --- a/guide/src/types.md +++ b/guide/src/types.md @@ -61,7 +61,7 @@ use pyo3::prelude::*; use pyo3::types::PyList; fn example<'py>(py: Python<'py>) -> PyResult<()> { - let x: Bound<'py, PyList> = PyList::empty_bound(py); + let x: Bound<'py, PyList> = PyList::empty(py); x.append(1)?; let y: Bound<'py, PyList> = x.clone(); // y is a new reference to the same list drop(x); // release the original reference x @@ -77,7 +77,7 @@ use pyo3::prelude::*; use pyo3::types::PyList; fn example(py: Python<'_>) -> PyResult<()> { - let x = PyList::empty_bound(py); + let x = PyList::empty(py); x.append(1)?; let y = x.clone(); drop(x); @@ -232,7 +232,7 @@ fn get_first_item<'py>(list: &Bound<'py, PyList>) -> PyResult> list.get_item(0) } # Python::with_gil(|py| { -# let l = PyList::new_bound(py, ["hello world"]); +# let l = PyList::new(py, ["hello world"]); # assert!(get_first_item(&l).unwrap().eq("hello world").unwrap()); # }) ``` diff --git a/pyo3-benches/benches/bench_frompyobject.rs b/pyo3-benches/benches/bench_frompyobject.rs index f53f116a154..3cb21639b68 100644 --- a/pyo3-benches/benches/bench_frompyobject.rs +++ b/pyo3-benches/benches/bench_frompyobject.rs @@ -25,7 +25,7 @@ fn enum_from_pyobject(b: &mut Bencher<'_>) { fn list_via_downcast(b: &mut Bencher<'_>) { Python::with_gil(|py| { - let any = PyList::empty_bound(py).into_any(); + let any = PyList::empty(py).into_any(); b.iter(|| black_box(&any).downcast::().unwrap()); }) @@ -33,7 +33,7 @@ fn list_via_downcast(b: &mut Bencher<'_>) { fn list_via_extract(b: &mut Bencher<'_>) { Python::with_gil(|py| { - let any = PyList::empty_bound(py).into_any(); + let any = PyList::empty(py).into_any(); b.iter(|| black_box(&any).extract::>().unwrap()); }) diff --git a/pyo3-benches/benches/bench_list.rs b/pyo3-benches/benches/bench_list.rs index dcbdb4779cb..4f6374d75eb 100644 --- a/pyo3-benches/benches/bench_list.rs +++ b/pyo3-benches/benches/bench_list.rs @@ -8,7 +8,7 @@ use pyo3::types::{PyList, PySequence}; fn iter_list(b: &mut Bencher<'_>) { Python::with_gil(|py| { const LEN: usize = 100_000; - let list = PyList::new_bound(py, 0..LEN); + let list = PyList::new(py, 0..LEN); let mut sum = 0; b.iter(|| { for x in &list { @@ -22,14 +22,14 @@ fn iter_list(b: &mut Bencher<'_>) { fn list_new(b: &mut Bencher<'_>) { Python::with_gil(|py| { const LEN: usize = 50_000; - b.iter_with_large_drop(|| PyList::new_bound(py, 0..LEN)); + b.iter_with_large_drop(|| PyList::new(py, 0..LEN)); }); } fn list_get_item(b: &mut Bencher<'_>) { Python::with_gil(|py| { const LEN: usize = 50_000; - let list = PyList::new_bound(py, 0..LEN); + let list = PyList::new(py, 0..LEN); let mut sum = 0; b.iter(|| { for i in 0..LEN { @@ -43,7 +43,7 @@ fn list_get_item(b: &mut Bencher<'_>) { fn list_get_item_unchecked(b: &mut Bencher<'_>) { Python::with_gil(|py| { const LEN: usize = 50_000; - let list = PyList::new_bound(py, 0..LEN); + let list = PyList::new(py, 0..LEN); let mut sum = 0; b.iter(|| { for i in 0..LEN { @@ -58,7 +58,7 @@ fn list_get_item_unchecked(b: &mut Bencher<'_>) { fn sequence_from_list(b: &mut Bencher<'_>) { Python::with_gil(|py| { const LEN: usize = 50_000; - let list = &PyList::new_bound(py, 0..LEN); + let list = &PyList::new(py, 0..LEN); b.iter(|| black_box(list).downcast::().unwrap()); }); } diff --git a/pyo3-benches/benches/bench_tuple.rs b/pyo3-benches/benches/bench_tuple.rs index 3c0b56a0234..e2985cc998f 100644 --- a/pyo3-benches/benches/bench_tuple.rs +++ b/pyo3-benches/benches/bench_tuple.rs @@ -103,7 +103,7 @@ fn tuple_new_list(b: &mut Bencher<'_>) { Python::with_gil(|py| { const LEN: usize = 50_000; let tuple = PyTuple::new_bound(py, 0..LEN); - b.iter_with_large_drop(|| PyList::new_bound(py, tuple.iter_borrowed())); + b.iter_with_large_drop(|| PyList::new(py, tuple.iter_borrowed())); }); } diff --git a/src/conversions/smallvec.rs b/src/conversions/smallvec.rs index 96dbfad14b7..80e6b09d611 100644 --- a/src/conversions/smallvec.rs +++ b/src/conversions/smallvec.rs @@ -104,7 +104,7 @@ mod tests { Python::with_gil(|py| { let sv: SmallVec<[u64; 8]> = [1, 2, 3, 4, 5].iter().cloned().collect(); let hso: PyObject = sv.clone().into_py(py); - let l = PyList::new_bound(py, [1, 2, 3, 4, 5]); + let l = PyList::new(py, [1, 2, 3, 4, 5]); assert!(l.eq(hso).unwrap()); }); } @@ -112,7 +112,7 @@ mod tests { #[test] fn test_smallvec_from_py_object() { Python::with_gil(|py| { - let l = PyList::new_bound(py, [1, 2, 3, 4, 5]); + 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]); }); @@ -135,7 +135,7 @@ mod tests { Python::with_gil(|py| { let sv: SmallVec<[u64; 8]> = [1, 2, 3, 4, 5].iter().cloned().collect(); let hso: PyObject = sv.to_object(py); - let l = PyList::new_bound(py, [1, 2, 3, 4, 5]); + let l = PyList::new(py, [1, 2, 3, 4, 5]); assert!(l.eq(hso).unwrap()); }); } diff --git a/src/lib.rs b/src/lib.rs index fdba3f842e6..16f7200519c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -41,7 +41,7 @@ //! - Types that also have the `'py` lifetime, such as the [`Bound<'py, T>`](Bound) smart pointer, are //! bound to the Python GIL and rely on this to offer their functionality. These types often //! have a [`.py()`](Bound::py) method to get the associated [`Python<'py>`](Python) token. -//! - Functions which depend on the `'py` lifetime, such as [`PyList::new_bound`](types::PyList::new_bound), +//! - Functions which depend on the `'py` lifetime, such as [`PyList::new`](types::PyList::new), //! require a [`Python<'py>`](Python) token as an input. Sometimes the token is passed implicitly by //! taking a [`Bound<'py, T>`](Bound) or other type which is bound to the `'py` lifetime. //! - Traits which depend on the `'py` lifetime, such as [`FromPyObject<'py>`](FromPyObject), usually have diff --git a/src/macros.rs b/src/macros.rs index 79b65c17b45..51c54621850 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -12,7 +12,7 @@ /// use pyo3::{prelude::*, py_run, types::PyList}; /// /// Python::with_gil(|py| { -/// let list = PyList::new_bound(py, &[1, 2, 3]); +/// let list = PyList::new(py, &[1, 2, 3]); /// py_run!(py, list, "assert list == [1, 2, 3]"); /// }); /// ``` diff --git a/src/marker.rs b/src/marker.rs index bc0e22e37e7..c42c9722509 100644 --- a/src/marker.rs +++ b/src/marker.rs @@ -882,7 +882,7 @@ mod tests { // If allow_threads is implemented correctly, this thread still owns the GIL here // so the following Python calls should not cause crashes. - let list = PyList::new_bound(py, [1, 2, 3, 4]); + let list = PyList::new(py, [1, 2, 3, 4]); assert_eq!(list.extract::>().unwrap(), vec![1, 2, 3, 4]); }); } @@ -890,7 +890,7 @@ mod tests { #[cfg(not(pyo3_disable_reference_pool))] #[test] fn test_allow_threads_pass_stuff_in() { - let list = Python::with_gil(|py| PyList::new_bound(py, vec!["foo", "bar"]).unbind()); + let list = Python::with_gil(|py| PyList::new(py, vec!["foo", "bar"]).unbind()); let mut v = vec![1, 2, 3]; let a = std::sync::Arc::new(String::from("foo")); diff --git a/src/sync.rs b/src/sync.rs index dee39fdfd83..4c4c8fb5dec 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -87,7 +87,7 @@ unsafe impl Sync for GILProtected where T: Send {} /// /// pub fn get_shared_list(py: Python<'_>) -> &Bound<'_, PyList> { /// LIST_CELL -/// .get_or_init(py, || PyList::empty_bound(py).unbind()) +/// .get_or_init(py, || PyList::empty(py).unbind()) /// .bind(py) /// } /// # Python::with_gil(|py| assert_eq!(get_shared_list(py).len(), 0)); diff --git a/src/tests/hygiene/pymethods.rs b/src/tests/hygiene/pymethods.rs index b6d090d9aa8..5abfc856ea2 100644 --- a/src/tests/hygiene/pymethods.rs +++ b/src/tests/hygiene/pymethods.rs @@ -73,7 +73,7 @@ impl Dummy { fn __delattr__(&mut self, name: ::std::string::String) {} fn __dir__<'py>(&self, py: crate::Python<'py>) -> crate::Bound<'py, crate::types::PyList> { - crate::types::PyList::new_bound(py, ::std::vec![0_u8]) + crate::types::PyList::new(py, ::std::vec![0_u8]) } ////////////////////// diff --git a/src/types/any.rs b/src/types/any.rs index 626feba0856..dd8edd885fb 100644 --- a/src/types/any.rs +++ b/src/types/any.rs @@ -1938,10 +1938,10 @@ class SimpleClass: #[test] fn test_is_empty() { Python::with_gil(|py| { - let empty_list = PyList::empty_bound(py).into_any(); + let empty_list = PyList::empty(py).into_any(); assert!(empty_list.is_empty().unwrap()); - let list = PyList::new_bound(py, vec![1, 2, 3]).into_any(); + let list = PyList::new(py, vec![1, 2, 3]).into_any(); assert!(!list.is_empty().unwrap()); let not_container = 5.to_object(py).into_bound(py); diff --git a/src/types/dict.rs b/src/types/dict.rs index 1c86b291e2d..dd6652a6969 100644 --- a/src/types/dict.rs +++ b/src/types/dict.rs @@ -605,7 +605,7 @@ mod tests { #[cfg(not(any(PyPy, GraalPy)))] fn test_from_sequence() { Python::with_gil(|py| { - let items = PyList::new_bound(py, vec![("a", 1), ("b", 2)]); + let items = PyList::new(py, vec![("a", 1), ("b", 2)]); let dict = PyDict::from_sequence_bound(&items).unwrap(); assert_eq!( 1, @@ -636,7 +636,7 @@ mod tests { #[cfg(not(any(PyPy, GraalPy)))] fn test_from_sequence_err() { Python::with_gil(|py| { - let items = PyList::new_bound(py, vec!["a", "b"]); + let items = PyList::new(py, vec!["a", "b"]); assert!(PyDict::from_sequence_bound(&items).is_err()); }); } diff --git a/src/types/iterator.rs b/src/types/iterator.rs index dccea94785f..57096ae97da 100644 --- a/src/types/iterator.rs +++ b/src/types/iterator.rs @@ -149,7 +149,7 @@ mod tests { let count; let obj = py.eval_bound("object()", None, None).unwrap(); let list = { - let list = PyList::empty_bound(py); + let list = PyList::empty(py); list.append(10).unwrap(); list.append(&obj).unwrap(); count = obj.get_refcnt(); diff --git a/src/types/list.rs b/src/types/list.rs index ae08798f946..f12a621e95c 100644 --- a/src/types/list.rs +++ b/src/types/list.rs @@ -75,7 +75,7 @@ impl PyList { /// # fn main() { /// Python::with_gil(|py| { /// let elements: Vec = vec![0, 1, 2, 3, 4, 5]; - /// let list = PyList::new_bound(py, elements); + /// let list = PyList::new(py, elements); /// assert_eq!(format!("{:?}", list), "[0, 1, 2, 3, 4, 5]"); /// }); /// # } @@ -87,7 +87,7 @@ impl PyList { /// All standard library structures implement this trait correctly, if they do, so calling this /// function with (for example) [`Vec`]`` or `&[T]` will always succeed. #[track_caller] - pub fn new_bound( + pub fn new( py: Python<'_>, elements: impl IntoIterator, ) -> Bound<'_, PyList> @@ -99,14 +99,36 @@ impl PyList { new_from_iter(py, &mut iter) } + /// Deprecated name for [`PyList::new`]. + #[deprecated(since = "0.23.0", note = "renamed to `PyList::new`")] + #[inline] + #[track_caller] + pub fn new_bound( + py: Python<'_>, + elements: impl IntoIterator, + ) -> Bound<'_, PyList> + where + T: ToPyObject, + U: ExactSizeIterator, + { + Self::new(py, elements) + } + /// Constructs a new empty list. - pub fn empty_bound(py: Python<'_>) -> Bound<'_, PyList> { + pub fn empty(py: Python<'_>) -> Bound<'_, PyList> { unsafe { ffi::PyList_New(0) .assume_owned(py) .downcast_into_unchecked() } } + + /// Deprecated name for [`PyList::empty`]. + #[deprecated(since = "0.23.0", note = "renamed to `PyList::empty`")] + #[inline] + pub fn empty_bound(py: Python<'_>) -> Bound<'_, PyList> { + Self::empty(py) + } } /// Implementation of functionality for [`PyList`]. @@ -133,7 +155,7 @@ pub trait PyListMethods<'py>: crate::sealed::Sealed { /// ``` /// use pyo3::{prelude::*, types::PyList}; /// Python::with_gil(|py| { - /// let list = PyList::new_bound(py, [2, 3, 5, 7]); + /// let list = PyList::new(py, [2, 3, 5, 7]); /// let obj = list.get_item(0); /// assert_eq!(obj.unwrap().extract::().unwrap(), 2); /// }); @@ -251,7 +273,7 @@ impl<'py> PyListMethods<'py> for Bound<'py, PyList> { /// ``` /// use pyo3::{prelude::*, types::PyList}; /// Python::with_gil(|py| { - /// let list = PyList::new_bound(py, [2, 3, 5, 7]); + /// let list = PyList::new(py, [2, 3, 5, 7]); /// let obj = list.get_item(0); /// assert_eq!(obj.unwrap().extract::().unwrap(), 2); /// }); @@ -521,7 +543,7 @@ mod tests { #[test] fn test_new() { Python::with_gil(|py| { - let list = PyList::new_bound(py, [2, 3, 5, 7]); + let list = PyList::new(py, [2, 3, 5, 7]); assert_eq!(2, list.get_item(0).unwrap().extract::().unwrap()); assert_eq!(3, list.get_item(1).unwrap().extract::().unwrap()); assert_eq!(5, list.get_item(2).unwrap().extract::().unwrap()); @@ -532,7 +554,7 @@ mod tests { #[test] fn test_len() { Python::with_gil(|py| { - let list = PyList::new_bound(py, [1, 2, 3, 4]); + let list = PyList::new(py, [1, 2, 3, 4]); assert_eq!(4, list.len()); }); } @@ -540,7 +562,7 @@ mod tests { #[test] fn test_get_item() { Python::with_gil(|py| { - let list = PyList::new_bound(py, [2, 3, 5, 7]); + let list = PyList::new(py, [2, 3, 5, 7]); assert_eq!(2, list.get_item(0).unwrap().extract::().unwrap()); assert_eq!(3, list.get_item(1).unwrap().extract::().unwrap()); assert_eq!(5, list.get_item(2).unwrap().extract::().unwrap()); @@ -551,7 +573,7 @@ mod tests { #[test] fn test_get_slice() { Python::with_gil(|py| { - let list = PyList::new_bound(py, [2, 3, 5, 7]); + let list = PyList::new(py, [2, 3, 5, 7]); let slice = list.get_slice(1, 3); assert_eq!(2, slice.len()); let slice = list.get_slice(1, 7); @@ -562,7 +584,7 @@ mod tests { #[test] fn test_set_item() { Python::with_gil(|py| { - let list = PyList::new_bound(py, [2, 3, 5, 7]); + let list = PyList::new(py, [2, 3, 5, 7]); let val = 42i32.to_object(py); let val2 = 42i32.to_object(py); assert_eq!(2, list.get_item(0).unwrap().extract::().unwrap()); @@ -592,7 +614,7 @@ mod tests { #[test] fn test_insert() { Python::with_gil(|py| { - let list = PyList::new_bound(py, [2, 3, 5, 7]); + let list = PyList::new(py, [2, 3, 5, 7]); let val = 42i32.to_object(py); let val2 = 43i32.to_object(py); assert_eq!(4, list.len()); @@ -612,7 +634,7 @@ mod tests { let cnt; let obj = py.eval_bound("object()", None, None).unwrap(); { - let list = PyList::empty_bound(py); + let list = PyList::empty(py); cnt = obj.get_refcnt(); list.insert(0, &obj).unwrap(); } @@ -624,7 +646,7 @@ mod tests { #[test] fn test_append() { Python::with_gil(|py| { - let list = PyList::new_bound(py, [2]); + let list = PyList::new(py, [2]); list.append(3).unwrap(); assert_eq!(2, list.get_item(0).unwrap().extract::().unwrap()); assert_eq!(3, list.get_item(1).unwrap().extract::().unwrap()); @@ -637,7 +659,7 @@ mod tests { let cnt; let obj = py.eval_bound("object()", None, None).unwrap(); { - let list = PyList::empty_bound(py); + let list = PyList::empty(py); cnt = obj.get_refcnt(); list.append(&obj).unwrap(); } @@ -649,7 +671,7 @@ mod tests { fn test_iter() { Python::with_gil(|py| { let v = vec![2, 3, 5, 7]; - let list = PyList::new_bound(py, &v); + let list = PyList::new(py, &v); let mut idx = 0; for el in list { assert_eq!(v[idx], el.extract::().unwrap()); @@ -709,7 +731,7 @@ mod tests { #[test] fn test_into_iter() { Python::with_gil(|py| { - let list = PyList::new_bound(py, [1, 2, 3, 4]); + let list = PyList::new(py, [1, 2, 3, 4]); for (i, item) in list.iter().enumerate() { assert_eq!((i + 1) as i32, item.extract::().unwrap()); } @@ -721,7 +743,7 @@ mod tests { use crate::types::any::PyAnyMethods; Python::with_gil(|py| { - let list = PyList::new_bound(py, [1, 2, 3, 4]); + let list = PyList::new(py, [1, 2, 3, 4]); let mut items = vec![]; for item in &list { items.push(item.extract::().unwrap()); @@ -733,7 +755,7 @@ mod tests { #[test] fn test_as_sequence() { Python::with_gil(|py| { - let list = PyList::new_bound(py, [1, 2, 3, 4]); + let list = PyList::new(py, [1, 2, 3, 4]); assert_eq!(list.as_sequence().len().unwrap(), 4); assert_eq!( @@ -750,7 +772,7 @@ mod tests { #[test] fn test_into_sequence() { Python::with_gil(|py| { - let list = PyList::new_bound(py, [1, 2, 3, 4]); + let list = PyList::new(py, [1, 2, 3, 4]); let sequence = list.into_sequence(); @@ -763,7 +785,7 @@ mod tests { fn test_extract() { Python::with_gil(|py| { let v = vec![2, 3, 5, 7]; - let list = PyList::new_bound(py, &v); + let list = PyList::new(py, &v); let v2 = list.as_ref().extract::>().unwrap(); assert_eq!(v, v2); }); @@ -773,7 +795,7 @@ mod tests { fn test_sort() { Python::with_gil(|py| { let v = vec![7, 3, 2, 5]; - let list = PyList::new_bound(py, &v); + let list = PyList::new(py, &v); assert_eq!(7, list.get_item(0).unwrap().extract::().unwrap()); assert_eq!(3, list.get_item(1).unwrap().extract::().unwrap()); assert_eq!(2, list.get_item(2).unwrap().extract::().unwrap()); @@ -790,7 +812,7 @@ mod tests { fn test_reverse() { Python::with_gil(|py| { let v = vec![2, 3, 5, 7]; - let list = PyList::new_bound(py, &v); + let list = PyList::new(py, &v); assert_eq!(2, list.get_item(0).unwrap().extract::().unwrap()); assert_eq!(3, list.get_item(1).unwrap().extract::().unwrap()); assert_eq!(5, list.get_item(2).unwrap().extract::().unwrap()); @@ -816,7 +838,7 @@ mod tests { #[test] fn test_list_get_item_invalid_index() { Python::with_gil(|py| { - let list = PyList::new_bound(py, [2, 3, 5, 7]); + let list = PyList::new(py, [2, 3, 5, 7]); let obj = list.get_item(5); assert!(obj.is_err()); assert_eq!( @@ -829,7 +851,7 @@ mod tests { #[test] fn test_list_get_item_sanity() { Python::with_gil(|py| { - let list = PyList::new_bound(py, [2, 3, 5, 7]); + let list = PyList::new(py, [2, 3, 5, 7]); let obj = list.get_item(0); assert_eq!(obj.unwrap().extract::().unwrap(), 2); }); @@ -839,7 +861,7 @@ mod tests { #[test] fn test_list_get_item_unchecked_sanity() { Python::with_gil(|py| { - let list = PyList::new_bound(py, [2, 3, 5, 7]); + let list = PyList::new(py, [2, 3, 5, 7]); let obj = unsafe { list.get_item_unchecked(0) }; assert_eq!(obj.extract::().unwrap(), 2); }); @@ -848,7 +870,7 @@ mod tests { #[test] fn test_list_del_item() { Python::with_gil(|py| { - let list = PyList::new_bound(py, [1, 1, 2, 3, 5, 8]); + let list = PyList::new(py, [1, 1, 2, 3, 5, 8]); assert!(list.del_item(10).is_err()); assert_eq!(1, list.get_item(0).unwrap().extract::().unwrap()); assert!(list.del_item(0).is_ok()); @@ -870,11 +892,11 @@ mod tests { #[test] fn test_list_set_slice() { Python::with_gil(|py| { - let list = PyList::new_bound(py, [1, 1, 2, 3, 5, 8]); - let ins = PyList::new_bound(py, [7, 4]); + let list = PyList::new(py, [1, 1, 2, 3, 5, 8]); + let ins = PyList::new(py, [7, 4]); list.set_slice(1, 4, &ins).unwrap(); assert_eq!([1, 7, 4, 5, 8], list.extract::<[i32; 5]>().unwrap()); - list.set_slice(3, 100, &PyList::empty_bound(py)).unwrap(); + list.set_slice(3, 100, &PyList::empty(py)).unwrap(); assert_eq!([1, 7, 4], list.extract::<[i32; 3]>().unwrap()); }); } @@ -882,7 +904,7 @@ mod tests { #[test] fn test_list_del_slice() { Python::with_gil(|py| { - let list = PyList::new_bound(py, [1, 1, 2, 3, 5, 8]); + let list = PyList::new(py, [1, 1, 2, 3, 5, 8]); list.del_slice(1, 4).unwrap(); assert_eq!([1, 5, 8], list.extract::<[i32; 3]>().unwrap()); list.del_slice(1, 100).unwrap(); @@ -893,7 +915,7 @@ mod tests { #[test] fn test_list_contains() { Python::with_gil(|py| { - let list = PyList::new_bound(py, [1, 1, 2, 3, 5, 8]); + let list = PyList::new(py, [1, 1, 2, 3, 5, 8]); assert_eq!(6, list.len()); let bad_needle = 7i32.to_object(py); @@ -910,7 +932,7 @@ mod tests { #[test] fn test_list_index() { Python::with_gil(|py| { - let list = PyList::new_bound(py, [1, 1, 2, 3, 5, 8]); + let list = PyList::new(py, [1, 1, 2, 3, 5, 8]); assert_eq!(0, list.index(1i32).unwrap()); assert_eq!(2, list.index(2i32).unwrap()); assert_eq!(3, list.index(3i32).unwrap()); @@ -947,7 +969,7 @@ mod tests { fn too_long_iterator() { Python::with_gil(|py| { let iter = FaultyIter(0..usize::MAX, 73); - let _list = PyList::new_bound(py, iter); + let _list = PyList::new(py, iter); }) } @@ -958,7 +980,7 @@ mod tests { fn too_short_iterator() { Python::with_gil(|py| { let iter = FaultyIter(0..35, 73); - let _list = PyList::new_bound(py, iter); + let _list = PyList::new(py, iter); }) } @@ -970,7 +992,7 @@ mod tests { Python::with_gil(|py| { let iter = FaultyIter(0..0, usize::MAX); - let _list = PyList::new_bound(py, iter); + let _list = PyList::new(py, iter); }) } @@ -1029,7 +1051,7 @@ mod tests { Python::with_gil(|py| { std::panic::catch_unwind(|| { let iter = FaultyIter(0..50, 50); - let _list = PyList::new_bound(py, iter); + let _list = PyList::new(py, iter); }) .unwrap_err(); }); @@ -1044,7 +1066,7 @@ mod tests { #[test] fn test_list_to_tuple() { Python::with_gil(|py| { - let list = PyList::new_bound(py, vec![1, 2, 3]); + let list = PyList::new(py, vec![1, 2, 3]); let tuple = list.to_tuple(); let tuple_expected = PyTuple::new(py, vec![1, 2, 3]); assert!(tuple.eq(tuple_expected).unwrap()); diff --git a/src/types/module.rs b/src/types/module.rs index 6b90e5de197..873d3347fc0 100644 --- a/src/types/module.rs +++ b/src/types/module.rs @@ -368,7 +368,7 @@ impl<'py> PyModuleMethods<'py> for Bound<'py, PyModule> { Ok(idx) => idx.downcast_into().map_err(PyErr::from), Err(err) => { if err.is_instance_of::(self.py()) { - let l = PyList::empty_bound(self.py()); + let l = PyList::empty(self.py()); self.setattr(__all__, &l).map_err(PyErr::from)?; Ok(l) } else { diff --git a/src/types/sequence.rs b/src/types/sequence.rs index 1b71a186b57..530b1969d5d 100644 --- a/src/types/sequence.rs +++ b/src/types/sequence.rs @@ -558,7 +558,7 @@ mod tests { let ins = w.to_object(py); seq.set_slice(1, 4, ins.bind(py)).unwrap(); assert_eq!([1, 7, 4, 5, 8], seq.extract::<[i32; 5]>().unwrap()); - seq.set_slice(3, 100, &PyList::empty_bound(py)).unwrap(); + seq.set_slice(3, 100, &PyList::empty(py)).unwrap(); assert_eq!([1, 7, 4], seq.extract::<[i32; 3]>().unwrap()); }); } @@ -704,11 +704,7 @@ mod tests { let v = vec!["foo", "bar"]; let ob = v.to_object(py); let seq = ob.downcast_bound::(py).unwrap(); - assert!(seq - .to_list() - .unwrap() - .eq(PyList::new_bound(py, &v)) - .unwrap()); + assert!(seq.to_list().unwrap().eq(PyList::new(py, &v)).unwrap()); }); } @@ -721,7 +717,7 @@ mod tests { assert!(seq .to_list() .unwrap() - .eq(PyList::new_bound(py, ["f", "o", "o"])) + .eq(PyList::new(py, ["f", "o", "o"])) .unwrap()); }); } diff --git a/src/types/tuple.rs b/src/types/tuple.rs index 3d67a062cde..711922ce3fa 100644 --- a/src/types/tuple.rs +++ b/src/types/tuple.rs @@ -1178,7 +1178,7 @@ mod tests { Python::with_gil(|py| { let tuple = PyTuple::new(py, vec![1, 2, 3]); let list = tuple.to_list(); - let list_expected = PyList::new_bound(py, vec![1, 2, 3]); + let list_expected = PyList::new(py, vec![1, 2, 3]); assert!(list.eq(list_expected).unwrap()); }) } diff --git a/tests/test_frompyobject.rs b/tests/test_frompyobject.rs index 9afe22141d2..9d8d81491cc 100644 --- a/tests/test_frompyobject.rs +++ b/tests/test_frompyobject.rs @@ -594,7 +594,7 @@ pub struct TransparentFromPyWith { #[test] fn test_transparent_from_py_with() { Python::with_gil(|py| { - let result = PyList::new_bound(py, [1, 2, 3]) + let result = PyList::new(py, [1, 2, 3]) .extract::() .unwrap(); let expected = TransparentFromPyWith { len: 3 }; diff --git a/tests/test_getter_setter.rs b/tests/test_getter_setter.rs index e3852fcd29a..dcccffd3aa6 100644 --- a/tests/test_getter_setter.rs +++ b/tests/test_getter_setter.rs @@ -54,7 +54,7 @@ impl ClassWithProperties { #[getter] fn get_data_list<'py>(&self, py: Python<'py>) -> Bound<'py, PyList> { - PyList::new_bound(py, [self.num]) + PyList::new(py, [self.num]) } } diff --git a/tests/test_methods.rs b/tests/test_methods.rs index 6888a9b0b14..16c82d0eca0 100644 --- a/tests/test_methods.rs +++ b/tests/test_methods.rs @@ -703,7 +703,7 @@ impl MethodWithLifeTime { for _ in 0..set.len() { items.push(set.pop().unwrap()); } - let list = PyList::new_bound(py, items); + let list = PyList::new(py, items); list.sort()?; Ok(list) } diff --git a/tests/test_proto_methods.rs b/tests/test_proto_methods.rs index 06e0d45e4a6..71d8fa6eaad 100644 --- a/tests/test_proto_methods.rs +++ b/tests/test_proto_methods.rs @@ -846,7 +846,7 @@ struct DefaultedContains; #[pymethods] impl DefaultedContains { fn __iter__(&self, py: Python<'_>) -> PyObject { - PyList::new_bound(py, ["a", "b", "c"]) + PyList::new(py, ["a", "b", "c"]) .as_ref() .iter() .unwrap() @@ -860,7 +860,7 @@ struct NoContains; #[pymethods] impl NoContains { fn __iter__(&self, py: Python<'_>) -> PyObject { - PyList::new_bound(py, ["a", "b", "c"]) + PyList::new(py, ["a", "b", "c"]) .as_ref() .iter() .unwrap() From e0bd22eacb301f95089f893878d59b2c08653c17 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 30 Jul 2024 05:50:09 +0100 Subject: [PATCH 181/495] Bump CodSpeedHQ/action from 2 to 3 (#4391) Bumps [CodSpeedHQ/action](https://github.com/codspeedhq/action) from 2 to 3. - [Release notes](https://github.com/codspeedhq/action/releases) - [Changelog](https://github.com/CodSpeedHQ/action/blob/main/CHANGELOG.md) - [Commits](https://github.com/codspeedhq/action/compare/v2...v3) --- updated-dependencies: - dependency-name: CodSpeedHQ/action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/benches.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/benches.yml b/.github/workflows/benches.yml index 572f77efd76..29bdabf5f7c 100644 --- a/.github/workflows/benches.yml +++ b/.github/workflows/benches.yml @@ -37,7 +37,7 @@ jobs: run: pip install nox - name: Run the benchmarks - uses: CodSpeedHQ/action@v2 + uses: CodSpeedHQ/action@v3 with: run: nox -s codspeed token: ${{ secrets.CODSPEED_TOKEN }} From a5dd124e93aeae2fbba688a3a8edc5dba176c698 Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Tue, 30 Jul 2024 23:24:22 +0200 Subject: [PATCH 182/495] fix ui tests (#4397) --- tests/test_compile_error.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_compile_error.rs b/tests/test_compile_error.rs index c2d8d3287c1..b1fcdc09fb7 100644 --- a/tests/test_compile_error.rs +++ b/tests/test_compile_error.rs @@ -29,12 +29,14 @@ fn test_compile_errors() { t.compile_fail("tests/ui/static_ref.rs"); t.compile_fail("tests/ui/wrong_aspyref_lifetimes.rs"); t.compile_fail("tests/ui/invalid_pyfunctions.rs"); + #[cfg(not(any(feature = "hashbrown", feature = "indexmap")))] t.compile_fail("tests/ui/invalid_pymethods.rs"); // output changes with async feature #[cfg(all(Py_LIMITED_API, feature = "experimental-async"))] t.compile_fail("tests/ui/abi3_nativetype_inheritance.rs"); t.compile_fail("tests/ui/invalid_intern_arg.rs"); t.compile_fail("tests/ui/invalid_frozen_pyclass_borrow.rs"); + #[cfg(not(any(feature = "hashbrown", feature = "indexmap")))] t.compile_fail("tests/ui/invalid_pymethod_receiver.rs"); t.compile_fail("tests/ui/missing_intopy.rs"); // adding extra error conversion impls changes the output From 6caefd151ee53b177229a6547722789b3925e367 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers <7943856+bschoenmaeckers@users.noreply.github.com> Date: Wed, 31 Jul 2024 09:42:07 +0200 Subject: [PATCH 183/495] Add back `PyBytes::new` (#4387) --- guide/src/conversions/traits.md | 2 +- pytests/src/buf_and_str.rs | 2 +- src/conversions/anyhow.rs | 2 +- src/conversions/eyre.rs | 2 +- src/conversions/num_bigint.rs | 2 +- src/conversions/std/slice.rs | 4 +-- src/pybacked.rs | 22 ++++++------- src/tests/hygiene/pymethods.rs | 2 +- src/types/bytes.rs | 55 ++++++++++++++++++++++++++------- tests/test_bytes.rs | 4 +-- 10 files changed, 64 insertions(+), 33 deletions(-) diff --git a/guide/src/conversions/traits.md b/guide/src/conversions/traits.md index c622bae58d2..344834f4a7c 100644 --- a/guide/src/conversions/traits.md +++ b/guide/src/conversions/traits.md @@ -388,7 +388,7 @@ enum RustyEnum<'py> { # } # # { -# let thing = PyBytes::new_bound(py, b"text"); +# let thing = PyBytes::new(py, b"text"); # let rust_thing: RustyEnum<'_> = thing.extract()?; # # assert_eq!( diff --git a/pytests/src/buf_and_str.rs b/pytests/src/buf_and_str.rs index 879d76af883..4a7add32bc2 100644 --- a/pytests/src/buf_and_str.rs +++ b/pytests/src/buf_and_str.rs @@ -43,7 +43,7 @@ impl BytesExtractor { #[pyfunction] fn return_memoryview(py: Python<'_>) -> PyResult> { - let bytes = PyBytes::new_bound(py, b"hello world"); + let bytes = PyBytes::new(py, b"hello world"); PyMemoryView::from_bound(&bytes) } diff --git a/src/conversions/anyhow.rs b/src/conversions/anyhow.rs index d6880ac4e96..0b2bf2bacfa 100644 --- a/src/conversions/anyhow.rs +++ b/src/conversions/anyhow.rs @@ -76,7 +76,7 @@ //! let res = Python::with_gil(|py| { //! let zlib = PyModule::import_bound(py, "zlib")?; //! let decompress = zlib.getattr("decompress")?; -//! let bytes = PyBytes::new_bound(py, bytes); +//! let bytes = PyBytes::new(py, bytes); //! let value = decompress.call1((bytes,))?; //! value.extract::>() //! })?; diff --git a/src/conversions/eyre.rs b/src/conversions/eyre.rs index 2d8b623fa58..9a876c3f1ef 100644 --- a/src/conversions/eyre.rs +++ b/src/conversions/eyre.rs @@ -75,7 +75,7 @@ //! let res = Python::with_gil(|py| { //! let zlib = PyModule::import_bound(py, "zlib")?; //! let decompress = zlib.getattr("decompress")?; -//! let bytes = PyBytes::new_bound(py, bytes); +//! let bytes = PyBytes::new(py, bytes); //! let value = decompress.call1((bytes,))?; //! value.extract::>() //! })?; diff --git a/src/conversions/num_bigint.rs b/src/conversions/num_bigint.rs index 99b5962622d..708c51bca62 100644 --- a/src/conversions/num_bigint.rs +++ b/src/conversions/num_bigint.rs @@ -112,7 +112,7 @@ macro_rules! bigint_conversion { #[cfg(Py_LIMITED_API)] fn to_object(&self, py: Python<'_>) -> PyObject { let bytes = $to_bytes(self); - let bytes_obj = PyBytes::new_bound(py, &bytes); + let bytes_obj = PyBytes::new(py, &bytes); let kwargs = if $is_signed { let kwargs = crate::types::PyDict::new_bound(py); kwargs.set_item(crate::intern!(py, "signed"), true).unwrap(); diff --git a/src/conversions/std/slice.rs b/src/conversions/std/slice.rs index 34b3e61eaf1..e2353a5d320 100644 --- a/src/conversions/std/slice.rs +++ b/src/conversions/std/slice.rs @@ -9,7 +9,7 @@ use crate::{ impl<'a> IntoPy for &'a [u8] { fn into_py(self, py: Python<'_>) -> PyObject { - PyBytes::new_bound(py, self).unbind().into() + PyBytes::new(py, self).unbind().into() } #[cfg(feature = "experimental-inspect")] @@ -52,7 +52,7 @@ impl<'a> crate::conversion::FromPyObjectBound<'a, '_> for Cow<'a, [u8]> { impl ToPyObject for Cow<'_, [u8]> { fn to_object(&self, py: Python<'_>) -> Py { - PyBytes::new_bound(py, self.as_ref()).into() + PyBytes::new(py, self.as_ref()).into() } } diff --git a/src/pybacked.rs b/src/pybacked.rs index f6a0f99fe9a..1d93042f039 100644 --- a/src/pybacked.rs +++ b/src/pybacked.rs @@ -207,7 +207,7 @@ impl ToPyObject for PyBackedBytes { fn to_object(&self, py: Python<'_>) -> Py { match &self.storage { PyBackedBytesStorage::Python(bytes) => bytes.to_object(py), - PyBackedBytesStorage::Rust(bytes) => PyBytes::new_bound(py, bytes).into_any().unbind(), + PyBackedBytesStorage::Rust(bytes) => PyBytes::new(py, bytes).into_any().unbind(), } } } @@ -216,7 +216,7 @@ impl IntoPy> for PyBackedBytes { fn into_py(self, py: Python<'_>) -> Py { match self.storage { PyBackedBytesStorage::Python(bytes) => bytes.into_any(), - PyBackedBytesStorage::Rust(bytes) => PyBytes::new_bound(py, &bytes).into_any().unbind(), + PyBackedBytesStorage::Rust(bytes) => PyBytes::new(py, &bytes).into_any().unbind(), } } } @@ -355,7 +355,7 @@ mod test { #[test] fn py_backed_bytes_empty() { Python::with_gil(|py| { - let b = PyBytes::new_bound(py, b""); + let b = PyBytes::new(py, b""); let py_backed_bytes = b.extract::().unwrap(); assert_eq!(&*py_backed_bytes, b""); }); @@ -364,7 +364,7 @@ mod test { #[test] fn py_backed_bytes() { Python::with_gil(|py| { - let b = PyBytes::new_bound(py, b"abcde"); + let b = PyBytes::new(py, b"abcde"); let py_backed_bytes = b.extract::().unwrap(); assert_eq!(&*py_backed_bytes, b"abcde"); }); @@ -373,7 +373,7 @@ mod test { #[test] fn py_backed_bytes_from_bytes() { Python::with_gil(|py| { - let b = PyBytes::new_bound(py, b"abcde"); + let b = PyBytes::new(py, b"abcde"); let py_backed_bytes = PyBackedBytes::from(b); assert_eq!(&*py_backed_bytes, b"abcde"); }); @@ -391,7 +391,7 @@ mod test { #[test] fn py_backed_bytes_into_py() { Python::with_gil(|py| { - let orig_bytes = PyBytes::new_bound(py, b"abcde"); + let orig_bytes = PyBytes::new(py, b"abcde"); let py_backed_bytes = PyBackedBytes::from(orig_bytes.clone()); assert!(py_backed_bytes.to_object(py).is(&orig_bytes)); assert!(py_backed_bytes.into_py(py).is(&orig_bytes)); @@ -495,7 +495,7 @@ mod test { #[test] fn test_backed_bytes_from_bytes_clone() { Python::with_gil(|py| { - let b1: PyBackedBytes = PyBytes::new_bound(py, b"abcde").into(); + let b1: PyBackedBytes = PyBytes::new(py, b"abcde").into(); let b2 = b1.clone(); assert_eq!(b1, b2); @@ -520,13 +520,13 @@ mod test { #[test] fn test_backed_bytes_eq() { Python::with_gil(|py| { - let b1: PyBackedBytes = PyBytes::new_bound(py, b"abcde").into(); + let b1: PyBackedBytes = PyBytes::new(py, b"abcde").into(); let b2: PyBackedBytes = PyByteArray::new_bound(py, b"abcde").into(); assert_eq!(b1, b"abcde"); assert_eq!(b1, b2); - let b3: PyBackedBytes = PyBytes::new_bound(py, b"hello").into(); + let b3: PyBackedBytes = PyBytes::new(py, b"hello").into(); assert_eq!(b"hello", b3); assert_ne!(b1, b3); }); @@ -541,7 +541,7 @@ mod test { hasher.finish() }; - let b1: PyBackedBytes = PyBytes::new_bound(py, b"abcde").into(); + let b1: PyBackedBytes = PyBytes::new(py, b"abcde").into(); let h1 = { let mut hasher = DefaultHasher::new(); b1.hash(&mut hasher); @@ -566,7 +566,7 @@ mod test { let mut a = vec![b"a", b"c", b"d", b"b", b"f", b"g", b"e"]; let mut b = a .iter() - .map(|&b| PyBytes::new_bound(py, b).into()) + .map(|&b| PyBytes::new(py, b).into()) .collect::>(); a.sort(); diff --git a/src/tests/hygiene/pymethods.rs b/src/tests/hygiene/pymethods.rs index 5abfc856ea2..af167db21c6 100644 --- a/src/tests/hygiene/pymethods.rs +++ b/src/tests/hygiene/pymethods.rs @@ -21,7 +21,7 @@ impl Dummy { } fn __bytes__<'py>(&self, py: crate::Python<'py>) -> crate::Bound<'py, crate::types::PyBytes> { - crate::types::PyBytes::new_bound(py, &[0]) + crate::types::PyBytes::new(py, &[0]) } fn __format__(&self, format_spec: ::std::string::String) -> ::std::string::String { diff --git a/src/types/bytes.rs b/src/types/bytes.rs index 0a8b4860d25..876cf156ab8 100644 --- a/src/types/bytes.rs +++ b/src/types/bytes.rs @@ -30,12 +30,12 @@ use std::str; /// use pyo3::types::PyBytes; /// /// # Python::with_gil(|py| { -/// let py_bytes = PyBytes::new_bound(py, b"foo".as_slice()); +/// let py_bytes = PyBytes::new(py, b"foo".as_slice()); /// // via PartialEq<[u8]> /// assert_eq!(py_bytes, b"foo".as_slice()); /// /// // via Python equality -/// let other = PyBytes::new_bound(py, b"foo".as_slice()); +/// let other = PyBytes::new(py, b"foo".as_slice()); /// assert!(py_bytes.as_any().eq(other).unwrap()); /// /// // Note that `eq` will convert it's argument to Python using `ToPyObject`, @@ -54,7 +54,7 @@ impl PyBytes { /// The bytestring is initialized by copying the data from the `&[u8]`. /// /// Panics if out of memory. - pub fn new_bound<'p>(py: Python<'p>, s: &[u8]) -> Bound<'p, PyBytes> { + pub fn new<'p>(py: Python<'p>, s: &[u8]) -> Bound<'p, PyBytes> { let ptr = s.as_ptr().cast(); let len = s.len() as ffi::Py_ssize_t; unsafe { @@ -64,6 +64,13 @@ impl PyBytes { } } + /// Deprecated name for [`PyBytes::new`]. + #[deprecated(since = "0.23.0", note = "renamed to `PyBytes::new`")] + #[inline] + pub fn new_bound<'p>(py: Python<'p>, s: &[u8]) -> Bound<'p, PyBytes> { + Self::new(py, s) + } + /// Creates a new Python `bytes` object with an `init` closure to write its contents. /// Before calling `init` the bytes' contents are zero-initialised. /// * If Python raises a MemoryError on the allocation, `new_with` will return @@ -78,7 +85,7 @@ impl PyBytes { /// /// # fn main() -> PyResult<()> { /// Python::with_gil(|py| -> PyResult<()> { - /// let py_bytes = PyBytes::new_bound_with(py, 10, |bytes: &mut [u8]| { + /// let py_bytes = PyBytes::new_with(py, 10, |bytes: &mut [u8]| { /// bytes.copy_from_slice(b"Hello Rust"); /// Ok(()) /// })?; @@ -88,7 +95,7 @@ impl PyBytes { /// }) /// # } /// ``` - pub fn new_bound_with(py: Python<'_>, len: usize, init: F) -> PyResult> + pub fn new_with(py: Python<'_>, len: usize, init: F) -> PyResult> where F: FnOnce(&mut [u8]) -> PyResult<()>, { @@ -106,6 +113,16 @@ impl PyBytes { } } + /// Deprecated name for [`PyBytes::new_with`]. + #[deprecated(since = "0.23.0", note = "renamed to `PyBytes::new_with`")] + #[inline] + pub fn new_bound_with(py: Python<'_>, len: usize, init: F) -> PyResult> + where + F: FnOnce(&mut [u8]) -> PyResult<()>, + { + Self::new_with(py, len, init) + } + /// Creates a new Python byte string object from a raw pointer and length. /// /// Panics if out of memory. @@ -116,11 +133,25 @@ impl PyBytes { /// leading pointer of a slice of length `len`. [As with /// `std::slice::from_raw_parts`, this is /// unsafe](https://doc.rust-lang.org/std/slice/fn.from_raw_parts.html#safety). - pub unsafe fn bound_from_ptr(py: Python<'_>, ptr: *const u8, len: usize) -> Bound<'_, PyBytes> { + pub unsafe fn from_ptr(py: Python<'_>, ptr: *const u8, len: usize) -> Bound<'_, PyBytes> { ffi::PyBytes_FromStringAndSize(ptr.cast(), len as isize) .assume_owned(py) .downcast_into_unchecked() } + + /// Deprecated name for [`PyBytes::from_ptr`]. + /// + /// # Safety + /// + /// This function dereferences the raw pointer `ptr` as the + /// leading pointer of a slice of length `len`. [As with + /// `std::slice::from_raw_parts`, this is + /// unsafe](https://doc.rust-lang.org/std/slice/fn.from_raw_parts.html#safety). + #[deprecated(since = "0.23.0", note = "renamed to `PyBytes::from_ptr`")] + #[inline] + pub unsafe fn bound_from_ptr(py: Python<'_>, ptr: *const u8, len: usize) -> Bound<'_, PyBytes> { + Self::from_ptr(py, ptr, len) + } } /// Implementation of functionality for [`PyBytes`]. @@ -279,7 +310,7 @@ mod tests { #[test] fn test_bytes_index() { Python::with_gil(|py| { - let bytes = PyBytes::new_bound(py, b"Hello World"); + let bytes = PyBytes::new(py, b"Hello World"); assert_eq!(bytes[1], b'e'); }); } @@ -287,7 +318,7 @@ mod tests { #[test] fn test_bound_bytes_index() { Python::with_gil(|py| { - let bytes = PyBytes::new_bound(py, b"Hello World"); + let bytes = PyBytes::new(py, b"Hello World"); assert_eq!(bytes[1], b'e'); let bytes = &bytes; @@ -298,7 +329,7 @@ mod tests { #[test] fn test_bytes_new_with() -> super::PyResult<()> { Python::with_gil(|py| -> super::PyResult<()> { - let py_bytes = PyBytes::new_bound_with(py, 10, |b: &mut [u8]| { + let py_bytes = PyBytes::new_with(py, 10, |b: &mut [u8]| { b.copy_from_slice(b"Hello Rust"); Ok(()) })?; @@ -311,7 +342,7 @@ mod tests { #[test] fn test_bytes_new_with_zero_initialised() -> super::PyResult<()> { Python::with_gil(|py| -> super::PyResult<()> { - let py_bytes = PyBytes::new_bound_with(py, 10, |_b: &mut [u8]| Ok(()))?; + let py_bytes = PyBytes::new_with(py, 10, |_b: &mut [u8]| Ok(()))?; let bytes: &[u8] = py_bytes.extract()?; assert_eq!(bytes, &[0; 10]); Ok(()) @@ -322,7 +353,7 @@ mod tests { fn test_bytes_new_with_error() { use crate::exceptions::PyValueError; Python::with_gil(|py| { - let py_bytes_result = PyBytes::new_bound_with(py, 10, |_b: &mut [u8]| { + let py_bytes_result = PyBytes::new_with(py, 10, |_b: &mut [u8]| { Err(PyValueError::new_err("Hello Crustaceans!")) }); assert!(py_bytes_result.is_err()); @@ -337,7 +368,7 @@ mod tests { fn test_comparisons() { Python::with_gil(|py| { let b = b"hello, world".as_slice(); - let py_bytes = PyBytes::new_bound(py, b); + let py_bytes = PyBytes::new(py, b); assert_eq!(py_bytes, b"hello, world".as_slice()); diff --git a/tests/test_bytes.rs b/tests/test_bytes.rs index 26686a2def3..4e58cdc08e0 100644 --- a/tests/test_bytes.rs +++ b/tests/test_bytes.rs @@ -21,7 +21,7 @@ fn test_pybytes_bytes_conversion() { #[pyfunction] fn bytes_vec_conversion(py: Python<'_>, bytes: Vec) -> Bound<'_, PyBytes> { - PyBytes::new_bound(py, bytes.as_slice()) + PyBytes::new(py, bytes.as_slice()) } #[test] @@ -43,7 +43,7 @@ fn test_bytearray_vec_conversion() { #[test] fn test_py_as_bytes() { let pyobj: pyo3::Py = - Python::with_gil(|py| pyo3::types::PyBytes::new_bound(py, b"abc").unbind()); + Python::with_gil(|py| pyo3::types::PyBytes::new(py, b"abc").unbind()); let data = Python::with_gil(|py| pyobj.as_bytes(py)); From 7c1ae15fba7beb937599287f03f506a6c17431ab Mon Sep 17 00:00:00 2001 From: konsti Date: Thu, 1 Aug 2024 07:45:04 +0200 Subject: [PATCH 184/495] Fix typo: Add missing closing bracket (#4393) * Fix typo * Update snapshot --- pyo3-macros-backend/src/pyclass.rs | 2 +- tests/ui/deprecations.stderr | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index 6fba5b7e23e..3b61770a870 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -1828,7 +1828,7 @@ fn pyclass_richcmp_simple_enum( quote! { #[deprecated( since = "0.22.0", - note = "Implicit equality for simple enums is deprecated. Use `#[pyclass(eq, eq_int)` to keep the current behavior." + note = "Implicit equality for simple enums is deprecated. Use `#[pyclass(eq, eq_int)]` to keep the current behavior." )] const DEPRECATION: () = (); const _: () = DEPRECATION; diff --git a/tests/ui/deprecations.stderr b/tests/ui/deprecations.stderr index f93f99179cf..567f5697c7f 100644 --- a/tests/ui/deprecations.stderr +++ b/tests/ui/deprecations.stderr @@ -28,7 +28,7 @@ error: use of deprecated constant `__pyfunction_pyfunction_option_4::SIGNATURE`: 21 | fn pyfunction_option_4( | ^^^^^^^^^^^^^^^^^^^ -error: use of deprecated constant `SimpleEnumWithoutEq::__pyo3__generated____richcmp__::DEPRECATION`: Implicit equality for simple enums is deprecated. Use `#[pyclass(eq, eq_int)` to keep the current behavior. +error: use of deprecated constant `SimpleEnumWithoutEq::__pyo3__generated____richcmp__::DEPRECATION`: Implicit equality for simple enums is deprecated. Use `#[pyclass(eq, eq_int)]` to keep the current behavior. --> tests/ui/deprecations.rs:28:1 | 28 | #[pyclass] From 45ba2fcfe71377e619de944e6ae4e4f06cb5c298 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers <7943856+bschoenmaeckers@users.noreply.github.com> Date: Thu, 1 Aug 2024 14:52:55 +0200 Subject: [PATCH 185/495] Reintroduce `PyDict` constructors (#4388) * Add back `PyDict::new` * Add back `IntoPyDict::into_py_dict` * Add changelog & migration entry --- README.md | 2 +- guide/src/conversions/traits.md | 2 +- guide/src/exception.md | 2 +- guide/src/migration.md | 51 ++++++++++++++ guide/src/module.md | 2 +- .../python-from-rust/calling-existing-code.md | 2 +- guide/src/python-from-rust/function-calls.md | 6 +- newsfragments/4388.changed.md | 1 + pyo3-benches/benches/bench_bigint.rs | 2 +- pyo3-benches/benches/bench_decimal.rs | 2 +- pyo3-benches/benches/bench_dict.rs | 14 ++-- pyo3-benches/benches/bench_extract.rs | 12 ++-- src/conversions/anyhow.rs | 4 +- src/conversions/chrono.rs | 4 +- src/conversions/eyre.rs | 4 +- src/conversions/hashbrown.rs | 6 +- src/conversions/indexmap.rs | 8 +-- src/conversions/num_bigint.rs | 6 +- src/conversions/num_rational.rs | 12 ++-- src/conversions/rust_decimal.rs | 10 +-- src/conversions/smallvec.rs | 2 +- src/conversions/std/map.rs | 8 +-- src/conversions/std/num.rs | 8 +-- src/conversions/std/time.rs | 2 +- src/exceptions.rs | 16 ++--- src/ffi/tests.rs | 6 +- src/impl_/extract_argument.rs | 6 +- src/instance.rs | 22 +++---- src/lib.rs | 2 +- src/macros.rs | 4 +- src/marker.rs | 6 +- src/marshal.rs | 4 +- src/sync.rs | 6 +- src/tests/common.rs | 4 +- src/types/any.rs | 10 +-- src/types/bytearray.rs | 2 +- src/types/dict.rs | 66 ++++++++++++------- src/types/ellipsis.rs | 2 +- src/types/iterator.rs | 4 +- src/types/none.rs | 2 +- src/types/notimplemented.rs | 4 +- src/types/traceback.rs | 4 +- tests/test_anyhow.rs | 2 +- tests/test_buffer_protocol.rs | 4 +- tests/test_class_basics.rs | 2 +- tests/test_class_new.rs | 2 +- tests/test_coroutine.rs | 6 +- tests/test_datetime.rs | 4 +- tests/test_dict_iter.rs | 2 +- tests/test_enum.rs | 4 +- tests/test_frompyobject.rs | 10 +-- tests/test_getter_setter.rs | 2 +- tests/test_inheritance.rs | 6 +- tests/test_macro_docs.rs | 2 +- tests/test_mapping.rs | 2 +- tests/test_methods.rs | 12 ++-- tests/test_module.rs | 6 +- tests/test_proto_methods.rs | 2 +- tests/test_sequence.rs | 6 +- tests/test_static_slots.rs | 2 +- tests/ui/wrong_aspyref_lifetimes.rs | 2 +- 61 files changed, 246 insertions(+), 174 deletions(-) create mode 100644 newsfragments/4388.changed.md diff --git a/README.md b/README.md index 2a6348437a2..066de85fa8b 100644 --- a/README.md +++ b/README.md @@ -152,7 +152,7 @@ fn main() -> PyResult<()> { let sys = py.import_bound("sys")?; let version: String = sys.getattr("version")?.extract()?; - let locals = [("os", py.import_bound("os")?)].into_py_dict_bound(py); + let locals = [("os", py.import_bound("os")?)].into_py_dict(py); let code = "os.getenv('USER') or os.getenv('USERNAME') or 'Unknown'"; let user: String = py.eval_bound(code, None, Some(&locals))?.extract()?; diff --git a/guide/src/conversions/traits.md b/guide/src/conversions/traits.md index 344834f4a7c..8c813c8fe1b 100644 --- a/guide/src/conversions/traits.md +++ b/guide/src/conversions/traits.md @@ -86,7 +86,7 @@ struct RustyStruct { # use pyo3::types::PyDict; # fn main() -> PyResult<()> { # Python::with_gil(|py| -> PyResult<()> { -# let dict = PyDict::new_bound(py); +# let dict = PyDict::new(py); # dict.set_item("my_string", "test")?; # # let rustystruct: RustyStruct = dict.extract()?; diff --git a/guide/src/exception.md b/guide/src/exception.md index bf979237f01..a6193d6a50b 100644 --- a/guide/src/exception.md +++ b/guide/src/exception.md @@ -24,7 +24,7 @@ use pyo3::exceptions::PyException; create_exception!(mymodule, CustomError, PyException); Python::with_gil(|py| { - let ctx = [("CustomError", py.get_type_bound::())].into_py_dict_bound(py); + let ctx = [("CustomError", py.get_type_bound::())].into_py_dict(py); pyo3::py_run!( py, *ctx, diff --git a/guide/src/migration.md b/guide/src/migration.md index 671a3333990..c55900651d5 100644 --- a/guide/src/migration.md +++ b/guide/src/migration.md @@ -41,6 +41,57 @@ let tup = PyTuple::new(py, [1, 2, 3]); ``` +### Renamed `IntoPyDict::into_py_dict_bound` into `IntoPyDict::into_py_dict`. +
+Click to expand + +The `IntoPyDict::into_py_dict_bound` method has been renamed to `IntoPyDict::into_py_dict`. If you implemented `IntoPyDict` for your type, you should implement `into_py_dict` instead of `into_py_dict_bound`. The old name is still available but deprecated. + +Before: + +```rust,ignore +# use pyo3::prelude::*; +# use pyo3::types::{PyDict, IntoPyDict}; +# use pyo3::types::dict::PyDictItem; +impl IntoPyDict for I +where + T: PyDictItem, + I: IntoIterator, +{ + fn into_py_dict_bound(self, py: Python<'_>) -> Bound<'_, PyDict> { + let dict = PyDict::new(py); + for item in self { + dict.set_item(item.key(), item.value()) + .expect("Failed to set_item on dict"); + } + dict + } +} +``` + +After: + +```rust,ignore +# use pyo3::prelude::*; +# use pyo3::types::{PyDict, IntoPyDict}; +# use pyo3::types::dict::PyDictItem; +impl IntoPyDict for I +where + T: PyDictItem, + I: IntoIterator, +{ + fn into_py_dict(self, py: Python<'_>) -> Bound<'_, PyDict> { + let dict = PyDict::new(py); + for item in self { + dict.set_item(item.key(), item.value()) + .expect("Failed to set_item on dict"); + } + dict + } +} +``` +
+ ## from 0.21.* to 0.22 ### Deprecation of `gil-refs` feature continues diff --git a/guide/src/module.md b/guide/src/module.md index 78616946357..c6a7b3c9c64 100644 --- a/guide/src/module.md +++ b/guide/src/module.md @@ -89,7 +89,7 @@ fn func() -> String { # use pyo3::wrap_pymodule; # use pyo3::types::IntoPyDict; # let parent_module = wrap_pymodule!(parent_module)(py); -# let ctx = [("parent_module", parent_module)].into_py_dict_bound(py); +# let ctx = [("parent_module", parent_module)].into_py_dict(py); # # py.run_bound("assert parent_module.child_module.func() == 'func'", None, Some(&ctx)).unwrap(); # }) diff --git a/guide/src/python-from-rust/calling-existing-code.md b/guide/src/python-from-rust/calling-existing-code.md index 9c0f592451f..dd4a28458ed 100644 --- a/guide/src/python-from-rust/calling-existing-code.md +++ b/guide/src/python-from-rust/calling-existing-code.md @@ -126,7 +126,7 @@ def leaky_relu(x, slope=0.01): let relu_result: f64 = activators.getattr("relu")?.call1((-1.0,))?.extract()?; assert_eq!(relu_result, 0.0); - let kwargs = [("slope", 0.2)].into_py_dict_bound(py); + let kwargs = [("slope", 0.2)].into_py_dict(py); let lrelu_result: f64 = activators .getattr("leaky_relu")? .call((-1.0,), Some(&kwargs))? diff --git a/guide/src/python-from-rust/function-calls.md b/guide/src/python-from-rust/function-calls.md index fa9c047609a..3f27b7d2da5 100644 --- a/guide/src/python-from-rust/function-calls.md +++ b/guide/src/python-from-rust/function-calls.md @@ -88,17 +88,17 @@ fn main() -> PyResult<()> { .into(); // call object with PyDict - let kwargs = [(key1, val1)].into_py_dict_bound(py); + let kwargs = [(key1, val1)].into_py_dict(py); fun.call_bound(py, (), Some(&kwargs))?; // pass arguments as Vec let kwargs = vec![(key1, val1), (key2, val2)]; - fun.call_bound(py, (), Some(&kwargs.into_py_dict_bound(py)))?; + fun.call_bound(py, (), Some(&kwargs.into_py_dict(py)))?; // pass arguments as HashMap let mut kwargs = HashMap::<&str, i32>::new(); kwargs.insert(key1, 1); - fun.call_bound(py, (), Some(&kwargs.into_py_dict_bound(py)))?; + fun.call_bound(py, (), Some(&kwargs.into_py_dict(py)))?; Ok(()) }) diff --git a/newsfragments/4388.changed.md b/newsfragments/4388.changed.md new file mode 100644 index 00000000000..6fa66449c9c --- /dev/null +++ b/newsfragments/4388.changed.md @@ -0,0 +1 @@ +Renamed `IntoPyDict::into_py_dict_bound` into `IntoPyDict::into_py_dict`. diff --git a/pyo3-benches/benches/bench_bigint.rs b/pyo3-benches/benches/bench_bigint.rs index 99635a70279..2e585a504ff 100644 --- a/pyo3-benches/benches/bench_bigint.rs +++ b/pyo3-benches/benches/bench_bigint.rs @@ -8,7 +8,7 @@ use pyo3::types::PyDict; fn extract_bigint_extract_fail(bench: &mut Bencher<'_>) { Python::with_gil(|py| { - let d = PyDict::new_bound(py).into_any(); + let d = PyDict::new(py).into_any(); bench.iter(|| match black_box(&d).extract::() { Ok(v) => panic!("should err {}", v), diff --git a/pyo3-benches/benches/bench_decimal.rs b/pyo3-benches/benches/bench_decimal.rs index 53b79abbd38..49eee023deb 100644 --- a/pyo3-benches/benches/bench_decimal.rs +++ b/pyo3-benches/benches/bench_decimal.rs @@ -8,7 +8,7 @@ use pyo3::types::PyDict; fn decimal_via_extract(b: &mut Bencher<'_>) { Python::with_gil(|py| { - let locals = PyDict::new_bound(py); + let locals = PyDict::new(py); py.run_bound( r#" import decimal diff --git a/pyo3-benches/benches/bench_dict.rs b/pyo3-benches/benches/bench_dict.rs index 8c3dfe023c8..8d79de8a8e4 100644 --- a/pyo3-benches/benches/bench_dict.rs +++ b/pyo3-benches/benches/bench_dict.rs @@ -9,7 +9,7 @@ use pyo3::{prelude::*, types::PyMapping}; fn iter_dict(b: &mut Bencher<'_>) { Python::with_gil(|py| { const LEN: usize = 100_000; - let dict = (0..LEN as u64).map(|i| (i, i * 2)).into_py_dict_bound(py); + let dict = (0..LEN as u64).map(|i| (i, i * 2)).into_py_dict(py); let mut sum = 0; b.iter(|| { for (k, _v) in &dict { @@ -23,14 +23,14 @@ fn iter_dict(b: &mut Bencher<'_>) { fn dict_new(b: &mut Bencher<'_>) { Python::with_gil(|py| { const LEN: usize = 50_000; - b.iter_with_large_drop(|| (0..LEN as u64).map(|i| (i, i * 2)).into_py_dict_bound(py)); + b.iter_with_large_drop(|| (0..LEN as u64).map(|i| (i, i * 2)).into_py_dict(py)); }); } fn dict_get_item(b: &mut Bencher<'_>) { Python::with_gil(|py| { const LEN: usize = 50_000; - let dict = (0..LEN as u64).map(|i| (i, i * 2)).into_py_dict_bound(py); + let dict = (0..LEN as u64).map(|i| (i, i * 2)).into_py_dict(py); let mut sum = 0; b.iter(|| { for i in 0..LEN { @@ -48,7 +48,7 @@ fn dict_get_item(b: &mut Bencher<'_>) { fn extract_hashmap(b: &mut Bencher<'_>) { Python::with_gil(|py| { const LEN: usize = 100_000; - let dict = (0..LEN as u64).map(|i| (i, i * 2)).into_py_dict_bound(py); + let dict = (0..LEN as u64).map(|i| (i, i * 2)).into_py_dict(py); b.iter(|| HashMap::::extract_bound(&dict)); }); } @@ -56,7 +56,7 @@ fn extract_hashmap(b: &mut Bencher<'_>) { fn extract_btreemap(b: &mut Bencher<'_>) { Python::with_gil(|py| { const LEN: usize = 100_000; - let dict = (0..LEN as u64).map(|i| (i, i * 2)).into_py_dict_bound(py); + let dict = (0..LEN as u64).map(|i| (i, i * 2)).into_py_dict(py); b.iter(|| BTreeMap::::extract_bound(&dict)); }); } @@ -65,7 +65,7 @@ fn extract_btreemap(b: &mut Bencher<'_>) { fn extract_hashbrown_map(b: &mut Bencher<'_>) { Python::with_gil(|py| { const LEN: usize = 100_000; - let dict = (0..LEN as u64).map(|i| (i, i * 2)).into_py_dict_bound(py); + let dict = (0..LEN as u64).map(|i| (i, i * 2)).into_py_dict(py); b.iter(|| hashbrown::HashMap::::extract_bound(&dict)); }); } @@ -73,7 +73,7 @@ fn extract_hashbrown_map(b: &mut Bencher<'_>) { fn mapping_from_dict(b: &mut Bencher<'_>) { Python::with_gil(|py| { const LEN: usize = 100_000; - let dict = &(0..LEN as u64).map(|i| (i, i * 2)).into_py_dict_bound(py); + let dict = &(0..LEN as u64).map(|i| (i, i * 2)).into_py_dict(py); b.iter(|| black_box(dict).downcast::().unwrap()); }); } diff --git a/pyo3-benches/benches/bench_extract.rs b/pyo3-benches/benches/bench_extract.rs index 9bb7ef60ab4..c69069c1b57 100644 --- a/pyo3-benches/benches/bench_extract.rs +++ b/pyo3-benches/benches/bench_extract.rs @@ -17,7 +17,7 @@ fn extract_str_extract_success(bench: &mut Bencher<'_>) { fn extract_str_extract_fail(bench: &mut Bencher<'_>) { Python::with_gil(|py| { - let d = PyDict::new_bound(py).into_any(); + let d = PyDict::new(py).into_any(); bench.iter(|| match black_box(&d).extract::<&str>() { Ok(v) => panic!("should err {}", v), @@ -40,7 +40,7 @@ fn extract_str_downcast_success(bench: &mut Bencher<'_>) { fn extract_str_downcast_fail(bench: &mut Bencher<'_>) { Python::with_gil(|py| { - let d = PyDict::new_bound(py).into_any(); + let d = PyDict::new(py).into_any(); bench.iter(|| match black_box(&d).downcast::() { Ok(v) => panic!("should err {}", v), @@ -59,7 +59,7 @@ fn extract_int_extract_success(bench: &mut Bencher<'_>) { fn extract_int_extract_fail(bench: &mut Bencher<'_>) { Python::with_gil(|py| { - let d = PyDict::new_bound(py).into_any(); + let d = PyDict::new(py).into_any(); bench.iter(|| match black_box(&d).extract::() { Ok(v) => panic!("should err {}", v), @@ -81,7 +81,7 @@ fn extract_int_downcast_success(bench: &mut Bencher<'_>) { fn extract_int_downcast_fail(bench: &mut Bencher<'_>) { Python::with_gil(|py| { - let d = PyDict::new_bound(py).into_any(); + let d = PyDict::new(py).into_any(); bench.iter(|| match black_box(&d).downcast::() { Ok(v) => panic!("should err {}", v), @@ -100,7 +100,7 @@ fn extract_float_extract_success(bench: &mut Bencher<'_>) { fn extract_float_extract_fail(bench: &mut Bencher<'_>) { Python::with_gil(|py| { - let d = PyDict::new_bound(py).into_any(); + let d = PyDict::new(py).into_any(); bench.iter(|| match black_box(&d).extract::() { Ok(v) => panic!("should err {}", v), @@ -122,7 +122,7 @@ fn extract_float_downcast_success(bench: &mut Bencher<'_>) { fn extract_float_downcast_fail(bench: &mut Bencher<'_>) { Python::with_gil(|py| { - let d = PyDict::new_bound(py).into_any(); + let d = PyDict::new(py).into_any(); bench.iter(|| match black_box(&d).downcast::() { Ok(v) => panic!("should err {}", v), diff --git a/src/conversions/anyhow.rs b/src/conversions/anyhow.rs index 0b2bf2bacfa..4167c5379e1 100644 --- a/src/conversions/anyhow.rs +++ b/src/conversions/anyhow.rs @@ -146,7 +146,7 @@ mod test_anyhow { let pyerr = PyErr::from(err); Python::with_gil(|py| { - let locals = [("err", pyerr)].into_py_dict_bound(py); + let locals = [("err", pyerr)].into_py_dict(py); let pyerr = py.run_bound("raise err", None, Some(&locals)).unwrap_err(); assert_eq!(pyerr.value_bound(py).to_string(), expected_contents); }) @@ -163,7 +163,7 @@ mod test_anyhow { let pyerr = PyErr::from(err); Python::with_gil(|py| { - let locals = [("err", pyerr)].into_py_dict_bound(py); + let locals = [("err", pyerr)].into_py_dict(py); let pyerr = py.run_bound("raise err", None, Some(&locals)).unwrap_err(); assert_eq!(pyerr.value_bound(py).to_string(), expected_contents); }) diff --git a/src/conversions/chrono.rs b/src/conversions/chrono.rs index c50c2f7ef75..e7d496d1acd 100644 --- a/src/conversions/chrono.rs +++ b/src/conversions/chrono.rs @@ -578,7 +578,7 @@ mod tests { use crate::types::dict::PyDictMethods; Python::with_gil(|py| { - let locals = crate::types::PyDict::new_bound(py); + let locals = crate::types::PyDict::new(py); py.run_bound( "import zoneinfo; zi = zoneinfo.ZoneInfo('Europe/London')", None, @@ -1108,7 +1108,7 @@ mod tests { fn test_pyo3_offset_fixed_frompyobject_created_in_python(timestamp in 0..(i32::MAX as i64), timedelta in -86399i32..=86399i32) { Python::with_gil(|py| { - let globals = [("datetime", py.import_bound("datetime").unwrap())].into_py_dict_bound(py); + let globals = [("datetime", py.import_bound("datetime").unwrap())].into_py_dict(py); let code = format!("datetime.datetime.fromtimestamp({}).replace(tzinfo=datetime.timezone(datetime.timedelta(seconds={})))", timestamp, timedelta); let t = py.eval_bound(&code, Some(&globals), None).unwrap(); diff --git a/src/conversions/eyre.rs b/src/conversions/eyre.rs index 9a876c3f1ef..87894783d27 100644 --- a/src/conversions/eyre.rs +++ b/src/conversions/eyre.rs @@ -151,7 +151,7 @@ mod tests { let pyerr = PyErr::from(err); Python::with_gil(|py| { - let locals = [("err", pyerr)].into_py_dict_bound(py); + let locals = [("err", pyerr)].into_py_dict(py); let pyerr = py.run_bound("raise err", None, Some(&locals)).unwrap_err(); assert_eq!(pyerr.value_bound(py).to_string(), expected_contents); }) @@ -168,7 +168,7 @@ mod tests { let pyerr = PyErr::from(err); Python::with_gil(|py| { - let locals = [("err", pyerr)].into_py_dict_bound(py); + let locals = [("err", pyerr)].into_py_dict(py); let pyerr = py.run_bound("raise err", None, Some(&locals)).unwrap_err(); assert_eq!(pyerr.value_bound(py).to_string(), expected_contents); }) diff --git a/src/conversions/hashbrown.rs b/src/conversions/hashbrown.rs index 9eea7734bfc..12276748fa3 100644 --- a/src/conversions/hashbrown.rs +++ b/src/conversions/hashbrown.rs @@ -33,7 +33,7 @@ where H: hash::BuildHasher, { fn to_object(&self, py: Python<'_>) -> PyObject { - IntoPyDict::into_py_dict_bound(self, py).into() + IntoPyDict::into_py_dict(self, py).into() } } @@ -47,7 +47,7 @@ where let iter = self .into_iter() .map(|(k, v)| (k.into_py(py), v.into_py(py))); - IntoPyDict::into_py_dict_bound(iter, py).into() + IntoPyDict::into_py_dict(iter, py).into() } } @@ -163,7 +163,7 @@ mod tests { let mut map = hashbrown::HashMap::::new(); map.insert(1, 1); - let py_map = map.into_py_dict_bound(py); + let py_map = map.into_py_dict(py); assert_eq!(py_map.len(), 1); assert_eq!( diff --git a/src/conversions/indexmap.rs b/src/conversions/indexmap.rs index fdbe057f32d..2d79bb8fba1 100644 --- a/src/conversions/indexmap.rs +++ b/src/conversions/indexmap.rs @@ -98,7 +98,7 @@ where H: hash::BuildHasher, { fn to_object(&self, py: Python<'_>) -> PyObject { - IntoPyDict::into_py_dict_bound(self, py).into() + IntoPyDict::into_py_dict(self, py).into() } } @@ -112,7 +112,7 @@ where let iter = self .into_iter() .map(|(k, v)| (k.into_py(py), v.into_py(py))); - IntoPyDict::into_py_dict_bound(iter, py).into() + IntoPyDict::into_py_dict(iter, py).into() } } @@ -192,7 +192,7 @@ mod test_indexmap { let mut map = indexmap::IndexMap::::new(); map.insert(1, 1); - let py_map = map.into_py_dict_bound(py); + let py_map = map.into_py_dict(py); assert_eq!(py_map.len(), 1); assert_eq!( @@ -221,7 +221,7 @@ mod test_indexmap { } } - let py_map = map.clone().into_py_dict_bound(py); + let py_map = map.clone().into_py_dict(py); let trip_map = py_map.extract::>().unwrap(); diff --git a/src/conversions/num_bigint.rs b/src/conversions/num_bigint.rs index 708c51bca62..3fc5952540c 100644 --- a/src/conversions/num_bigint.rs +++ b/src/conversions/num_bigint.rs @@ -114,7 +114,7 @@ macro_rules! bigint_conversion { let bytes = $to_bytes(self); let bytes_obj = PyBytes::new(py, &bytes); let kwargs = if $is_signed { - let kwargs = crate::types::PyDict::new_bound(py); + let kwargs = crate::types::PyDict::new(py); kwargs.set_item(crate::intern!(py, "signed"), true).unwrap(); Some(kwargs) } else { @@ -297,7 +297,7 @@ fn int_to_py_bytes<'py>( use crate::intern; let py = long.py(); let kwargs = if is_signed { - let kwargs = crate::types::PyDict::new_bound(py); + let kwargs = crate::types::PyDict::new(py); kwargs.set_item(intern!(py, "signed"), true)?; Some(kwargs) } else { @@ -414,7 +414,7 @@ mod tests { fn convert_index_class() { Python::with_gil(|py| { let index = python_index_class(py); - let locals = PyDict::new_bound(py); + let locals = PyDict::new(py); locals.set_item("index", index).unwrap(); let ob = py.eval_bound("index.C(10)", None, Some(&locals)).unwrap(); let _: BigInt = ob.extract().unwrap(); diff --git a/src/conversions/num_rational.rs b/src/conversions/num_rational.rs index e3a0c7e6d3a..65d0b6f3714 100644 --- a/src/conversions/num_rational.rs +++ b/src/conversions/num_rational.rs @@ -115,7 +115,7 @@ mod tests { #[test] fn test_negative_fraction() { Python::with_gil(|py| { - let locals = PyDict::new_bound(py); + let locals = PyDict::new(py); py.run_bound( "import fractions\npy_frac = fractions.Fraction(-0.125)", None, @@ -131,7 +131,7 @@ mod tests { #[test] fn test_obj_with_incorrect_atts() { Python::with_gil(|py| { - let locals = PyDict::new_bound(py); + let locals = PyDict::new(py); py.run_bound( "not_fraction = \"contains_incorrect_atts\"", None, @@ -146,7 +146,7 @@ mod tests { #[test] fn test_fraction_with_fraction_type() { Python::with_gil(|py| { - let locals = PyDict::new_bound(py); + let locals = PyDict::new(py); py.run_bound( "import fractions\npy_frac = fractions.Fraction(fractions.Fraction(10))", None, @@ -163,7 +163,7 @@ mod tests { #[test] fn test_fraction_with_decimal() { Python::with_gil(|py| { - let locals = PyDict::new_bound(py); + let locals = PyDict::new(py); py.run_bound( "import fractions\n\nfrom decimal import Decimal\npy_frac = fractions.Fraction(Decimal(\"1.1\"))", None, @@ -180,7 +180,7 @@ mod tests { #[test] fn test_fraction_with_num_den() { Python::with_gil(|py| { - let locals = PyDict::new_bound(py); + let locals = PyDict::new(py); py.run_bound( "import fractions\npy_frac = fractions.Fraction(10,5)", None, @@ -245,7 +245,7 @@ mod tests { #[test] fn test_infinity() { Python::with_gil(|py| { - let locals = PyDict::new_bound(py); + let locals = PyDict::new(py); let py_bound = py.run_bound( "import fractions\npy_frac = fractions.Fraction(\"Infinity\")", None, diff --git a/src/conversions/rust_decimal.rs b/src/conversions/rust_decimal.rs index 782ca2e80f0..31178850318 100644 --- a/src/conversions/rust_decimal.rs +++ b/src/conversions/rust_decimal.rs @@ -116,7 +116,7 @@ mod test_rust_decimal { Python::with_gil(|py| { let rs_orig = $rs; let rs_dec = rs_orig.into_py(py); - let locals = PyDict::new_bound(py); + let locals = PyDict::new(py); locals.set_item("rs_dec", &rs_dec).unwrap(); // Checks if Rust Decimal -> Python Decimal conversion is correct py.run_bound( @@ -158,7 +158,7 @@ mod test_rust_decimal { let num = Decimal::from_parts(lo, mid, high, negative, scale); Python::with_gil(|py| { let rs_dec = num.into_py(py); - let locals = PyDict::new_bound(py); + let locals = PyDict::new(py); locals.set_item("rs_dec", &rs_dec).unwrap(); py.run_bound( &format!( @@ -184,7 +184,7 @@ mod test_rust_decimal { #[test] fn test_nan() { Python::with_gil(|py| { - let locals = PyDict::new_bound(py); + let locals = PyDict::new(py); py.run_bound( "import decimal\npy_dec = decimal.Decimal(\"NaN\")", None, @@ -200,7 +200,7 @@ mod test_rust_decimal { #[test] fn test_scientific_notation() { Python::with_gil(|py| { - let locals = PyDict::new_bound(py); + let locals = PyDict::new(py); py.run_bound( "import decimal\npy_dec = decimal.Decimal(\"1e3\")", None, @@ -217,7 +217,7 @@ mod test_rust_decimal { #[test] fn test_infinity() { Python::with_gil(|py| { - let locals = PyDict::new_bound(py); + let locals = PyDict::new(py); py.run_bound( "import decimal\npy_dec = decimal.Decimal(\"Infinity\")", None, diff --git a/src/conversions/smallvec.rs b/src/conversions/smallvec.rs index 80e6b09d611..7a6d5a4a78d 100644 --- a/src/conversions/smallvec.rs +++ b/src/conversions/smallvec.rs @@ -121,7 +121,7 @@ mod tests { #[test] fn test_smallvec_from_py_object_fails() { Python::with_gil(|py| { - let dict = PyDict::new_bound(py); + let dict = PyDict::new(py); let sv: PyResult> = dict.extract(); assert_eq!( sv.unwrap_err().to_string(), diff --git a/src/conversions/std/map.rs b/src/conversions/std/map.rs index 49eff4c1e8d..4343b28fb78 100644 --- a/src/conversions/std/map.rs +++ b/src/conversions/std/map.rs @@ -16,7 +16,7 @@ where H: hash::BuildHasher, { fn to_object(&self, py: Python<'_>) -> PyObject { - IntoPyDict::into_py_dict_bound(self, py).into() + IntoPyDict::into_py_dict(self, py).into() } } @@ -26,7 +26,7 @@ where V: ToPyObject, { fn to_object(&self, py: Python<'_>) -> PyObject { - IntoPyDict::into_py_dict_bound(self, py).into() + IntoPyDict::into_py_dict(self, py).into() } } @@ -40,7 +40,7 @@ where let iter = self .into_iter() .map(|(k, v)| (k.into_py(py), v.into_py(py))); - IntoPyDict::into_py_dict_bound(iter, py).into() + IntoPyDict::into_py_dict(iter, py).into() } #[cfg(feature = "experimental-inspect")] @@ -58,7 +58,7 @@ where let iter = self .into_iter() .map(|(k, v)| (k.into_py(py), v.into_py(py))); - IntoPyDict::into_py_dict_bound(iter, py).into() + IntoPyDict::into_py_dict(iter, py).into() } #[cfg(feature = "experimental-inspect")] diff --git a/src/conversions/std/num.rs b/src/conversions/std/num.rs index 1431b232169..2648f1d9ee8 100644 --- a/src/conversions/std/num.rs +++ b/src/conversions/std/num.rs @@ -436,7 +436,7 @@ mod test_128bit_integers { fn test_i128_roundtrip(x: i128) { Python::with_gil(|py| { let x_py = x.into_py(py); - let locals = PyDict::new_bound(py); + let locals = PyDict::new(py); locals.set_item("x_py", x_py.clone_ref(py)).unwrap(); py.run_bound(&format!("assert x_py == {}", x), None, Some(&locals)).unwrap(); let roundtripped: i128 = x_py.extract(py).unwrap(); @@ -452,7 +452,7 @@ mod test_128bit_integers { ) { Python::with_gil(|py| { let x_py = x.into_py(py); - let locals = PyDict::new_bound(py); + let locals = PyDict::new(py); locals.set_item("x_py", x_py.clone_ref(py)).unwrap(); py.run_bound(&format!("assert x_py == {}", x), None, Some(&locals)).unwrap(); let roundtripped: NonZeroI128 = x_py.extract(py).unwrap(); @@ -467,7 +467,7 @@ mod test_128bit_integers { fn test_u128_roundtrip(x: u128) { Python::with_gil(|py| { let x_py = x.into_py(py); - let locals = PyDict::new_bound(py); + let locals = PyDict::new(py); locals.set_item("x_py", x_py.clone_ref(py)).unwrap(); py.run_bound(&format!("assert x_py == {}", x), None, Some(&locals)).unwrap(); let roundtripped: u128 = x_py.extract(py).unwrap(); @@ -483,7 +483,7 @@ mod test_128bit_integers { ) { Python::with_gil(|py| { let x_py = x.into_py(py); - let locals = PyDict::new_bound(py); + let locals = PyDict::new(py); locals.set_item("x_py", x_py.clone_ref(py)).unwrap(); py.run_bound(&format!("assert x_py == {}", x), None, Some(&locals)).unwrap(); let roundtripped: NonZeroU128 = x_py.extract(py).unwrap(); diff --git a/src/conversions/std/time.rs b/src/conversions/std/time.rs index 89d61e696a1..c35cb914064 100755 --- a/src/conversions/std/time.rs +++ b/src/conversions/std/time.rs @@ -348,7 +348,7 @@ mod tests { fn max_datetime(py: Python<'_>) -> Bound<'_, PyAny> { let naive_max = datetime_class(py).getattr("max").unwrap(); - let kargs = PyDict::new_bound(py); + let kargs = PyDict::new(py); kargs.set_item("tzinfo", tz_utc(py)).unwrap(); naive_max.call_method("replace", (), Some(&kargs)).unwrap() } diff --git a/src/exceptions.rs b/src/exceptions.rs index e9be7a96606..e2649edd08b 100644 --- a/src/exceptions.rs +++ b/src/exceptions.rs @@ -62,7 +62,7 @@ macro_rules! impl_exception_boilerplate_bound { /// import_exception!(socket, gaierror); /// /// Python::with_gil(|py| { -/// let ctx = [("gaierror", py.get_type_bound::())].into_py_dict_bound(py); +/// let ctx = [("gaierror", py.get_type_bound::())].into_py_dict(py); /// pyo3::py_run!(py, *ctx, "import socket; assert gaierror is socket.gaierror"); /// }); /// @@ -175,7 +175,7 @@ macro_rules! import_exception_bound { /// # fn main() -> PyResult<()> { /// # Python::with_gil(|py| -> PyResult<()> { /// # let fun = wrap_pyfunction!(raise_myerror, py)?; -/// # let locals = pyo3::types::PyDict::new_bound(py); +/// # let locals = pyo3::types::PyDict::new(py); /// # locals.set_item("MyError", py.get_type_bound::())?; /// # locals.set_item("raise_myerror", fun)?; /// # @@ -826,7 +826,7 @@ mod tests { .map_err(|e| e.display(py)) .expect("could not import socket"); - let d = PyDict::new_bound(py); + let d = PyDict::new(py); d.set_item("socket", socket) .map_err(|e| e.display(py)) .expect("could not setitem"); @@ -850,7 +850,7 @@ mod tests { .map_err(|e| e.display(py)) .expect("could not import email"); - let d = PyDict::new_bound(py); + let d = PyDict::new(py); d.set_item("email", email) .map_err(|e| e.display(py)) .expect("could not setitem"); @@ -874,7 +874,7 @@ mod tests { Python::with_gil(|py| { let error_type = py.get_type_bound::(); - let ctx = [("CustomError", error_type)].into_py_dict_bound(py); + let ctx = [("CustomError", error_type)].into_py_dict(py); let type_description: String = py .eval_bound("str(CustomError)", None, Some(&ctx)) .unwrap() @@ -897,7 +897,7 @@ mod tests { create_exception!(mymodule.exceptions, CustomError, PyException); Python::with_gil(|py| { let error_type = py.get_type_bound::(); - let ctx = [("CustomError", error_type)].into_py_dict_bound(py); + let ctx = [("CustomError", error_type)].into_py_dict(py); let type_description: String = py .eval_bound("str(CustomError)", None, Some(&ctx)) .unwrap() @@ -916,7 +916,7 @@ mod tests { Python::with_gil(|py| { let error_type = py.get_type_bound::(); - let ctx = [("CustomError", error_type)].into_py_dict_bound(py); + let ctx = [("CustomError", error_type)].into_py_dict(py); let type_description: String = py .eval_bound("str(CustomError)", None, Some(&ctx)) .unwrap() @@ -949,7 +949,7 @@ mod tests { Python::with_gil(|py| { let error_type = py.get_type_bound::(); - let ctx = [("CustomError", error_type)].into_py_dict_bound(py); + let ctx = [("CustomError", error_type)].into_py_dict(py); let type_description: String = py .eval_bound("str(CustomError)", None, Some(&ctx)) .unwrap() diff --git a/src/ffi/tests.rs b/src/ffi/tests.rs index b7878c9626c..f2629ea5667 100644 --- a/src/ffi/tests.rs +++ b/src/ffi/tests.rs @@ -20,7 +20,7 @@ fn test_datetime_fromtimestamp() { PyDateTime_IMPORT(); Bound::from_owned_ptr(py, PyDateTime_FromTimestamp(args.as_ptr())) }; - let locals = PyDict::new_bound(py); + let locals = PyDict::new(py); locals.set_item("dt", dt).unwrap(); py.run_bound( "import datetime; assert dt == datetime.datetime.fromtimestamp(100)", @@ -41,7 +41,7 @@ fn test_date_fromtimestamp() { PyDateTime_IMPORT(); Bound::from_owned_ptr(py, PyDate_FromTimestamp(args.as_ptr())) }; - let locals = PyDict::new_bound(py); + let locals = PyDict::new(py); locals.set_item("dt", dt).unwrap(); py.run_bound( "import datetime; assert dt == datetime.date.fromtimestamp(100)", @@ -61,7 +61,7 @@ fn test_utc_timezone() { PyDateTime_IMPORT(); Bound::from_borrowed_ptr(py, PyDateTime_TimeZone_UTC()) }; - let locals = PyDict::new_bound(py); + let locals = PyDict::new(py); locals.set_item("utc_timezone", utc_timezone).unwrap(); py.run_bound( "import datetime; assert utc_timezone is datetime.timezone.utc", diff --git a/src/impl_/extract_argument.rs b/src/impl_/extract_argument.rs index 42b936a5ec7..3466dc268ea 100644 --- a/src/impl_/extract_argument.rs +++ b/src/impl_/extract_argument.rs @@ -762,7 +762,7 @@ impl<'py> VarkeywordsHandler<'py> for DictVarkeywords { _function_description: &FunctionDescription, ) -> PyResult<()> { varkeywords - .get_or_insert_with(|| PyDict::new_bound(name.py())) + .get_or_insert_with(|| PyDict::new(name.py())) .set_item(name, value) } } @@ -808,7 +808,7 @@ mod tests { Python::with_gil(|py| { let args = PyTuple::empty(py); - let kwargs = [("foo", 0u8)].into_py_dict_bound(py); + let kwargs = [("foo", 0u8)].into_py_dict(py); let err = unsafe { function_description .extract_arguments_tuple_dict::( @@ -839,7 +839,7 @@ mod tests { Python::with_gil(|py| { let args = PyTuple::empty(py); - let kwargs = [(1u8, 1u8)].into_py_dict_bound(py); + let kwargs = [(1u8, 1u8)].into_py_dict(py); let err = unsafe { function_description .extract_arguments_tuple_dict::( diff --git a/src/instance.rs b/src/instance.rs index 2287f5d3700..70d6919f5b7 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -786,7 +786,7 @@ impl IntoPy for Borrowed<'_, '_, T> { /// /// // `Bound<'py, PyDict>` inherits the GIL lifetime from `py` and /// // so won't be able to outlive this closure. -/// let dict: Bound<'_, PyDict> = PyDict::new_bound(py); +/// let dict: Bound<'_, PyDict> = PyDict::new(py); /// /// // because `Foo` contains `dict` its lifetime /// // is now also tied to `py`. @@ -815,7 +815,7 @@ impl IntoPy for Borrowed<'_, '_, T> { /// #[new] /// fn __new__() -> Foo { /// Python::with_gil(|py| { -/// let dict: Py = PyDict::new_bound(py).unbind(); +/// let dict: Py = PyDict::new(py).unbind(); /// Foo { inner: dict } /// }) /// } @@ -887,7 +887,7 @@ impl IntoPy for Borrowed<'_, '_, T> { /// /// # fn main() { /// Python::with_gil(|py| { -/// let first: Py = PyDict::new_bound(py).unbind(); +/// let first: Py = PyDict::new(py).unbind(); /// /// // All of these are valid syntax /// let second = Py::clone_ref(&first, py); @@ -1229,7 +1229,7 @@ impl Py { /// /// # fn main() { /// Python::with_gil(|py| { - /// let first: Py = PyDict::new_bound(py).unbind(); + /// let first: Py = PyDict::new(py).unbind(); /// let second = Py::clone_ref(&first, py); /// /// // Both point to the same object @@ -1261,7 +1261,7 @@ impl Py { /// /// # fn main() { /// Python::with_gil(|py| { - /// let object: Py = PyDict::new_bound(py).unbind(); + /// let object: Py = PyDict::new(py).unbind(); /// /// // some usage of object /// @@ -1740,7 +1740,7 @@ impl PyObject { /// use pyo3::types::{PyDict, PyList}; /// /// Python::with_gil(|py| { - /// let any: PyObject = PyDict::new_bound(py).into(); + /// let any: PyObject = PyDict::new(py).into(); /// /// assert!(any.downcast_bound::(py).is_ok()); /// assert!(any.downcast_bound::(py).is_err()); @@ -1818,7 +1818,7 @@ mod tests { assert_repr(obj.call1(py, ((('x', 1),),)).unwrap().bind(py), "{'x': 1}"); assert_repr( - obj.call_bound(py, (), Some(&[('x', 1)].into_py_dict_bound(py))) + obj.call_bound(py, (), Some(&[('x', 1)].into_py_dict(py))) .unwrap() .bind(py), "{'x': 1}", @@ -1829,7 +1829,7 @@ mod tests { #[test] fn test_call_for_non_existing_method() { Python::with_gil(|py| { - let obj: PyObject = PyDict::new_bound(py).into(); + let obj: PyObject = PyDict::new(py).into(); assert!(obj.call_method0(py, "asdf").is_err()); assert!(obj .call_method_bound(py, "nonexistent_method", (1,), None) @@ -1842,7 +1842,7 @@ mod tests { #[test] fn py_from_dict() { let dict: Py = Python::with_gil(|py| { - let native = PyDict::new_bound(py); + let native = PyDict::new(py); Py::from(native) }); @@ -1854,7 +1854,7 @@ mod tests { #[test] fn pyobject_from_py() { Python::with_gil(|py| { - let dict: Py = PyDict::new_bound(py).unbind(); + let dict: Py = PyDict::new(py).unbind(); let cnt = dict.get_refcnt(py); let p: PyObject = dict.into(); assert_eq!(p.get_refcnt(py), cnt); @@ -2074,7 +2074,7 @@ a = A() #[test] fn explicit_drop_ref() { Python::with_gil(|py| { - let object: Py = PyDict::new_bound(py).unbind(); + let object: Py = PyDict::new(py).unbind(); let object2 = object.clone_ref(py); assert_eq!(object.as_ptr(), object2.as_ptr()); diff --git a/src/lib.rs b/src/lib.rs index 16f7200519c..12a0ff9916f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -238,7 +238,7 @@ //! let sys = py.import_bound("sys")?; //! let version: String = sys.getattr("version")?.extract()?; //! -//! let locals = [("os", py.import_bound("os")?)].into_py_dict_bound(py); +//! let locals = [("os", py.import_bound("os")?)].into_py_dict(py); //! let code = "os.getenv('USER') or os.getenv('USERNAME') or 'Unknown'"; //! let user: String = py.eval_bound(code, None, Some(&locals))?.extract()?; //! diff --git a/src/macros.rs b/src/macros.rs index 51c54621850..60d146c3b22 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -73,7 +73,7 @@ /// } /// /// Python::with_gil(|py| { -/// let locals = [("C", py.get_type_bound::())].into_py_dict_bound(py); +/// let locals = [("C", py.get_type_bound::())].into_py_dict(py); /// pyo3::py_run!(py, *locals, "c = C()"); /// }); /// ``` @@ -99,7 +99,7 @@ macro_rules! py_run_impl { ($py:expr, $($val:ident)+, $code:expr) => {{ use $crate::types::IntoPyDict; use $crate::ToPyObject; - let d = [$((stringify!($val), $val.to_object($py)),)+].into_py_dict_bound($py); + let d = [$((stringify!($val), $val.to_object($py)),)+].into_py_dict($py); $crate::py_run_impl!($py, *d, $code) }}; ($py:expr, *$dict:expr, $code:expr) => {{ diff --git a/src/marker.rs b/src/marker.rs index c42c9722509..baf8d07f026 100644 --- a/src/marker.rs +++ b/src/marker.rs @@ -557,7 +557,7 @@ impl<'py> Python<'py> { /// types::{PyBytes, PyDict}, /// }; /// Python::with_gil(|py| { - /// let locals = PyDict::new_bound(py); + /// let locals = PyDict::new(py); /// py.run_bound( /// r#" /// import base64 @@ -815,7 +815,7 @@ mod tests { .unwrap(); assert_eq!(v, 1); - let d = [("foo", 13)].into_py_dict_bound(py); + let d = [("foo", 13)].into_py_dict(py); // Inject our own global namespace let v: i32 = py @@ -943,7 +943,7 @@ mod tests { use crate::types::dict::PyDictMethods; Python::with_gil(|py| { - let namespace = PyDict::new_bound(py); + let namespace = PyDict::new(py); py.run_bound("class Foo: pass", Some(&namespace), Some(&namespace)) .unwrap(); assert!(matches!(namespace.get_item("Foo"), Ok(Some(..)))); diff --git a/src/marshal.rs b/src/marshal.rs index dbd3c9a5a2d..197302891e5 100644 --- a/src/marshal.rs +++ b/src/marshal.rs @@ -24,7 +24,7 @@ pub const VERSION: i32 = 4; /// ``` /// # use pyo3::{marshal, types::PyDict, prelude::PyDictMethods}; /// # pyo3::Python::with_gil(|py| { -/// let dict = PyDict::new_bound(py); +/// let dict = PyDict::new(py); /// dict.set_item("aap", "noot").unwrap(); /// dict.set_item("mies", "wim").unwrap(); /// dict.set_item("zus", "jet").unwrap(); @@ -64,7 +64,7 @@ mod tests { #[test] fn marshal_roundtrip() { Python::with_gil(|py| { - let dict = PyDict::new_bound(py); + let dict = PyDict::new(py); dict.set_item("aap", "noot").unwrap(); dict.set_item("mies", "wim").unwrap(); dict.set_item("zus", "jet").unwrap(); diff --git a/src/sync.rs b/src/sync.rs index 4c4c8fb5dec..4e327e88a67 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -231,7 +231,7 @@ impl GILOnceCell> { /// /// #[pyfunction] /// fn create_dict(py: Python<'_>) -> PyResult> { -/// let dict = PyDict::new_bound(py); +/// let dict = PyDict::new(py); /// // 👇 A new `PyString` is created /// // for every call of this function. /// dict.set_item("foo", 42)?; @@ -240,7 +240,7 @@ impl GILOnceCell> { /// /// #[pyfunction] /// fn create_dict_faster(py: Python<'_>) -> PyResult> { -/// let dict = PyDict::new_bound(py); +/// let dict = PyDict::new(py); /// // 👇 A `PyString` is created once and reused /// // for the lifetime of the program. /// dict.set_item(intern!(py, "foo"), 42)?; @@ -296,7 +296,7 @@ mod tests { let foo2 = intern!(py, "foo"); let foo3 = intern!(py, stringify!(foo)); - let dict = PyDict::new_bound(py); + let dict = PyDict::new(py); dict.set_item(foo1, 42_usize).unwrap(); assert!(dict.contains(foo2).unwrap()); assert_eq!( diff --git a/src/tests/common.rs b/src/tests/common.rs index e1f2e7dfc28..c56249c2796 100644 --- a/src/tests/common.rs +++ b/src/tests/common.rs @@ -35,7 +35,7 @@ mod inner { // Case1: idents & no err_msg ($py:expr, $($val:ident)+, $code:expr, $err:ident) => {{ use pyo3::types::IntoPyDict; - let d = [$((stringify!($val), $val.to_object($py)),)+].into_py_dict_bound($py); + let d = [$((stringify!($val), $val.to_object($py)),)+].into_py_dict($py); py_expect_exception!($py, *d, $code, $err) }}; // Case2: dict & no err_msg @@ -119,7 +119,7 @@ mod inner { f: impl FnOnce(&Bound<'py, PyList>) -> PyResult, ) -> PyResult { let warnings = py.import_bound("warnings")?; - let kwargs = [("record", true)].into_py_dict_bound(py); + let kwargs = [("record", true)].into_py_dict(py); let catch_warnings = warnings .getattr("catch_warnings")? .call((), Some(&kwargs))?; diff --git a/src/types/any.rs b/src/types/any.rs index dd8edd885fb..bb5d0aa9c6a 100644 --- a/src/types/any.rs +++ b/src/types/any.rs @@ -424,7 +424,7 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed { /// let module = PyModule::from_code_bound(py, CODE, "", "")?; /// let fun = module.getattr("function")?; /// let args = ("hello",); - /// let kwargs = PyDict::new_bound(py); + /// let kwargs = PyDict::new(py); /// kwargs.set_item("cruel", "world")?; /// let result = fun.call(args, Some(&kwargs))?; /// assert_eq!(result.extract::()?, "called with args and kwargs"); @@ -516,7 +516,7 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed { /// let module = PyModule::from_code_bound(py, CODE, "", "")?; /// let instance = module.getattr("a")?; /// let args = ("hello",); - /// let kwargs = PyDict::new_bound(py); + /// let kwargs = PyDict::new(py); /// kwargs.set_item("cruel", "world")?; /// let result = instance.call_method("method", args, Some(&kwargs))?; /// assert_eq!(result.extract::()?, "called with args and kwargs"); @@ -676,7 +676,7 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed { /// use pyo3::types::{PyDict, PyList}; /// /// Python::with_gil(|py| { - /// let dict = PyDict::new_bound(py); + /// let dict = PyDict::new(py); /// assert!(dict.is_instance_of::()); /// let any = dict.as_any(); /// @@ -728,7 +728,7 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed { /// use pyo3::types::{PyDict, PyList}; /// /// Python::with_gil(|py| { - /// let obj: Bound<'_, PyAny> = PyDict::new_bound(py).into_any(); + /// let obj: Bound<'_, PyAny> = PyDict::new(py).into_any(); /// /// let obj: Bound<'_, PyAny> = match obj.downcast_into::() { /// Ok(_) => panic!("obj should not be a list"), @@ -1623,7 +1623,7 @@ class NonHeapNonDescriptorInt: fn test_call_with_kwargs() { Python::with_gil(|py| { let list = vec![3, 6, 5, 4, 7].to_object(py); - let dict = vec![("reverse", true)].into_py_dict_bound(py); + let dict = vec![("reverse", true)].into_py_dict(py); list.call_method_bound(py, "sort", (), Some(&dict)).unwrap(); assert_eq!(list.extract::>(py).unwrap(), vec![7, 6, 5, 4, 3]); }); diff --git a/src/types/bytearray.rs b/src/types/bytearray.rs index c56c312acbd..ec1aa29f841 100644 --- a/src/types/bytearray.rs +++ b/src/types/bytearray.rs @@ -161,7 +161,7 @@ pub trait PyByteArrayMethods<'py>: crate::sealed::Sealed { /// # fn main() -> PyResult<()> { /// # Python::with_gil(|py| -> PyResult<()> { /// # let fun = wrap_pyfunction!(a_valid_function, py)?; - /// # let locals = pyo3::types::PyDict::new_bound(py); + /// # let locals = pyo3::types::PyDict::new(py); /// # locals.set_item("a_valid_function", fun)?; /// # /// # py.run_bound( diff --git a/src/types/dict.rs b/src/types/dict.rs index dd6652a6969..8231e4df94f 100644 --- a/src/types/dict.rs +++ b/src/types/dict.rs @@ -63,10 +63,17 @@ pyobject_native_type_core!( impl PyDict { /// Creates a new empty dictionary. - pub fn new_bound(py: Python<'_>) -> Bound<'_, PyDict> { + pub fn new(py: Python<'_>) -> Bound<'_, PyDict> { unsafe { ffi::PyDict_New().assume_owned(py).downcast_into_unchecked() } } + /// Deprecated name for [`PyDict::new`]. + #[deprecated(since = "0.23.0", note = "renamed to `PyDict::new`")] + #[inline] + pub fn new_bound(py: Python<'_>) -> Bound<'_, PyDict> { + Self::new(py) + } + /// Creates a new dictionary from the sequence given. /// /// The sequence must consist of `(PyObject, PyObject)`. This is @@ -75,14 +82,22 @@ impl PyDict { /// Returns an error on invalid input. In the case of key collisions, /// this keeps the last entry seen. #[cfg(not(any(PyPy, GraalPy)))] - pub fn from_sequence_bound<'py>(seq: &Bound<'py, PyAny>) -> PyResult> { + pub fn from_sequence<'py>(seq: &Bound<'py, PyAny>) -> PyResult> { let py = seq.py(); - let dict = Self::new_bound(py); + let dict = Self::new(py); err::error_on_minusone(py, unsafe { ffi::PyDict_MergeFromSeq2(dict.as_ptr(), seq.as_ptr(), 1) })?; Ok(dict) } + + /// Deprecated name for [`PyDict::from_sequence`]. + #[cfg(not(any(PyPy, GraalPy)))] + #[deprecated(since = "0.23.0", note = "renamed to `PyDict::from_sequence`")] + #[inline] + pub fn from_sequence_bound<'py>(seq: &Bound<'py, PyAny>) -> PyResult> { + Self::from_sequence(seq) + } } /// Implementation of functionality for [`PyDict`]. @@ -519,7 +534,14 @@ pub(crate) use borrowed_iter::BorrowedDictIter; pub trait IntoPyDict: Sized { /// Converts self into a `PyDict` object pointer. Whether pointer owned or borrowed /// depends on implementation. - fn into_py_dict_bound(self, py: Python<'_>) -> Bound<'_, PyDict>; + fn into_py_dict(self, py: Python<'_>) -> Bound<'_, PyDict>; + + /// Deprecated name for [`IntoPyDict::into_py_dict`]. + #[deprecated(since = "0.23.0", note = "renamed to `IntoPyDict::into_py_dict`")] + #[inline] + fn into_py_dict_bound(self, py: Python<'_>) -> Bound<'_, PyDict> { + self.into_py_dict(py) + } } impl IntoPyDict for I @@ -527,8 +549,8 @@ where T: PyDictItem, I: IntoIterator, { - fn into_py_dict_bound(self, py: Python<'_>) -> Bound<'_, PyDict> { - let dict = PyDict::new_bound(py); + fn into_py_dict(self, py: Python<'_>) -> Bound<'_, PyDict> { + let dict = PyDict::new(py); for item in self { dict.set_item(item.key(), item.value()) .expect("Failed to set_item on dict"); @@ -584,7 +606,7 @@ mod tests { #[test] fn test_new() { Python::with_gil(|py| { - let dict = [(7, 32)].into_py_dict_bound(py); + let dict = [(7, 32)].into_py_dict(py); assert_eq!( 32, dict.get_item(7i32) @@ -606,7 +628,7 @@ mod tests { fn test_from_sequence() { Python::with_gil(|py| { let items = PyList::new(py, vec![("a", 1), ("b", 2)]); - let dict = PyDict::from_sequence_bound(&items).unwrap(); + let dict = PyDict::from_sequence(&items).unwrap(); assert_eq!( 1, dict.get_item("a") @@ -637,14 +659,14 @@ mod tests { fn test_from_sequence_err() { Python::with_gil(|py| { let items = PyList::new(py, vec!["a", "b"]); - assert!(PyDict::from_sequence_bound(&items).is_err()); + assert!(PyDict::from_sequence(&items).is_err()); }); } #[test] fn test_copy() { Python::with_gil(|py| { - let dict = [(7, 32)].into_py_dict_bound(py); + let dict = [(7, 32)].into_py_dict(py); let ndict = dict.copy().unwrap(); assert_eq!( @@ -740,7 +762,7 @@ mod tests { let obj = py.eval_bound("object()", None, None).unwrap(); { cnt = obj.get_refcnt(); - let _dict = [(10, &obj)].into_py_dict_bound(py); + let _dict = [(10, &obj)].into_py_dict(py); } { assert_eq!(cnt, obj.get_refcnt()); @@ -1005,7 +1027,7 @@ mod tests { let mut map = HashMap::::new(); map.insert(1, 1); - let py_map = map.into_py_dict_bound(py); + let py_map = map.into_py_dict(py); assert_eq!(py_map.len(), 1); assert_eq!( @@ -1026,7 +1048,7 @@ mod tests { let mut map = BTreeMap::::new(); map.insert(1, 1); - let py_map = map.into_py_dict_bound(py); + let py_map = map.into_py_dict(py); assert_eq!(py_map.len(), 1); assert_eq!( @@ -1045,7 +1067,7 @@ mod tests { fn test_vec_into_dict() { Python::with_gil(|py| { let vec = vec![("a", 1), ("b", 2), ("c", 3)]; - let py_map = vec.into_py_dict_bound(py); + let py_map = vec.into_py_dict(py); assert_eq!(py_map.len(), 3); assert_eq!( @@ -1064,7 +1086,7 @@ mod tests { fn test_slice_into_dict() { Python::with_gil(|py| { let arr = [("a", 1), ("b", 2), ("c", 3)]; - let py_map = arr.into_py_dict_bound(py); + let py_map = arr.into_py_dict(py); assert_eq!(py_map.len(), 3); assert_eq!( @@ -1085,7 +1107,7 @@ mod tests { let mut map = HashMap::::new(); map.insert(1, 1); - let py_map = map.into_py_dict_bound(py); + let py_map = map.into_py_dict(py); assert_eq!(py_map.as_mapping().len().unwrap(), 1); assert_eq!( @@ -1106,7 +1128,7 @@ mod tests { let mut map = HashMap::::new(); map.insert(1, 1); - let py_map = map.into_py_dict_bound(py); + let py_map = map.into_py_dict(py); let py_mapping = py_map.into_mapping(); assert_eq!(py_mapping.len().unwrap(), 1); @@ -1120,7 +1142,7 @@ mod tests { map.insert("a", 1); map.insert("b", 2); map.insert("c", 3); - map.into_py_dict_bound(py) + map.into_py_dict(py) } #[test] @@ -1162,8 +1184,8 @@ mod tests { #[test] fn dict_update() { Python::with_gil(|py| { - let dict = [("a", 1), ("b", 2), ("c", 3)].into_py_dict_bound(py); - let other = [("b", 4), ("c", 5), ("d", 6)].into_py_dict_bound(py); + let dict = [("a", 1), ("b", 2), ("c", 3)].into_py_dict(py); + let other = [("b", 4), ("c", 5), ("d", 6)].into_py_dict(py); dict.update(other.as_mapping()).unwrap(); assert_eq!(dict.len(), 4); assert_eq!( @@ -1233,8 +1255,8 @@ mod tests { #[test] fn dict_update_if_missing() { Python::with_gil(|py| { - let dict = [("a", 1), ("b", 2), ("c", 3)].into_py_dict_bound(py); - let other = [("b", 4), ("c", 5), ("d", 6)].into_py_dict_bound(py); + let dict = [("a", 1), ("b", 2), ("c", 3)].into_py_dict(py); + let other = [("b", 4), ("c", 5), ("d", 6)].into_py_dict(py); dict.update_if_missing(other.as_mapping()).unwrap(); assert_eq!(dict.len(), 4); assert_eq!( diff --git a/src/types/ellipsis.rs b/src/types/ellipsis.rs index 0f13c20d4e1..c14839eb253 100644 --- a/src/types/ellipsis.rs +++ b/src/types/ellipsis.rs @@ -67,7 +67,7 @@ mod tests { #[test] fn test_dict_is_not_ellipsis() { Python::with_gil(|py| { - assert!(PyDict::new_bound(py).downcast::().is_err()); + assert!(PyDict::new(py).downcast::().is_err()); }) } } diff --git a/src/types/iterator.rs b/src/types/iterator.rs index 57096ae97da..453a4882c46 100644 --- a/src/types/iterator.rs +++ b/src/types/iterator.rs @@ -183,7 +183,7 @@ def fibonacci(target): "#; Python::with_gil(|py| { - let context = PyDict::new_bound(py); + let context = PyDict::new(py); py.run_bound(fibonacci_generator, None, Some(&context)) .unwrap(); @@ -210,7 +210,7 @@ def fibonacci(target): "#; Python::with_gil(|py| { - let context = PyDict::new_bound(py); + let context = PyDict::new(py); py.run_bound(fibonacci_generator, None, Some(&context)) .unwrap(); diff --git a/src/types/none.rs b/src/types/none.rs index cfbba359113..e9846f21969 100644 --- a/src/types/none.rs +++ b/src/types/none.rs @@ -106,7 +106,7 @@ mod tests { #[test] fn test_dict_is_not_none() { Python::with_gil(|py| { - assert!(PyDict::new_bound(py).downcast::().is_err()); + assert!(PyDict::new(py).downcast::().is_err()); }) } } diff --git a/src/types/notimplemented.rs b/src/types/notimplemented.rs index b33f9217fb9..4f8164d39fb 100644 --- a/src/types/notimplemented.rs +++ b/src/types/notimplemented.rs @@ -70,9 +70,7 @@ mod tests { #[test] fn test_dict_is_not_notimplemented() { Python::with_gil(|py| { - assert!(PyDict::new_bound(py) - .downcast::() - .is_err()); + assert!(PyDict::new(py).downcast::().is_err()); }) } } diff --git a/src/types/traceback.rs b/src/types/traceback.rs index 7f716c6f24a..ef0062c6af9 100644 --- a/src/types/traceback.rs +++ b/src/types/traceback.rs @@ -102,7 +102,7 @@ mod tests { #[test] fn test_err_from_value() { Python::with_gil(|py| { - let locals = PyDict::new_bound(py); + let locals = PyDict::new(py); // Produce an error from python so that it has a traceback py.run_bound( r" @@ -124,7 +124,7 @@ except Exception as e: #[test] fn test_err_into_py() { Python::with_gil(|py| { - let locals = PyDict::new_bound(py); + let locals = PyDict::new(py); // Produce an error from python so that it has a traceback py.run_bound( r" diff --git a/tests/test_anyhow.rs b/tests/test_anyhow.rs index 3bd76bd637f..55ef3e8c46d 100644 --- a/tests/test_anyhow.rs +++ b/tests/test_anyhow.rs @@ -37,7 +37,7 @@ fn test_anyhow_py_function_err_result() { Python::with_gil(|py| { let func = wrap_pyfunction!(produce_err_result)(py).unwrap(); - let locals = PyDict::new_bound(py); + let locals = PyDict::new(py); locals.set_item("func", func).unwrap(); py.run_bound( diff --git a/tests/test_buffer_protocol.rs b/tests/test_buffer_protocol.rs index 700bcdcd206..6ee7b0db328 100644 --- a/tests/test_buffer_protocol.rs +++ b/tests/test_buffer_protocol.rs @@ -57,7 +57,7 @@ fn test_buffer() { }, ) .unwrap(); - let env = [("ob", instance)].into_py_dict_bound(py); + let env = [("ob", instance)].into_py_dict(py); py_assert!(py, *env, "bytes(ob) == b' 23'"); }); @@ -122,7 +122,7 @@ fn test_releasebuffer_unraisable_error() { let capture = UnraisableCapture::install(py); let instance = Py::new(py, ReleaseBufferError {}).unwrap(); - let env = [("ob", instance.clone_ref(py))].into_py_dict_bound(py); + let env = [("ob", instance.clone_ref(py))].into_py_dict(py); assert!(capture.borrow(py).capture.is_none()); diff --git a/tests/test_class_basics.rs b/tests/test_class_basics.rs index c1a4b923225..a255cc16de5 100644 --- a/tests/test_class_basics.rs +++ b/tests/test_class_basics.rs @@ -249,7 +249,7 @@ fn class_with_hash() { ("obj", Py::new(py, class).unwrap().into_any()), ("hsh", hash.into_py(py)), ] - .into_py_dict_bound(py); + .into_py_dict(py); py_assert!(py, *env, "hash(obj) == hsh"); }); diff --git a/tests/test_class_new.rs b/tests/test_class_new.rs index 1ccc152317d..b5e5a2b8c9c 100644 --- a/tests/test_class_new.rs +++ b/tests/test_class_new.rs @@ -29,7 +29,7 @@ fn empty_class_with_new() { // Calling with arbitrary args or kwargs is not ok assert!(typeobj.call(("some", "args"), None).is_err()); assert!(typeobj - .call((), Some(&[("some", "kwarg")].into_py_dict_bound(py))) + .call((), Some(&[("some", "kwarg")].into_py_dict(py))) .is_err()); }); } diff --git a/tests/test_coroutine.rs b/tests/test_coroutine.rs index 887251a5084..cfbaed334af 100644 --- a/tests/test_coroutine.rs +++ b/tests/test_coroutine.rs @@ -75,7 +75,7 @@ fn test_coroutine_qualname() { ), ("MyClass", gil.get_type_bound::().as_any()), ] - .into_py_dict_bound(gil); + .into_py_dict(gil); py_run!(gil, *locals, &handle_windows(test)); }) } @@ -302,7 +302,7 @@ fn test_async_method_receiver() { assert False assert asyncio.run(coro3) == 1 "#; - let locals = [("Counter", gil.get_type_bound::())].into_py_dict_bound(gil); + let locals = [("Counter", gil.get_type_bound::())].into_py_dict(gil); py_run!(gil, *locals, test); }); @@ -337,7 +337,7 @@ fn test_async_method_receiver_with_other_args() { assert asyncio.run(v.set_value(10)) == 10 assert asyncio.run(v.get_value_plus_with(1, 1)) == 12 "#; - let locals = [("Value", gil.get_type_bound::())].into_py_dict_bound(gil); + let locals = [("Value", gil.get_type_bound::())].into_py_dict(gil); py_run!(gil, *locals, test); }); } diff --git a/tests/test_datetime.rs b/tests/test_datetime.rs index 8a9d190ff7b..da820e3d264 100644 --- a/tests/test_datetime.rs +++ b/tests/test_datetime.rs @@ -12,7 +12,7 @@ fn _get_subclasses<'py>( // Import the class from Python and create some subclasses let datetime = py.import_bound("datetime")?; - let locals = [(py_type, datetime.getattr(py_type)?)].into_py_dict_bound(py); + let locals = [(py_type, datetime.getattr(py_type)?)].into_py_dict(py); let make_subclass_py = format!("class Subklass({}):\n pass", py_type); @@ -122,7 +122,7 @@ fn test_datetime_utc() { let dt = PyDateTime::new_bound(py, 2018, 1, 1, 0, 0, 0, 0, Some(&utc)).unwrap(); - let locals = [("dt", dt)].into_py_dict_bound(py); + let locals = [("dt", dt)].into_py_dict(py); let offset: f32 = py .eval_bound("dt.utcoffset().total_seconds()", None, Some(&locals)) diff --git a/tests/test_dict_iter.rs b/tests/test_dict_iter.rs index e502a4ca2b6..dc32eb61fd7 100644 --- a/tests/test_dict_iter.rs +++ b/tests/test_dict_iter.rs @@ -6,7 +6,7 @@ use pyo3::types::IntoPyDict; fn iter_dict_nosegv() { Python::with_gil(|py| { const LEN: usize = 10_000_000; - let dict = (0..LEN as u64).map(|i| (i, i * 2)).into_py_dict_bound(py); + let dict = (0..LEN as u64).map(|i| (i, i * 2)).into_py_dict(py); let mut sum = 0; for (k, _v) in dict { let i: u64 = k.extract().unwrap(); diff --git a/tests/test_enum.rs b/tests/test_enum.rs index 5b8d45a9e1d..cedb7ebaadb 100644 --- a/tests/test_enum.rs +++ b/tests/test_enum.rs @@ -259,7 +259,7 @@ fn test_simple_enum_with_hash() { ("obj", Py::new(py, class).unwrap().into_any()), ("hsh", hash.into_py(py)), ] - .into_py_dict_bound(py); + .into_py_dict(py); py_assert!(py, *env, "hash(obj) == hsh"); }); @@ -290,7 +290,7 @@ fn test_complex_enum_with_hash() { ("obj", Py::new(py, class).unwrap().into_any()), ("hsh", hash.into_py(py)), ] - .into_py_dict_bound(py); + .into_py_dict(py); py_assert!(py, *env, "hash(obj) == hsh"); }); diff --git a/tests/test_frompyobject.rs b/tests/test_frompyobject.rs index 9d8d81491cc..c50541a30d7 100644 --- a/tests/test_frompyobject.rs +++ b/tests/test_frompyobject.rs @@ -384,7 +384,7 @@ fn test_enum() { _ => panic!("Expected extracting Foo::StructVarGetAttrArg, got {:?}", f), } - let dict = PyDict::new_bound(py); + let dict = PyDict::new(py); dict.set_item("a", "test").expect("Failed to set item"); let f = dict .extract::>() @@ -394,7 +394,7 @@ fn test_enum() { _ => panic!("Expected extracting Foo::StructWithGetItem, got {:?}", f), } - let dict = PyDict::new_bound(py); + let dict = PyDict::new(py); dict.set_item("foo", "test").expect("Failed to set item"); let f = dict .extract::>() @@ -409,7 +409,7 @@ fn test_enum() { #[test] fn test_enum_error() { Python::with_gil(|py| { - let dict = PyDict::new_bound(py); + let dict = PyDict::new(py); let err = dict.extract::>().unwrap_err(); assert_eq!( err.to_string(), @@ -453,7 +453,7 @@ enum EnumWithCatchAll<'py> { #[test] fn test_enum_catch_all() { Python::with_gil(|py| { - let dict = PyDict::new_bound(py); + let dict = PyDict::new(py); let f = dict .extract::>() .expect("Failed to extract EnumWithCatchAll from dict"); @@ -483,7 +483,7 @@ pub enum Bar { #[test] fn test_err_rename() { Python::with_gil(|py| { - let dict = PyDict::new_bound(py); + let dict = PyDict::new(py); let f = dict.extract::(); assert!(f.is_err()); assert_eq!( diff --git a/tests/test_getter_setter.rs b/tests/test_getter_setter.rs index dcccffd3aa6..a28030c4de0 100644 --- a/tests/test_getter_setter.rs +++ b/tests/test_getter_setter.rs @@ -85,7 +85,7 @@ fn class_with_properties() { py_run!(py, inst, "inst.from_any = 15"); py_run!(py, inst, "assert inst.get_num() == 15"); - let d = [("C", py.get_type_bound::())].into_py_dict_bound(py); + let d = [("C", py.get_type_bound::())].into_py_dict(py); py_assert!(py, *d, "C.DATA.__doc__ == 'a getter for data'"); }); } diff --git a/tests/test_inheritance.rs b/tests/test_inheritance.rs index fc9a7699c1b..6037ae1a57b 100644 --- a/tests/test_inheritance.rs +++ b/tests/test_inheritance.rs @@ -20,7 +20,7 @@ struct SubclassAble {} #[test] fn subclass() { Python::with_gil(|py| { - let d = [("SubclassAble", py.get_type_bound::())].into_py_dict_bound(py); + let d = [("SubclassAble", py.get_type_bound::())].into_py_dict(py); py.run_bound( "class A(SubclassAble): pass\nassert issubclass(A, SubclassAble)", @@ -97,7 +97,7 @@ fn call_base_and_sub_methods() { fn mutation_fails() { Python::with_gil(|py| { let obj = Py::new(py, SubClass::new()).unwrap(); - let global = [("obj", obj)].into_py_dict_bound(py); + let global = [("obj", obj)].into_py_dict(py); let e = py .run_bound( "obj.base_set(lambda: obj.sub_set_and_ret(1))", @@ -275,7 +275,7 @@ mod inheriting_native_type { fn custom_exception() { Python::with_gil(|py| { let cls = py.get_type_bound::(); - let dict = [("cls", &cls)].into_py_dict_bound(py); + let dict = [("cls", &cls)].into_py_dict(py); let res = py.run_bound( "e = cls('hello'); assert str(e) == 'hello'; assert e.context == 'Hello :)'; raise e", None, diff --git a/tests/test_macro_docs.rs b/tests/test_macro_docs.rs index 6289626d32e..2eb21c52a00 100644 --- a/tests/test_macro_docs.rs +++ b/tests/test_macro_docs.rs @@ -23,7 +23,7 @@ impl MacroDocs { #[test] fn meth_doc() { Python::with_gil(|py| { - let d = [("C", py.get_type_bound::())].into_py_dict_bound(py); + let d = [("C", py.get_type_bound::())].into_py_dict(py); py_assert!( py, *d, diff --git a/tests/test_mapping.rs b/tests/test_mapping.rs index 784ab8845cd..65d07dd2611 100644 --- a/tests/test_mapping.rs +++ b/tests/test_mapping.rs @@ -71,7 +71,7 @@ impl Mapping { /// Return a dict with `m = Mapping(['1', '2', '3'])`. fn map_dict(py: Python<'_>) -> Bound<'_, pyo3::types::PyDict> { - let d = [("Mapping", py.get_type_bound::())].into_py_dict_bound(py); + let d = [("Mapping", py.get_type_bound::())].into_py_dict(py); py_run!(py, *d, "m = Mapping(['1', '2', '3'])"); d } diff --git a/tests/test_methods.rs b/tests/test_methods.rs index 16c82d0eca0..159cc210133 100644 --- a/tests/test_methods.rs +++ b/tests/test_methods.rs @@ -86,7 +86,7 @@ impl ClassMethod { #[test] fn class_method() { Python::with_gil(|py| { - let d = [("C", py.get_type_bound::())].into_py_dict_bound(py); + let d = [("C", py.get_type_bound::())].into_py_dict(py); py_assert!(py, *d, "C.method() == 'ClassMethod.method()!'"); py_assert!(py, *d, "C().method() == 'ClassMethod.method()!'"); py_assert!( @@ -113,7 +113,7 @@ impl ClassMethodWithArgs { #[test] fn class_method_with_args() { Python::with_gil(|py| { - let d = [("C", py.get_type_bound::())].into_py_dict_bound(py); + let d = [("C", py.get_type_bound::())].into_py_dict(py); py_assert!( py, *d, @@ -144,7 +144,7 @@ fn static_method() { Python::with_gil(|py| { assert_eq!(StaticMethod::method(py), "StaticMethod.method()!"); - let d = [("C", py.get_type_bound::())].into_py_dict_bound(py); + let d = [("C", py.get_type_bound::())].into_py_dict(py); py_assert!(py, *d, "C.method() == 'StaticMethod.method()!'"); py_assert!(py, *d, "C().method() == 'StaticMethod.method()!'"); py_assert!(py, *d, "C.method.__doc__ == 'Test static method.'"); @@ -168,7 +168,7 @@ fn static_method_with_args() { Python::with_gil(|py| { assert_eq!(StaticMethodWithArgs::method(py, 1234), "0x4d2"); - let d = [("C", py.get_type_bound::())].into_py_dict_bound(py); + let d = [("C", py.get_type_bound::())].into_py_dict(py); py_assert!(py, *d, "C.method(1337) == '0x539'"); }); } @@ -677,7 +677,7 @@ impl MethDocs { #[test] fn meth_doc() { Python::with_gil(|py| { - let d = [("C", py.get_type_bound::())].into_py_dict_bound(py); + let d = [("C", py.get_type_bound::())].into_py_dict(py); py_assert!(py, *d, "C.__doc__ == 'A class with \"documentation\".'"); py_assert!( py, @@ -764,7 +764,7 @@ fn method_with_pyclassarg() { Python::with_gil(|py| { let obj1 = Py::new(py, MethodWithPyClassArg { value: 10 }).unwrap(); let obj2 = Py::new(py, MethodWithPyClassArg { value: 10 }).unwrap(); - let d = [("obj1", obj1), ("obj2", obj2)].into_py_dict_bound(py); + let d = [("obj1", obj1), ("obj2", obj2)].into_py_dict(py); py_run!(py, *d, "obj = obj1.add(obj2); assert obj.value == 20"); py_run!(py, *d, "obj = obj1.add_pyref(obj2); assert obj.value == 20"); py_run!(py, *d, "obj = obj1.optional_add(); assert obj.value == 20"); diff --git a/tests/test_module.rs b/tests/test_module.rs index eba1bcce6d2..a300ff052d3 100644 --- a/tests/test_module.rs +++ b/tests/test_module.rs @@ -75,7 +75,7 @@ fn test_module_with_functions() { "module_with_functions", wrap_pymodule!(module_with_functions)(py), )] - .into_py_dict_bound(py); + .into_py_dict(py); py_assert!( py, @@ -132,7 +132,7 @@ fn test_module_with_explicit_py_arg() { "module_with_explicit_py_arg", wrap_pymodule!(module_with_explicit_py_arg)(py), )] - .into_py_dict_bound(py); + .into_py_dict(py); py_assert!(py, *d, "module_with_explicit_py_arg.double(3) == 6"); }); @@ -149,7 +149,7 @@ fn test_module_renaming() { use pyo3::wrap_pymodule; Python::with_gil(|py| { - let d = [("different_name", wrap_pymodule!(some_name)(py))].into_py_dict_bound(py); + let d = [("different_name", wrap_pymodule!(some_name)(py))].into_py_dict(py); py_run!(py, *d, "assert different_name.__name__ == 'other_name'"); }); diff --git a/tests/test_proto_methods.rs b/tests/test_proto_methods.rs index 71d8fa6eaad..597eed9b7cf 100644 --- a/tests/test_proto_methods.rs +++ b/tests/test_proto_methods.rs @@ -212,7 +212,7 @@ fn mapping() { let inst = Py::new( py, Mapping { - values: PyDict::new_bound(py).into(), + values: PyDict::new(py).into(), }, ) .unwrap(); diff --git a/tests/test_sequence.rs b/tests/test_sequence.rs index 8b1e3114797..22ae864c8cf 100644 --- a/tests/test_sequence.rs +++ b/tests/test_sequence.rs @@ -107,7 +107,7 @@ impl ByteSequence { /// Return a dict with `s = ByteSequence([1, 2, 3])`. fn seq_dict(py: Python<'_>) -> Bound<'_, pyo3::types::PyDict> { - let d = [("ByteSequence", py.get_type_bound::())].into_py_dict_bound(py); + let d = [("ByteSequence", py.get_type_bound::())].into_py_dict(py); // Though we can construct `s` in Rust, let's test `__new__` works. py_run!(py, *d, "s = ByteSequence([1, 2, 3])"); d @@ -139,7 +139,7 @@ fn test_setitem() { #[test] fn test_delitem() { Python::with_gil(|py| { - let d = [("ByteSequence", py.get_type_bound::())].into_py_dict_bound(py); + let d = [("ByteSequence", py.get_type_bound::())].into_py_dict(py); py_run!( py, @@ -235,7 +235,7 @@ fn test_repeat() { #[test] fn test_inplace_repeat() { Python::with_gil(|py| { - let d = [("ByteSequence", py.get_type_bound::())].into_py_dict_bound(py); + let d = [("ByteSequence", py.get_type_bound::())].into_py_dict(py); py_run!( py, diff --git a/tests/test_static_slots.rs b/tests/test_static_slots.rs index 3c270a3154c..581459b7d6e 100644 --- a/tests/test_static_slots.rs +++ b/tests/test_static_slots.rs @@ -38,7 +38,7 @@ impl Count5 { /// Return a dict with `s = Count5()`. fn test_dict(py: Python<'_>) -> Bound<'_, pyo3::types::PyDict> { - let d = [("Count5", py.get_type_bound::())].into_py_dict_bound(py); + let d = [("Count5", py.get_type_bound::())].into_py_dict(py); // Though we can construct `s` in Rust, let's test `__new__` works. py_run!(py, *d, "s = Count5()"); d diff --git a/tests/ui/wrong_aspyref_lifetimes.rs b/tests/ui/wrong_aspyref_lifetimes.rs index 755b0cf2a2c..d547e9e86fc 100644 --- a/tests/ui/wrong_aspyref_lifetimes.rs +++ b/tests/ui/wrong_aspyref_lifetimes.rs @@ -1,7 +1,7 @@ use pyo3::{types::PyDict, Bound, Py, Python}; fn main() { - let dict: Py = Python::with_gil(|py| PyDict::new_bound(py).unbind()); + let dict: Py = Python::with_gil(|py| PyDict::new(py).unbind()); // Should not be able to get access to Py contents outside of with_gil. let dict: &Bound<'_, PyDict> = Python::with_gil(|py| dict.bind(py)); From ac61a41bbbf7f5a0cd1f6a2bdceb12a62e4bc852 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers <7943856+bschoenmaeckers@users.noreply.github.com> Date: Thu, 1 Aug 2024 14:55:06 +0200 Subject: [PATCH 186/495] reintroduce `PyBool` constructors (#4402) --- guide/src/exception.md | 2 +- src/impl_/pyclass.rs | 2 +- src/types/any.rs | 6 +++--- src/types/boolobject.rs | 27 +++++++++++++++++---------- tests/test_class_attributes.rs | 4 ++-- 5 files changed, 24 insertions(+), 17 deletions(-) diff --git a/guide/src/exception.md b/guide/src/exception.md index a6193d6a50b..0235614f6ea 100644 --- a/guide/src/exception.md +++ b/guide/src/exception.md @@ -79,7 +79,7 @@ use pyo3::prelude::*; use pyo3::types::{PyBool, PyList}; Python::with_gil(|py| { - assert!(PyBool::new_bound(py, true).is_instance_of::()); + assert!(PyBool::new(py, true).is_instance_of::()); let list = PyList::new(py, &[1, 2, 3, 4]); assert!(!list.is_instance_of::()); assert!(list.is_instance_of::()); diff --git a/src/impl_/pyclass.rs b/src/impl_/pyclass.rs index 7ad772f0496..19242fce2ac 100644 --- a/src/impl_/pyclass.rs +++ b/src/impl_/pyclass.rs @@ -821,7 +821,7 @@ slot_fragment_trait! { // By default `__ne__` will try `__eq__` and invert the result let slf = Borrowed::from_ptr(py, slf); let other = Borrowed::from_ptr(py, other); - slf.eq(other).map(|is_eq| PyBool::new_bound(py, !is_eq).to_owned().into_ptr()) + slf.eq(other).map(|is_eq| PyBool::new(py, !is_eq).to_owned().into_ptr()) } } diff --git a/src/types/any.rs b/src/types/any.rs index bb5d0aa9c6a..84bb1182eda 100644 --- a/src/types/any.rs +++ b/src/types/any.rs @@ -761,7 +761,7 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed { /// use pyo3::types::{PyBool, PyInt}; /// /// Python::with_gil(|py| { - /// let b = PyBool::new_bound(py, true); + /// let b = PyBool::new(py, true); /// assert!(b.is_instance_of::()); /// let any: &Bound<'_, PyAny> = b.as_any(); /// @@ -1755,7 +1755,7 @@ class SimpleClass: let x = 5.to_object(py).into_bound(py); assert!(x.is_exact_instance_of::()); - let t = PyBool::new_bound(py, true); + let t = PyBool::new(py, true); assert!(t.is_instance_of::()); assert!(!t.is_exact_instance_of::()); assert!(t.is_exact_instance_of::()); @@ -1768,7 +1768,7 @@ class SimpleClass: #[test] fn test_any_is_exact_instance() { Python::with_gil(|py| { - let t = PyBool::new_bound(py, true); + let t = PyBool::new(py, true); assert!(t.is_instance(&py.get_type_bound::()).unwrap()); assert!(!t.is_exact_instance(&py.get_type_bound::())); assert!(t.is_exact_instance(&py.get_type_bound::())); diff --git a/src/types/boolobject.rs b/src/types/boolobject.rs index 4da827de6a8..e678ca2c601 100644 --- a/src/types/boolobject.rs +++ b/src/types/boolobject.rs @@ -27,13 +27,20 @@ impl PyBool { /// This returns a [`Borrowed`] reference to one of Pythons `True` or /// `False` singletons #[inline] - pub fn new_bound(py: Python<'_>, val: bool) -> Borrowed<'_, '_, Self> { + pub fn new(py: Python<'_>, val: bool) -> Borrowed<'_, '_, Self> { unsafe { if val { ffi::Py_True() } else { ffi::Py_False() } .assume_borrowed(py) .downcast_unchecked() } } + + /// Deprecated name for [`PyBool::new`]. + #[deprecated(since = "0.23.0", note = "renamed to `PyBool::new`")] + #[inline] + pub fn new_bound(py: Python<'_>, val: bool) -> Borrowed<'_, '_, Self> { + Self::new(py, val) + } } /// Implementation of functionality for [`PyBool`]. @@ -154,7 +161,7 @@ impl ToPyObject for bool { impl IntoPy for bool { #[inline] fn into_py(self, py: Python<'_>) -> PyObject { - PyBool::new_bound(py, self).into_py(py) + PyBool::new(py, self).into_py(py) } #[cfg(feature = "experimental-inspect")] @@ -237,28 +244,28 @@ mod tests { #[test] fn test_true() { Python::with_gil(|py| { - assert!(PyBool::new_bound(py, true).is_true()); - let t = PyBool::new_bound(py, true); + assert!(PyBool::new(py, true).is_true()); + let t = PyBool::new(py, true); assert!(t.extract::().unwrap()); - assert!(true.to_object(py).is(&*PyBool::new_bound(py, true))); + assert!(true.to_object(py).is(&*PyBool::new(py, true))); }); } #[test] fn test_false() { Python::with_gil(|py| { - assert!(!PyBool::new_bound(py, false).is_true()); - let t = PyBool::new_bound(py, false); + assert!(!PyBool::new(py, false).is_true()); + let t = PyBool::new(py, false); assert!(!t.extract::().unwrap()); - assert!(false.to_object(py).is(&*PyBool::new_bound(py, false))); + assert!(false.to_object(py).is(&*PyBool::new(py, false))); }); } #[test] fn test_pybool_comparisons() { Python::with_gil(|py| { - let py_bool = PyBool::new_bound(py, true); - let py_bool_false = PyBool::new_bound(py, false); + let py_bool = PyBool::new(py, true); + let py_bool_false = PyBool::new(py, false); let rust_bool = true; // Bound<'_, PyBool> == bool diff --git a/tests/test_class_attributes.rs b/tests/test_class_attributes.rs index a2e099549c0..53ed16320d0 100644 --- a/tests/test_class_attributes.rs +++ b/tests/test_class_attributes.rs @@ -190,12 +190,12 @@ fn test_renaming_all_struct_fields() { let struct_class = py.get_type_bound::(); let struct_obj = struct_class.call0().unwrap(); assert!(struct_obj - .setattr("firstField", PyBool::new_bound(py, false)) + .setattr("firstField", PyBool::new(py, false)) .is_ok()); py_assert!(py, struct_obj, "struct_obj.firstField == False"); py_assert!(py, struct_obj, "struct_obj.secondField == 5"); assert!(struct_obj - .setattr("third_field", PyBool::new_bound(py, true)) + .setattr("third_field", PyBool::new(py, true)) .is_ok()); py_assert!(py, struct_obj, "struct_obj.third_field == True"); }); From da8d91a5721d18ce95c8a946bc7c6146bc4d6ea0 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers <7943856+bschoenmaeckers@users.noreply.github.com> Date: Thu, 1 Aug 2024 17:51:36 +0200 Subject: [PATCH 187/495] reintroduce `PyCapsule` constructors (#4403) --- src/instance.rs | 4 ++-- src/types/capsule.rs | 54 ++++++++++++++++++++++++++++++++----------- src/types/function.rs | 2 +- 3 files changed, 43 insertions(+), 17 deletions(-) diff --git a/src/instance.rs b/src/instance.rs index 70d6919f5b7..8a89a2db935 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -2005,7 +2005,7 @@ a = A() method: impl FnOnce(*mut ffi::PyObject) -> Bound<'py, PyAny>, ) { let mut dropped = false; - let capsule = PyCapsule::new_bound_with_destructor( + let capsule = PyCapsule::new_with_destructor( py, (&mut dropped) as *mut _ as usize, None, @@ -2044,7 +2044,7 @@ a = A() method: impl FnOnce(&*mut ffi::PyObject) -> Borrowed<'_, 'py, PyAny>, ) { let mut dropped = false; - let capsule = PyCapsule::new_bound_with_destructor( + let capsule = PyCapsule::new_with_destructor( py, (&mut dropped) as *mut _ as usize, None, diff --git a/src/types/capsule.rs b/src/types/capsule.rs index 63cef1bed17..83ce85faf47 100644 --- a/src/types/capsule.rs +++ b/src/types/capsule.rs @@ -33,7 +33,7 @@ use std::os::raw::{c_char, c_int, c_void}; /// let foo = Foo { val: 123 }; /// let name = CString::new("builtins.capsule").unwrap(); /// -/// let capsule = PyCapsule::new_bound(py, foo, Some(name.clone()))?; +/// let capsule = PyCapsule::new(py, foo, Some(name.clone()))?; /// /// let module = PyModule::import_bound(py, "builtins")?; /// module.add("capsule", capsule)?; @@ -65,7 +65,7 @@ impl PyCapsule { /// /// Python::with_gil(|py| { /// let name = CString::new("foo").unwrap(); - /// let capsule = PyCapsule::new_bound(py, 123_u32, Some(name)).unwrap(); + /// let capsule = PyCapsule::new(py, 123_u32, Some(name)).unwrap(); /// let val = unsafe { capsule.reference::() }; /// assert_eq!(*val, 123); /// }); @@ -78,15 +78,26 @@ impl PyCapsule { /// use std::ffi::CString; /// /// Python::with_gil(|py| { - /// let capsule = PyCapsule::new_bound(py, (), None).unwrap(); // Oops! `()` is zero sized! + /// let capsule = PyCapsule::new(py, (), None).unwrap(); // Oops! `()` is zero sized! /// }); /// ``` + pub fn new( + py: Python<'_>, + value: T, + name: Option, + ) -> PyResult> { + Self::new_with_destructor(py, value, name, |_, _| {}) + } + + /// Deprecated name for [`PyCapsule::new`]. + #[deprecated(since = "0.23.0", note = "renamed to `PyCapsule::new`")] + #[inline] pub fn new_bound( py: Python<'_>, value: T, name: Option, ) -> PyResult> { - Self::new_bound_with_destructor(py, value, name, |_, _| {}) + Self::new(py, value, name) } /// Constructs a new capsule whose contents are `value`, associated with `name`. @@ -96,7 +107,7 @@ impl PyCapsule { /// /// The `destructor` must be `Send`, because there is no guarantee which thread it will eventually /// be called from. - pub fn new_bound_with_destructor< + pub fn new_with_destructor< T: 'static + Send + AssertNotZeroSized, F: FnOnce(T, *mut c_void) + Send, >( @@ -128,6 +139,21 @@ impl PyCapsule { } } + /// Deprecated name for [`PyCapsule::new_with_destructor`]. + #[deprecated(since = "0.23.0", note = "renamed to `PyCapsule::new_with_destructor`")] + #[inline] + pub fn new_bound_with_destructor< + T: 'static + Send + AssertNotZeroSized, + F: FnOnce(T, *mut c_void) + Send, + >( + py: Python<'_>, + value: T, + name: Option, + destructor: F, + ) -> PyResult> { + Self::new_with_destructor(py, value, name, destructor) + } + /// Imports an existing capsule. /// /// The `name` should match the path to the module attribute exactly in the form @@ -181,7 +207,7 @@ pub trait PyCapsuleMethods<'py>: crate::sealed::Sealed { /// /// Python::with_gil(|py| { /// let capsule = - /// PyCapsule::new_bound_with_destructor(py, 123, None, destructor as fn(u32, *mut c_void)) + /// PyCapsule::new_with_destructor(py, 123, None, destructor as fn(u32, *mut c_void)) /// .unwrap(); /// let context = Box::new(tx); // `Sender` is our context, box it up and ship it! /// capsule.set_context(Box::into_raw(context).cast()).unwrap(); @@ -357,7 +383,7 @@ mod tests { let foo = Foo { val: 123 }; let name = CString::new("foo").unwrap(); - let cap = PyCapsule::new_bound(py, foo, Some(name.clone()))?; + let cap = PyCapsule::new(py, foo, Some(name.clone()))?; assert!(cap.is_valid()); let foo_capi = unsafe { cap.reference::() }; @@ -376,7 +402,7 @@ mod tests { let cap: Py = Python::with_gil(|py| { let name = CString::new("foo").unwrap(); - let cap = PyCapsule::new_bound(py, foo as fn(u32) -> u32, Some(name)).unwrap(); + let cap = PyCapsule::new(py, foo as fn(u32) -> u32, Some(name)).unwrap(); cap.into() }); @@ -390,7 +416,7 @@ mod tests { fn test_pycapsule_context() -> PyResult<()> { Python::with_gil(|py| { let name = CString::new("foo").unwrap(); - let cap = PyCapsule::new_bound(py, 0, Some(name))?; + let cap = PyCapsule::new(py, 0, Some(name))?; let c = cap.context()?; assert!(c.is_null()); @@ -416,7 +442,7 @@ mod tests { let foo = Foo { val: 123 }; let name = CString::new("builtins.capsule").unwrap(); - let capsule = PyCapsule::new_bound(py, foo, Some(name.clone()))?; + let capsule = PyCapsule::new(py, foo, Some(name.clone()))?; let module = PyModule::import_bound(py, "builtins")?; module.add("capsule", capsule)?; @@ -439,7 +465,7 @@ mod tests { let name = CString::new("foo").unwrap(); let stuff: Vec = vec![1, 2, 3, 4]; - let cap = PyCapsule::new_bound(py, stuff, Some(name)).unwrap(); + let cap = PyCapsule::new(py, stuff, Some(name)).unwrap(); cap.into() }); @@ -456,7 +482,7 @@ mod tests { let cap: Py = Python::with_gil(|py| { let name = CString::new("foo").unwrap(); - let cap = PyCapsule::new_bound(py, 0, Some(name)).unwrap(); + let cap = PyCapsule::new(py, 0, Some(name)).unwrap(); cap.set_context(Box::into_raw(Box::new(&context)).cast()) .unwrap(); @@ -482,7 +508,7 @@ mod tests { Python::with_gil(move |py| { let name = CString::new("foo").unwrap(); - let cap = PyCapsule::new_bound_with_destructor(py, 0, Some(name), destructor).unwrap(); + let cap = PyCapsule::new_with_destructor(py, 0, Some(name), destructor).unwrap(); cap.set_context(Box::into_raw(Box::new(tx)).cast()).unwrap(); }); @@ -493,7 +519,7 @@ mod tests { #[test] fn test_pycapsule_no_name() { Python::with_gil(|py| { - let cap = PyCapsule::new_bound(py, 0usize, None).unwrap(); + let cap = PyCapsule::new(py, 0usize, None).unwrap(); assert_eq!(unsafe { cap.reference::() }, &0usize); assert_eq!(cap.name().unwrap(), None); diff --git a/src/types/function.rs b/src/types/function.rs index 65c17984ad2..38d21e32428 100644 --- a/src/types/function.rs +++ b/src/types/function.rs @@ -86,7 +86,7 @@ impl PyCFunction { pymethods::PyMethodDef::cfunction_with_keywords(name, run_closure::, doc); let def = method_def.as_method_def(); - let capsule = PyCapsule::new_bound( + let capsule = PyCapsule::new( py, ClosureDestructor:: { closure, From b50fd81c856c46277828efe2e56f965fd1a09d24 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Thu, 1 Aug 2024 10:31:45 -0600 Subject: [PATCH 188/495] Update dict.get_item binding to use PyDict_GetItemRef (#4355) * Update dict.get_item binding to use PyDict_GetItemRef Refs #4265 * test: add test for dict.get_item error path * test: add test for dict.get_item error path * test: add test for dict.get_item error path * fix: fix logic error in dict.get_item bindings * update: apply david's review suggestions for dict.get_item bindings * update: create ffi::compat to store compatibility shims * update: move PyDict_GetItemRef bindings to spot in order from dictobject.h * build: fix build warning with --no-default-features * doc: expand release note fragments * fix: fix clippy warnings * respond to review comments * Apply suggestion from @mejrs * refactor so cfg is applied to functions * properly set cfgs * fix clippy lints * Apply @davidhewitt's suggestion * deal with upstream deprecation of new_bound --- newsfragments/4355.added.md | 10 ++++++++ newsfragments/4355.fixed.md | 2 ++ pyo3-ffi/src/compat.rs | 44 +++++++++++++++++++++++++++++++++++ pyo3-ffi/src/dictobject.rs | 6 +++++ pyo3-ffi/src/lib.rs | 3 +++ src/types/dict.rs | 46 +++++++++++++++++++++++++++++++++---- 6 files changed, 106 insertions(+), 5 deletions(-) create mode 100644 newsfragments/4355.added.md create mode 100644 newsfragments/4355.fixed.md create mode 100644 pyo3-ffi/src/compat.rs diff --git a/newsfragments/4355.added.md b/newsfragments/4355.added.md new file mode 100644 index 00000000000..1410c0720bf --- /dev/null +++ b/newsfragments/4355.added.md @@ -0,0 +1,10 @@ +* Added an `ffi::compat` namespace to store compatibility shims for C API + functions added in recent versions of Python. + +* Added bindings for `PyDict_GetItemRef` on Python 3.13 and newer. Also added + `ffi::compat::PyDict_GetItemRef` which re-exports the FFI binding on Python + 3.13 or newer and defines a compatibility version on older versions of + Python. This function is inherently safer to use than `PyDict_GetItem` and has + an API that is easier to use than `PyDict_GetItemWithError`. It returns a + strong reference to value, as opposed to the two older functions which return + a possibly unsafe borrowed reference. diff --git a/newsfragments/4355.fixed.md b/newsfragments/4355.fixed.md new file mode 100644 index 00000000000..9a141bc6b96 --- /dev/null +++ b/newsfragments/4355.fixed.md @@ -0,0 +1,2 @@ +Avoid creating temporary borrowed reference in dict.get_item bindings. Borrowed +references like this are unsafe in the free-threading build. diff --git a/pyo3-ffi/src/compat.rs b/pyo3-ffi/src/compat.rs new file mode 100644 index 00000000000..ef9a3fbe1c9 --- /dev/null +++ b/pyo3-ffi/src/compat.rs @@ -0,0 +1,44 @@ +//! C API Compatibility Shims +//! +//! Some CPython C API functions added in recent versions of Python are +//! inherently safer to use than older C API constructs. This module +//! exposes functions available on all Python versions that wrap the +//! old C API on old Python versions and wrap the function directly +//! on newer Python versions. + +// Unless otherwise noted, the compatibility shims are adapted from +// the pythoncapi-compat project: https://github.com/python/pythoncapi-compat + +#[cfg(not(Py_3_13))] +use crate::object::PyObject; +#[cfg(not(Py_3_13))] +use std::os::raw::c_int; + +#[cfg_attr(docsrs, doc(cfg(all)))] +#[cfg(Py_3_13)] +pub use crate::dictobject::PyDict_GetItemRef; + +#[cfg_attr(docsrs, doc(cfg(all)))] +#[cfg(not(Py_3_13))] +pub unsafe fn PyDict_GetItemRef( + dp: *mut PyObject, + key: *mut PyObject, + result: *mut *mut PyObject, +) -> c_int { + { + use crate::dictobject::PyDict_GetItemWithError; + use crate::object::_Py_NewRef; + use crate::pyerrors::PyErr_Occurred; + + let item: *mut PyObject = PyDict_GetItemWithError(dp, key); + if !item.is_null() { + *result = _Py_NewRef(item); + return 1; // found + } + *result = std::ptr::null_mut(); + if PyErr_Occurred().is_null() { + return 0; // not found + } + -1 + } +} diff --git a/pyo3-ffi/src/dictobject.rs b/pyo3-ffi/src/dictobject.rs index 99fc56b246b..4d8315d441e 100644 --- a/pyo3-ffi/src/dictobject.rs +++ b/pyo3-ffi/src/dictobject.rs @@ -66,6 +66,12 @@ extern "C" { ) -> c_int; #[cfg_attr(PyPy, link_name = "PyPyDict_DelItemString")] pub fn PyDict_DelItemString(dp: *mut PyObject, key: *const c_char) -> c_int; + #[cfg(Py_3_13)] + pub fn PyDict_GetItemRef( + dp: *mut PyObject, + key: *mut PyObject, + result: *mut *mut PyObject, + ) -> c_int; // skipped 3.10 / ex-non-limited PyObject_GenericGetDict } diff --git a/pyo3-ffi/src/lib.rs b/pyo3-ffi/src/lib.rs index ff4d03d3a44..55c7f31404f 100644 --- a/pyo3-ffi/src/lib.rs +++ b/pyo3-ffi/src/lib.rs @@ -1,3 +1,4 @@ +#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] //! Raw FFI declarations for Python's C API. //! //! PyO3 can be used to write native Python modules or run Python code and modules from Rust. @@ -290,6 +291,8 @@ pub const fn _cstr_from_utf8_with_nul_checked(s: &str) -> &CStr { use std::ffi::CStr; +pub mod compat; + pub use self::abstract_::*; pub use self::bltinmodule::*; pub use self::boolobject::*; diff --git a/src/types/dict.rs b/src/types/dict.rs index 8231e4df94f..20664541f0c 100644 --- a/src/types/dict.rs +++ b/src/types/dict.rs @@ -247,13 +247,13 @@ impl<'py> PyDictMethods<'py> for Bound<'py, PyDict> { key: Bound<'_, PyAny>, ) -> PyResult>> { let py = dict.py(); + let mut result: *mut ffi::PyObject = std::ptr::null_mut(); match unsafe { - ffi::PyDict_GetItemWithError(dict.as_ptr(), key.as_ptr()) - .assume_borrowed_or_opt(py) - .map(Borrowed::to_owned) + ffi::compat::PyDict_GetItemRef(dict.as_ptr(), key.as_ptr(), &mut result) } { - some @ Some(_) => Ok(some), - None => PyErr::take(py).map(Err).transpose(), + std::os::raw::c_int::MIN..=-1 => Err(PyErr::fetch(py)), + 0 => Ok(None), + 1..=std::os::raw::c_int::MAX => Ok(Some(unsafe { result.assume_owned(py) })), } } @@ -727,6 +727,42 @@ mod tests { }); } + #[cfg(feature = "macros")] + #[test] + fn test_get_item_error_path() { + use crate::exceptions::PyTypeError; + + #[crate::pyclass(crate = "crate")] + struct HashErrors; + + #[crate::pymethods(crate = "crate")] + impl HashErrors { + #[new] + fn new() -> Self { + HashErrors {} + } + + fn __hash__(&self) -> PyResult { + Err(PyTypeError::new_err("Error from __hash__")) + } + } + + Python::with_gil(|py| { + let class = py.get_type_bound::(); + let instance = class.call0().unwrap(); + let d = PyDict::new(py); + match d.get_item(instance) { + Ok(_) => { + panic!("this get_item call should always error") + } + Err(err) => { + assert!(err.is_instance_of::(py)); + assert_eq!(err.value_bound(py).to_string(), "Error from __hash__") + } + } + }) + } + #[test] fn test_set_item() { Python::with_gil(|py| { From ef0f544fa55258e0010d9a15ff41bb596e12b1f3 Mon Sep 17 00:00:00 2001 From: Georg Brandl Date: Thu, 1 Aug 2024 21:41:35 +0200 Subject: [PATCH 189/495] fix returning tuples from async fns (#4407) Fixes #4400 As the return value is ultimately communicated back via a StopIteration exception instance, a peculiar behavior of `PyErr::new` is encountered here: when the argument is a tuple `arg`, it is used to construct the exception as if calling from Python `Exception(*arg)` and not `Exception(arg)` like for every other type of argument. This comes from from CPython's `PyErr_SetObject` which ultimately calls `_PyErr_CreateException` where the "culprit" is found here: https://github.com/python/cpython/blob/main/Python/errors.c#L33 We can fix this particular bug in the invocation of `PyErr::new` but it is a more general question if we want to keep reflecting this somewhat surprising CPython behavior, or create a better API, introducing a breaking change. --- newsfragments/4407.fixed.md | 1 + src/coroutine.rs | 2 +- tests/test_coroutine.rs | 14 ++++++++++++++ 3 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 newsfragments/4407.fixed.md diff --git a/newsfragments/4407.fixed.md b/newsfragments/4407.fixed.md new file mode 100644 index 00000000000..be2706bca05 --- /dev/null +++ b/newsfragments/4407.fixed.md @@ -0,0 +1 @@ +Fix async functions returning a tuple only returning the first element to Python. diff --git a/src/coroutine.rs b/src/coroutine.rs index f2feab4af16..652292c7892 100644 --- a/src/coroutine.rs +++ b/src/coroutine.rs @@ -95,7 +95,7 @@ impl Coroutine { match panic::catch_unwind(panic::AssertUnwindSafe(poll)) { Ok(Poll::Ready(res)) => { self.close(); - return Err(PyStopIteration::new_err(res?)); + return Err(PyStopIteration::new_err((res?,))); } Err(err) => { self.close(); diff --git a/tests/test_coroutine.rs b/tests/test_coroutine.rs index cfbaed334af..d99e7f80448 100644 --- a/tests/test_coroutine.rs +++ b/tests/test_coroutine.rs @@ -121,6 +121,20 @@ fn sleep_coroutine() { }) } +#[pyfunction] +async fn return_tuple() -> (usize, usize) { + (42, 43) +} + +#[test] +fn tuple_coroutine() { + Python::with_gil(|gil| { + let func = wrap_pyfunction!(return_tuple, gil).unwrap(); + let test = r#"import asyncio; assert asyncio.run(func()) == (42, 43)"#; + py_run!(gil, func, &handle_windows(test)); + }) +} + #[test] fn cancelled_coroutine() { Python::with_gil(|gil| { From 2ee7c3211f04f12d4f1251dcf125a094ae472e76 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers <7943856+bschoenmaeckers@users.noreply.github.com> Date: Thu, 1 Aug 2024 23:50:58 +0200 Subject: [PATCH 190/495] Reintroduce constructors for `PyByteArray`, `PyComplex`, `PyFloat`, `PyEllipsis`, `PyFrozenSet` (#4409) * Reintroduce `bytearray` constructors * Reintroduce `PyComplex` constructors * Reintroduce `PyFloat` constructors * Reintroduce `PyEllipsis` constructors * Reintroduce `PyFrozenSet` constructors --- guide/src/class/numeric.md | 4 +- src/conversions/hashbrown.rs | 2 +- src/conversions/num_complex.rs | 8 ++-- src/conversions/std/set.rs | 4 +- src/marker.rs | 2 +- src/pybacked.rs | 10 ++--- src/tests/hygiene/pymethods.rs | 2 +- src/types/any.rs | 6 +-- src/types/bytearray.rs | 68 +++++++++++++++++++++++----------- src/types/complex.rs | 43 ++++++++++++--------- src/types/ellipsis.rs | 17 ++++++--- src/types/float.rs | 23 ++++++++---- src/types/frozenset.rs | 48 ++++++++++++++++++------ 13 files changed, 153 insertions(+), 84 deletions(-) diff --git a/guide/src/class/numeric.md b/guide/src/class/numeric.md index 20a1a041450..541f8fb5893 100644 --- a/guide/src/class/numeric.md +++ b/guide/src/class/numeric.md @@ -171,7 +171,7 @@ impl Number { } fn __complex__<'py>(&self, py: Python<'py>) -> Bound<'py, PyComplex> { - PyComplex::from_doubles_bound(py, self.0 as f64, 0.0) + PyComplex::from_doubles(py, self.0 as f64, 0.0) } } ``` @@ -321,7 +321,7 @@ impl Number { } fn __complex__<'py>(&self, py: Python<'py>) -> Bound<'py, PyComplex> { - PyComplex::from_doubles_bound(py, self.0 as f64, 0.0) + PyComplex::from_doubles(py, self.0 as f64, 0.0) } } diff --git a/src/conversions/hashbrown.rs b/src/conversions/hashbrown.rs index 12276748fa3..753b52a965c 100644 --- a/src/conversions/hashbrown.rs +++ b/src/conversions/hashbrown.rs @@ -185,7 +185,7 @@ mod tests { let hash_set: hashbrown::HashSet = set.extract().unwrap(); assert_eq!(hash_set, [1, 2, 3, 4, 5].iter().copied().collect()); - let set = PyFrozenSet::new_bound(py, &[1, 2, 3, 4, 5]).unwrap(); + let set = PyFrozenSet::new(py, &[1, 2, 3, 4, 5]).unwrap(); let hash_set: hashbrown::HashSet = set.extract().unwrap(); assert_eq!(hash_set, [1, 2, 3, 4, 5].iter().copied().collect()); }); diff --git a/src/conversions/num_complex.rs b/src/conversions/num_complex.rs index 172d1efd505..caae94fb9e0 100644 --- a/src/conversions/num_complex.rs +++ b/src/conversions/num_complex.rs @@ -59,10 +59,10 @@ //! # //! # module.add_function(&wrap_pyfunction!(get_eigenvalues, module)?)?; //! # -//! # let m11 = PyComplex::from_doubles_bound(py, 0_f64, -1_f64); -//! # let m12 = PyComplex::from_doubles_bound(py, 1_f64, 0_f64); -//! # let m21 = PyComplex::from_doubles_bound(py, 2_f64, -1_f64); -//! # let m22 = PyComplex::from_doubles_bound(py, -1_f64, 0_f64); +//! # let m11 = PyComplex::from_doubles(py, 0_f64, -1_f64); +//! # let m12 = PyComplex::from_doubles(py, 1_f64, 0_f64); +//! # let m21 = PyComplex::from_doubles(py, 2_f64, -1_f64); +//! # let m22 = PyComplex::from_doubles(py, -1_f64, 0_f64); //! # //! # let result = module //! # .getattr("get_eigenvalues")? diff --git a/src/conversions/std/set.rs b/src/conversions/std/set.rs index c955801a916..e98a10e5fad 100644 --- a/src/conversions/std/set.rs +++ b/src/conversions/std/set.rs @@ -127,7 +127,7 @@ mod tests { let hash_set: HashSet = set.extract().unwrap(); assert_eq!(hash_set, [1, 2, 3, 4, 5].iter().copied().collect()); - let set = PyFrozenSet::new_bound(py, &[1, 2, 3, 4, 5]).unwrap(); + let set = PyFrozenSet::new(py, &[1, 2, 3, 4, 5]).unwrap(); let hash_set: HashSet = set.extract().unwrap(); assert_eq!(hash_set, [1, 2, 3, 4, 5].iter().copied().collect()); }); @@ -140,7 +140,7 @@ mod tests { let hash_set: BTreeSet = set.extract().unwrap(); assert_eq!(hash_set, [1, 2, 3, 4, 5].iter().copied().collect()); - let set = PyFrozenSet::new_bound(py, &[1, 2, 3, 4, 5]).unwrap(); + let set = PyFrozenSet::new(py, &[1, 2, 3, 4, 5]).unwrap(); let hash_set: BTreeSet = set.extract().unwrap(); assert_eq!(hash_set, [1, 2, 3, 4, 5].iter().copied().collect()); }); diff --git a/src/marker.rs b/src/marker.rs index baf8d07f026..67a7381974f 100644 --- a/src/marker.rs +++ b/src/marker.rs @@ -677,7 +677,7 @@ impl<'py> Python<'py> { #[allow(non_snake_case)] // the Python keyword starts with uppercase #[inline] pub fn Ellipsis(self) -> PyObject { - PyEllipsis::get_bound(self).into_py(self) + PyEllipsis::get(self).into_py(self) } /// Gets the Python builtin value `NotImplemented`. diff --git a/src/pybacked.rs b/src/pybacked.rs index 1d93042f039..f9249978b5f 100644 --- a/src/pybacked.rs +++ b/src/pybacked.rs @@ -382,7 +382,7 @@ mod test { #[test] fn py_backed_bytes_from_bytearray() { Python::with_gil(|py| { - let b = PyByteArray::new_bound(py, b"abcde"); + let b = PyByteArray::new(py, b"abcde"); let py_backed_bytes = PyBackedBytes::from(b); assert_eq!(&*py_backed_bytes, b"abcde"); }); @@ -401,7 +401,7 @@ mod test { #[test] fn rust_backed_bytes_into_py() { Python::with_gil(|py| { - let orig_bytes = PyByteArray::new_bound(py, b"abcde"); + let orig_bytes = PyByteArray::new(py, b"abcde"); let rust_backed_bytes = PyBackedBytes::from(orig_bytes); assert!(matches!( rust_backed_bytes.storage, @@ -508,7 +508,7 @@ mod test { #[test] fn test_backed_bytes_from_bytearray_clone() { Python::with_gil(|py| { - let b1: PyBackedBytes = PyByteArray::new_bound(py, b"abcde").into(); + let b1: PyBackedBytes = PyByteArray::new(py, b"abcde").into(); let b2 = b1.clone(); assert_eq!(b1, b2); @@ -521,7 +521,7 @@ mod test { fn test_backed_bytes_eq() { Python::with_gil(|py| { let b1: PyBackedBytes = PyBytes::new(py, b"abcde").into(); - let b2: PyBackedBytes = PyByteArray::new_bound(py, b"abcde").into(); + let b2: PyBackedBytes = PyByteArray::new(py, b"abcde").into(); assert_eq!(b1, b"abcde"); assert_eq!(b1, b2); @@ -548,7 +548,7 @@ mod test { hasher.finish() }; - let b2: PyBackedBytes = PyByteArray::new_bound(py, b"abcde").into(); + let b2: PyBackedBytes = PyByteArray::new(py, b"abcde").into(); let h2 = { let mut hasher = DefaultHasher::new(); b2.hash(&mut hasher); diff --git a/src/tests/hygiene/pymethods.rs b/src/tests/hygiene/pymethods.rs index af167db21c6..6a1a2a50d13 100644 --- a/src/tests/hygiene/pymethods.rs +++ b/src/tests/hygiene/pymethods.rs @@ -291,7 +291,7 @@ impl Dummy { &self, py: crate::Python<'py>, ) -> crate::Bound<'py, crate::types::PyComplex> { - crate::types::PyComplex::from_doubles_bound(py, 0.0, 0.0) + crate::types::PyComplex::from_doubles(py, 0.0, 0.0) } fn __int__(&self) -> u32 { diff --git a/src/types/any.rs b/src/types/any.rs index 84bb1182eda..165816c13fd 100644 --- a/src/types/any.rs +++ b/src/types/any.rs @@ -176,8 +176,8 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed { /// /// # fn main() -> PyResult<()> { /// Python::with_gil(|py| -> PyResult<()> { - /// let a = PyFloat::new_bound(py, 0_f64); - /// let b = PyFloat::new_bound(py, 42_f64); + /// let a = PyFloat::new(py, 0_f64); + /// let b = PyFloat::new(py, 42_f64); /// assert_eq!(a.compare(b)?, Ordering::Less); /// Ok(()) /// })?; @@ -192,7 +192,7 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed { /// /// # fn main() -> PyResult<()> { /// Python::with_gil(|py| -> PyResult<()> { - /// let a = PyFloat::new_bound(py, 0_f64); + /// let a = PyFloat::new(py, 0_f64); /// let b = PyString::new_bound(py, "zero"); /// assert!(a.compare(b).is_err()); /// Ok(()) diff --git a/src/types/bytearray.rs b/src/types/bytearray.rs index ec1aa29f841..d29d67e7c3e 100644 --- a/src/types/bytearray.rs +++ b/src/types/bytearray.rs @@ -22,7 +22,7 @@ impl PyByteArray { /// Creates a new Python bytearray object. /// /// The byte string is initialized by copying the data from the `&[u8]`. - pub fn new_bound<'py>(py: Python<'py>, src: &[u8]) -> Bound<'py, PyByteArray> { + pub fn new<'py>(py: Python<'py>, src: &[u8]) -> Bound<'py, PyByteArray> { let ptr = src.as_ptr().cast(); let len = src.len() as ffi::Py_ssize_t; unsafe { @@ -32,6 +32,13 @@ impl PyByteArray { } } + /// Deprecated name for [`PyByteArray::new`]. + #[deprecated(since = "0.23.0", note = "renamed to `PyByteArray::new`")] + #[inline] + pub fn new_bound<'py>(py: Python<'py>, src: &[u8]) -> Bound<'py, PyByteArray> { + Self::new(py, src) + } + /// Creates a new Python `bytearray` object with an `init` closure to write its contents. /// Before calling `init` the bytearray is zero-initialised. /// * If Python raises a MemoryError on the allocation, `new_with` will return @@ -46,7 +53,7 @@ impl PyByteArray { /// /// # fn main() -> PyResult<()> { /// Python::with_gil(|py| -> PyResult<()> { - /// let py_bytearray = PyByteArray::new_bound_with(py, 10, |bytes: &mut [u8]| { + /// let py_bytearray = PyByteArray::new_with(py, 10, |bytes: &mut [u8]| { /// bytes.copy_from_slice(b"Hello Rust"); /// Ok(()) /// })?; @@ -56,11 +63,7 @@ impl PyByteArray { /// }) /// # } /// ``` - pub fn new_bound_with( - py: Python<'_>, - len: usize, - init: F, - ) -> PyResult> + pub fn new_with(py: Python<'_>, len: usize, init: F) -> PyResult> where F: FnOnce(&mut [u8]) -> PyResult<()>, { @@ -81,15 +84,36 @@ impl PyByteArray { } } + /// Deprecated name for [`PyByteArray::new_with`]. + #[deprecated(since = "0.23.0", note = "renamed to `PyByteArray::new_with`")] + #[inline] + pub fn new_bound_with( + py: Python<'_>, + len: usize, + init: F, + ) -> PyResult> + where + F: FnOnce(&mut [u8]) -> PyResult<()>, + { + Self::new_with(py, len, init) + } + /// Creates a new Python `bytearray` object from another Python object that /// implements the buffer protocol. - pub fn from_bound<'py>(src: &Bound<'py, PyAny>) -> PyResult> { + pub fn from<'py>(src: &Bound<'py, PyAny>) -> PyResult> { unsafe { ffi::PyByteArray_FromObject(src.as_ptr()) .assume_owned_or_err(src.py()) .downcast_into_unchecked() } } + + ///Deprecated name for [`PyByteArray::from`]. + #[deprecated(since = "0.23.0", note = "renamed to `PyByteArray::from`")] + #[inline] + pub fn from_bound<'py>(src: &Bound<'py, PyAny>) -> PyResult> { + Self::from(src) + } } /// Implementation of functionality for [`PyByteArray`]. @@ -227,7 +251,7 @@ pub trait PyByteArrayMethods<'py>: crate::sealed::Sealed { /// # use pyo3::prelude::*; /// # use pyo3::types::PyByteArray; /// # Python::with_gil(|py| { - /// let bytearray = PyByteArray::new_bound(py, b"Hello World."); + /// let bytearray = PyByteArray::new(py, b"Hello World."); /// let mut copied_message = bytearray.to_vec(); /// assert_eq!(b"Hello World.", copied_message.as_slice()); /// @@ -308,7 +332,7 @@ impl<'py> TryFrom<&Bound<'py, PyAny>> for Bound<'py, PyByteArray> { /// Creates a new Python `bytearray` object from another Python object that /// implements the buffer protocol. fn try_from(value: &Bound<'py, PyAny>) -> Result { - PyByteArray::from_bound(value) + PyByteArray::from(value) } } @@ -321,7 +345,7 @@ mod tests { fn test_len() { Python::with_gil(|py| { let src = b"Hello Python"; - let bytearray = PyByteArray::new_bound(py, src); + let bytearray = PyByteArray::new(py, src); assert_eq!(src.len(), bytearray.len()); }); } @@ -330,7 +354,7 @@ mod tests { fn test_as_bytes() { Python::with_gil(|py| { let src = b"Hello Python"; - let bytearray = PyByteArray::new_bound(py, src); + let bytearray = PyByteArray::new(py, src); let slice = unsafe { bytearray.as_bytes() }; assert_eq!(src, slice); @@ -342,7 +366,7 @@ mod tests { fn test_as_bytes_mut() { Python::with_gil(|py| { let src = b"Hello Python"; - let bytearray = PyByteArray::new_bound(py, src); + let bytearray = PyByteArray::new(py, src); let slice = unsafe { bytearray.as_bytes_mut() }; assert_eq!(src, slice); @@ -358,7 +382,7 @@ mod tests { fn test_to_vec() { Python::with_gil(|py| { let src = b"Hello Python"; - let bytearray = PyByteArray::new_bound(py, src); + let bytearray = PyByteArray::new(py, src); let vec = bytearray.to_vec(); assert_eq!(src, vec.as_slice()); @@ -369,10 +393,10 @@ mod tests { fn test_from() { Python::with_gil(|py| { let src = b"Hello Python"; - let bytearray = PyByteArray::new_bound(py, src); + let bytearray = PyByteArray::new(py, src); let ba: PyObject = bytearray.into(); - let bytearray = PyByteArray::from_bound(ba.bind(py)).unwrap(); + let bytearray = PyByteArray::from(ba.bind(py)).unwrap(); assert_eq!(src, unsafe { bytearray.as_bytes() }); }); @@ -381,7 +405,7 @@ mod tests { #[test] fn test_from_err() { Python::with_gil(|py| { - if let Err(err) = PyByteArray::from_bound(py.None().bind(py)) { + if let Err(err) = PyByteArray::from(py.None().bind(py)) { assert!(err.is_instance_of::(py)); } else { panic!("error"); @@ -393,7 +417,7 @@ mod tests { fn test_try_from() { Python::with_gil(|py| { let src = b"Hello Python"; - let bytearray: &Bound<'_, PyAny> = &PyByteArray::new_bound(py, src); + let bytearray: &Bound<'_, PyAny> = &PyByteArray::new(py, src); let bytearray: Bound<'_, PyByteArray> = TryInto::try_into(bytearray).unwrap(); assert_eq!(src, unsafe { bytearray.as_bytes() }); @@ -404,7 +428,7 @@ mod tests { fn test_resize() { Python::with_gil(|py| { let src = b"Hello Python"; - let bytearray = PyByteArray::new_bound(py, src); + let bytearray = PyByteArray::new(py, src); bytearray.resize(20).unwrap(); assert_eq!(20, bytearray.len()); @@ -414,7 +438,7 @@ mod tests { #[test] fn test_byte_array_new_with() -> super::PyResult<()> { Python::with_gil(|py| -> super::PyResult<()> { - let py_bytearray = PyByteArray::new_bound_with(py, 10, |b: &mut [u8]| { + let py_bytearray = PyByteArray::new_with(py, 10, |b: &mut [u8]| { b.copy_from_slice(b"Hello Rust"); Ok(()) })?; @@ -427,7 +451,7 @@ mod tests { #[test] fn test_byte_array_new_with_zero_initialised() -> super::PyResult<()> { Python::with_gil(|py| -> super::PyResult<()> { - let py_bytearray = PyByteArray::new_bound_with(py, 10, |_b: &mut [u8]| Ok(()))?; + let py_bytearray = PyByteArray::new_with(py, 10, |_b: &mut [u8]| Ok(()))?; let bytearray: &[u8] = unsafe { py_bytearray.as_bytes() }; assert_eq!(bytearray, &[0; 10]); Ok(()) @@ -438,7 +462,7 @@ mod tests { fn test_byte_array_new_with_error() { use crate::exceptions::PyValueError; Python::with_gil(|py| { - let py_bytearray_result = PyByteArray::new_bound_with(py, 10, |_b: &mut [u8]| { + let py_bytearray_result = PyByteArray::new_with(py, 10, |_b: &mut [u8]| { Err(PyValueError::new_err("Hello Crustaceans!")) }); assert!(py_bytearray_result.is_err()); diff --git a/src/types/complex.rs b/src/types/complex.rs index 4276f8424ad..131bcc09347 100644 --- a/src/types/complex.rs +++ b/src/types/complex.rs @@ -28,11 +28,7 @@ pyobject_native_type!( impl PyComplex { /// Creates a new `PyComplex` from the given real and imaginary values. - pub fn from_doubles_bound( - py: Python<'_>, - real: c_double, - imag: c_double, - ) -> Bound<'_, PyComplex> { + pub fn from_doubles(py: Python<'_>, real: c_double, imag: c_double) -> Bound<'_, PyComplex> { use crate::ffi_ptr_ext::FfiPtrExt; unsafe { ffi::PyComplex_FromDoubles(real, imag) @@ -40,6 +36,17 @@ impl PyComplex { .downcast_into_unchecked() } } + + /// Deprecated name for [`PyComplex::from_doubles`]. + #[deprecated(since = "0.23.0", note = "renamed to `PyComplex::from_doubles`")] + #[inline] + pub fn from_doubles_bound( + py: Python<'_>, + real: c_double, + imag: c_double, + ) -> Bound<'_, PyComplex> { + Self::from_doubles(py, real, imag) + } } #[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))] @@ -130,8 +137,8 @@ mod not_limited_impls { #[test] fn test_add() { Python::with_gil(|py| { - let l = PyComplex::from_doubles_bound(py, 3.0, 1.2); - let r = PyComplex::from_doubles_bound(py, 1.0, 2.6); + let l = PyComplex::from_doubles(py, 3.0, 1.2); + let r = PyComplex::from_doubles(py, 1.0, 2.6); let res = l + r; assert_approx_eq!(res.real(), 4.0); assert_approx_eq!(res.imag(), 3.8); @@ -141,8 +148,8 @@ mod not_limited_impls { #[test] fn test_sub() { Python::with_gil(|py| { - let l = PyComplex::from_doubles_bound(py, 3.0, 1.2); - let r = PyComplex::from_doubles_bound(py, 1.0, 2.6); + let l = PyComplex::from_doubles(py, 3.0, 1.2); + let r = PyComplex::from_doubles(py, 1.0, 2.6); let res = l - r; assert_approx_eq!(res.real(), 2.0); assert_approx_eq!(res.imag(), -1.4); @@ -152,8 +159,8 @@ mod not_limited_impls { #[test] fn test_mul() { Python::with_gil(|py| { - let l = PyComplex::from_doubles_bound(py, 3.0, 1.2); - let r = PyComplex::from_doubles_bound(py, 1.0, 2.6); + let l = PyComplex::from_doubles(py, 3.0, 1.2); + let r = PyComplex::from_doubles(py, 1.0, 2.6); let res = l * r; assert_approx_eq!(res.real(), -0.12); assert_approx_eq!(res.imag(), 9.0); @@ -163,8 +170,8 @@ mod not_limited_impls { #[test] fn test_div() { Python::with_gil(|py| { - let l = PyComplex::from_doubles_bound(py, 3.0, 1.2); - let r = PyComplex::from_doubles_bound(py, 1.0, 2.6); + let l = PyComplex::from_doubles(py, 3.0, 1.2); + let r = PyComplex::from_doubles(py, 1.0, 2.6); let res = l / r; assert_approx_eq!(res.real(), 0.788_659_793_814_432_9); assert_approx_eq!(res.imag(), -0.850_515_463_917_525_7); @@ -174,7 +181,7 @@ mod not_limited_impls { #[test] fn test_neg() { Python::with_gil(|py| { - let val = PyComplex::from_doubles_bound(py, 3.0, 1.2); + let val = PyComplex::from_doubles(py, 3.0, 1.2); let res = -val; assert_approx_eq!(res.real(), -3.0); assert_approx_eq!(res.imag(), -1.2); @@ -184,7 +191,7 @@ mod not_limited_impls { #[test] fn test_abs() { Python::with_gil(|py| { - let val = PyComplex::from_doubles_bound(py, 3.0, 1.2); + let val = PyComplex::from_doubles(py, 3.0, 1.2); assert_approx_eq!(val.abs(), 3.231_098_884_280_702_2); }); } @@ -192,8 +199,8 @@ mod not_limited_impls { #[test] fn test_pow() { Python::with_gil(|py| { - let l = PyComplex::from_doubles_bound(py, 3.0, 1.2); - let r = PyComplex::from_doubles_bound(py, 1.2, 2.6); + let l = PyComplex::from_doubles(py, 3.0, 1.2); + let r = PyComplex::from_doubles(py, 1.2, 2.6); let val = l.pow(&r); assert_approx_eq!(val.real(), -1.419_309_997_016_603_7); assert_approx_eq!(val.imag(), -0.541_297_466_033_544_6); @@ -260,7 +267,7 @@ mod tests { use assert_approx_eq::assert_approx_eq; Python::with_gil(|py| { - let complex = PyComplex::from_doubles_bound(py, 3.0, 1.2); + let complex = PyComplex::from_doubles(py, 3.0, 1.2); assert_approx_eq!(complex.real(), 3.0); assert_approx_eq!(complex.imag(), 1.2); }); diff --git a/src/types/ellipsis.rs b/src/types/ellipsis.rs index c14839eb253..4507ff6253b 100644 --- a/src/types/ellipsis.rs +++ b/src/types/ellipsis.rs @@ -15,9 +15,16 @@ pyobject_native_type_named!(PyEllipsis); impl PyEllipsis { /// Returns the `Ellipsis` object. #[inline] - pub fn get_bound(py: Python<'_>) -> Borrowed<'_, '_, PyEllipsis> { + pub fn get(py: Python<'_>) -> Borrowed<'_, '_, PyEllipsis> { unsafe { ffi::Py_Ellipsis().assume_borrowed(py).downcast_unchecked() } } + + /// Deprecated name for [`PyEllipsis::get`]. + #[deprecated(since = "0.23.0", note = "renamed to `PyEllipsis::get`")] + #[inline] + pub fn get_bound(py: Python<'_>) -> Borrowed<'_, '_, PyEllipsis> { + Self::get(py) + } } unsafe impl PyTypeInfo for PyEllipsis { @@ -37,7 +44,7 @@ unsafe impl PyTypeInfo for PyEllipsis { #[inline] fn is_exact_type_of_bound(object: &Bound<'_, PyAny>) -> bool { - object.is(&**Self::get_bound(object.py())) + object.is(&**Self::get(object.py())) } } @@ -50,15 +57,15 @@ mod tests { #[test] fn test_ellipsis_is_itself() { Python::with_gil(|py| { - assert!(PyEllipsis::get_bound(py).is_instance_of::()); - assert!(PyEllipsis::get_bound(py).is_exact_instance_of::()); + assert!(PyEllipsis::get(py).is_instance_of::()); + assert!(PyEllipsis::get(py).is_exact_instance_of::()); }) } #[test] fn test_ellipsis_type_object_consistent() { Python::with_gil(|py| { - assert!(PyEllipsis::get_bound(py) + assert!(PyEllipsis::get(py) .get_type() .is(&PyEllipsis::type_object_bound(py))); }) diff --git a/src/types/float.rs b/src/types/float.rs index b1992d3983a..70c5e1cd9e3 100644 --- a/src/types/float.rs +++ b/src/types/float.rs @@ -30,13 +30,20 @@ pyobject_native_type!( impl PyFloat { /// Creates a new Python `float` object. - pub fn new_bound(py: Python<'_>, val: c_double) -> Bound<'_, PyFloat> { + pub fn new(py: Python<'_>, val: c_double) -> Bound<'_, PyFloat> { unsafe { ffi::PyFloat_FromDouble(val) .assume_owned(py) .downcast_into_unchecked() } } + + /// Deprecated name for [`PyFloat::new`]. + #[deprecated(since = "0.23.0", note = "renamed to `PyFloat::new`")] + #[inline] + pub fn new_bound(py: Python<'_>, val: c_double) -> Bound<'_, PyFloat> { + Self::new(py, val) + } } /// Implementation of functionality for [`PyFloat`]. @@ -67,13 +74,13 @@ impl<'py> PyFloatMethods<'py> for Bound<'py, PyFloat> { impl ToPyObject for f64 { fn to_object(&self, py: Python<'_>) -> PyObject { - PyFloat::new_bound(py, *self).into() + PyFloat::new(py, *self).into() } } impl IntoPy for f64 { fn into_py(self, py: Python<'_>) -> PyObject { - PyFloat::new_bound(py, self).into() + PyFloat::new(py, self).into() } #[cfg(feature = "experimental-inspect")] @@ -114,13 +121,13 @@ impl<'py> FromPyObject<'py> for f64 { impl ToPyObject for f32 { fn to_object(&self, py: Python<'_>) -> PyObject { - PyFloat::new_bound(py, f64::from(*self)).into() + PyFloat::new(py, f64::from(*self)).into() } } impl IntoPy for f32 { fn into_py(self, py: Python<'_>) -> PyObject { - PyFloat::new_bound(py, f64::from(self)).into() + PyFloat::new(py, f64::from(self)).into() } #[cfg(feature = "experimental-inspect")] @@ -250,7 +257,7 @@ mod tests { Python::with_gil(|py| { let v = 1.23f64; - let obj = PyFloat::new_bound(py, 1.23); + let obj = PyFloat::new(py, 1.23); assert_approx_eq!(v, obj.value()); }); } @@ -259,7 +266,7 @@ mod tests { fn test_pyfloat_comparisons() { Python::with_gil(|py| { let f_64 = 1.01f64; - let py_f64 = PyFloat::new_bound(py, 1.01); + let py_f64 = PyFloat::new(py, 1.01); let py_f64_ref = &py_f64; let py_f64_borrowed = py_f64.as_borrowed(); @@ -288,7 +295,7 @@ mod tests { assert_eq!(&f_64, py_f64_borrowed); let f_32 = 2.02f32; - let py_f32 = PyFloat::new_bound(py, 2.02); + let py_f32 = PyFloat::new(py, 2.02); let py_f32_ref = &py_f32; let py_f32_borrowed = py_f32.as_borrowed(); diff --git a/src/types/frozenset.rs b/src/types/frozenset.rs index 1e8cf947c39..b25f0173e4d 100644 --- a/src/types/frozenset.rs +++ b/src/types/frozenset.rs @@ -20,7 +20,7 @@ impl<'py> PyFrozenSetBuilder<'py> { /// panic when running out of memory. pub fn new(py: Python<'py>) -> PyResult> { Ok(PyFrozenSetBuilder { - py_frozen_set: PyFrozenSet::empty_bound(py)?, + py_frozen_set: PyFrozenSet::empty(py)?, }) } @@ -39,9 +39,16 @@ impl<'py> PyFrozenSetBuilder<'py> { } /// Finish building the set and take ownership of its current value - pub fn finalize_bound(self) -> Bound<'py, PyFrozenSet> { + pub fn finalize(self) -> Bound<'py, PyFrozenSet> { self.py_frozen_set } + + /// Deprecated name for [`PyFrozenSetBuilder::finalize`]. + #[deprecated(since = "0.23.0", note = "renamed to `PyFrozenSetBuilder::finalize`")] + #[inline] + pub fn finalize_bound(self) -> Bound<'py, PyFrozenSet> { + self.finalize() + } } /// Represents a Python `frozenset`. @@ -74,21 +81,38 @@ impl PyFrozenSet { /// /// May panic when running out of memory. #[inline] - pub fn new_bound<'a, 'p, T: ToPyObject + 'a>( + pub fn new<'a, 'p, T: ToPyObject + 'a>( py: Python<'p>, elements: impl IntoIterator, ) -> PyResult> { new_from_iter(py, elements) } + /// Deprecated name for [`PyFrozenSet::new`]. + #[deprecated(since = "0.23.0", note = "renamed to `PyFrozenSet::new`")] + #[inline] + pub fn new_bound<'a, 'p, T: ToPyObject + 'a>( + py: Python<'p>, + elements: impl IntoIterator, + ) -> PyResult> { + Self::new(py, elements) + } + /// Creates a new empty frozen set - pub fn empty_bound(py: Python<'_>) -> PyResult> { + pub fn empty(py: Python<'_>) -> PyResult> { unsafe { ffi::PyFrozenSet_New(ptr::null_mut()) .assume_owned_or_err(py) .downcast_into_unchecked() } } + + /// Deprecated name for [`PyFrozenSet::empty`]. + #[deprecated(since = "0.23.0", note = "renamed to `PyFrozenSet::empty`")] + #[inline] + pub fn empty_bound(py: Python<'_>) -> PyResult> { + Self::empty(py) + } } /// Implementation of functionality for [`PyFrozenSet`]. @@ -237,18 +261,18 @@ mod tests { #[test] fn test_frozenset_new_and_len() { Python::with_gil(|py| { - let set = PyFrozenSet::new_bound(py, &[1]).unwrap(); + let set = PyFrozenSet::new(py, &[1]).unwrap(); assert_eq!(1, set.len()); let v = vec![1]; - assert!(PyFrozenSet::new_bound(py, &[v]).is_err()); + assert!(PyFrozenSet::new(py, &[v]).is_err()); }); } #[test] fn test_frozenset_empty() { Python::with_gil(|py| { - let set = PyFrozenSet::empty_bound(py).unwrap(); + let set = PyFrozenSet::empty(py).unwrap(); assert_eq!(0, set.len()); assert!(set.is_empty()); }); @@ -257,7 +281,7 @@ mod tests { #[test] fn test_frozenset_contains() { Python::with_gil(|py| { - let set = PyFrozenSet::new_bound(py, &[1]).unwrap(); + let set = PyFrozenSet::new(py, &[1]).unwrap(); assert!(set.contains(1).unwrap()); }); } @@ -265,7 +289,7 @@ mod tests { #[test] fn test_frozenset_iter() { Python::with_gil(|py| { - let set = PyFrozenSet::new_bound(py, &[1]).unwrap(); + let set = PyFrozenSet::new(py, &[1]).unwrap(); for el in set { assert_eq!(1i32, el.extract::().unwrap()); @@ -276,7 +300,7 @@ mod tests { #[test] fn test_frozenset_iter_bound() { Python::with_gil(|py| { - let set = PyFrozenSet::new_bound(py, &[1]).unwrap(); + let set = PyFrozenSet::new(py, &[1]).unwrap(); for el in &set { assert_eq!(1i32, el.extract::().unwrap()); @@ -287,7 +311,7 @@ mod tests { #[test] fn test_frozenset_iter_size_hint() { Python::with_gil(|py| { - let set = PyFrozenSet::new_bound(py, &[1]).unwrap(); + let set = PyFrozenSet::new(py, &[1]).unwrap(); let mut iter = set.iter(); // Exact size @@ -312,7 +336,7 @@ mod tests { builder.add(2).unwrap(); // finalize it - let set = builder.finalize_bound(); + let set = builder.finalize(); assert!(set.contains(1).unwrap()); assert!(set.contains(2).unwrap()); From 1549bbc2b96cb2e89bc14d917c9e7526d1186914 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Fri, 2 Aug 2024 10:16:44 +0100 Subject: [PATCH 191/495] docs: add some recent videos to the README (#4392) --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 066de85fa8b..631918c466a 100644 --- a/README.md +++ b/README.md @@ -224,6 +224,10 @@ about this topic. ## Articles and other media +- [(Video) PyO3: From Python to Rust and Back Again](https://www.youtube.com/watch?v=UmL_CA-v3O8) - Jul 3, 2024 +- [Parsing Python ASTs 20x Faster with Rust](https://www.gauge.sh/blog/parsing-python-asts-20x-faster-with-rust) - Jun 17, 2024 +- [(Video) How Python Harnesses Rust through PyO3](https://www.youtube.com/watch?v=UkZ_m3Wj2hA) - May 18, 2024 +- [(Video) Combining Rust and Python: The Best of Both Worlds?](https://www.youtube.com/watch?v=lyG6AKzu4ew) - Mar 1, 2024 - [(Video) Extending Python with Rust using PyO3](https://www.youtube.com/watch?v=T45ZEmSR1-s) - Dec 16, 2023 - [A Week of PyO3 + rust-numpy (How to Speed Up Your Data Pipeline X Times)](https://terencezl.github.io/blog/2023/06/06/a-week-of-pyo3-rust-numpy/) - Jun 6, 2023 - [(Podcast) PyO3 with David Hewitt](https://rustacean-station.org/episode/david-hewitt/) - May 19, 2023 From 0ba244cce44c05b063850f686f317d4981616451 Mon Sep 17 00:00:00 2001 From: deedy5 <65482418+deedy5@users.noreply.github.com> Date: Fri, 2 Aug 2024 12:17:47 +0300 Subject: [PATCH 192/495] docs: Update README.md - 1) rename `pyreqwest_impersonate` to `primp`, 2) add `html2text_rs` (#4405) * README: rename `pyreqwest_impersonate` to `primp` * README: add `html2text-rs` to examples --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 631918c466a..908f184cc6b 100644 --- a/README.md +++ b/README.md @@ -196,6 +196,7 @@ about this topic. - [granian](https://github.com/emmett-framework/granian) _A Rust HTTP server for Python applications._ - [greptimedb](https://github.com/GreptimeTeam/greptimedb/tree/main/src/script) _Support [Python scripting](https://docs.greptime.com/user-guide/python-scripts/overview) in the database_ - [haem](https://github.com/BooleanCat/haem) _A Python library for working on Bioinformatics problems._ +- [html2text-rs](https://github.com/deedy5/html2text_rs) _Python library for converting HTML to markup or plain text._ - [html-py-ever](https://github.com/PyO3/setuptools-rust/tree/main/examples/html-py-ever) _Using [html5ever](https://github.com/servo/html5ever) through [kuchiki](https://github.com/kuchiki-rs/kuchiki) to speed up html parsing and css-selecting._ - [hyperjson](https://github.com/mre/hyperjson) _A hyper-fast Python module for reading/writing JSON data using Rust's serde-json._ - [inline-python](https://github.com/fusion-engineering/inline-python) _Inline Python code directly in your Rust code._ @@ -212,7 +213,7 @@ about this topic. - [pyheck](https://github.com/kevinheavey/pyheck) _Fast case conversion library, built by wrapping [heck](https://github.com/withoutboats/heck)._ - Quite easy to follow as there's not much code. - [pyre](https://github.com/Project-Dream-Weaver/pyre-http) _Fast Python HTTP server written in Rust._ -- [pyreqwest_impersonate](https://github.com/deedy5/pyreqwest_impersonate) _The fastest python HTTP client that can impersonate web browsers by mimicking their headers and TLS/JA3/JA4/HTTP2 fingerprints._ +- [primp](https://github.com/deedy5/primp) _The fastest python HTTP client that can impersonate web browsers by mimicking their headers and TLS/JA3/JA4/HTTP2 fingerprints._ - [ril-py](https://github.com/Cryptex-github/ril-py) _A performant and high-level image processing library for Python written in Rust._ - [river](https://github.com/online-ml/river) _Online machine learning in python, the computationally heavy statistics algorithms are implemented in Rust._ - [rust-python-coverage](https://github.com/cjermain/rust-python-coverage) _Example PyO3 project with automated test coverage for Rust and Python._ From 0e03b39caa18d016a6951307b297ca7e6f999e24 Mon Sep 17 00:00:00 2001 From: Lily Foote Date: Fri, 2 Aug 2024 12:14:38 +0200 Subject: [PATCH 193/495] Add missing #[allow(unsafe_code)] attributes (#4396) Fixes #4394. --- newsfragments/4396.fixed.md | 1 + pyo3-macros-backend/src/pyclass.rs | 1 + pyo3-macros-backend/src/pymethod.rs | 2 ++ src/impl_/pyclass.rs | 6 +++++- src/macros.rs | 1 + src/tests/hygiene/mod.rs | 1 + src/types/mod.rs | 2 ++ 7 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 newsfragments/4396.fixed.md diff --git a/newsfragments/4396.fixed.md b/newsfragments/4396.fixed.md new file mode 100644 index 00000000000..285358ad526 --- /dev/null +++ b/newsfragments/4396.fixed.md @@ -0,0 +1 @@ +Hide confusing warnings about unsafe usage in `#[pyclass]` implementation. diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index 3b61770a870..48c86ac53e0 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -1744,6 +1744,7 @@ fn impl_pytypeinfo(cls: &syn::Ident, attr: &PyClassArgs, ctx: &Ctx) -> TokenStre }; quote! { + #[allow(unsafe_code)] unsafe impl #pyo3_path::type_object::PyTypeInfo for #cls { const NAME: &'static str = #cls_name; const MODULE: ::std::option::Option<&'static str> = #module; diff --git a/pyo3-macros-backend/src/pymethod.rs b/pyo3-macros-backend/src/pymethod.rs index 77cc9ed5cc6..584dd8f04dd 100644 --- a/pyo3-macros-backend/src/pymethod.rs +++ b/pyo3-macros-backend/src/pymethod.rs @@ -772,6 +772,7 @@ pub fn impl_py_getter_def( use #pyo3_path::impl_::pyclass::Probe; struct Offset; + #[allow(unsafe_code)] unsafe impl #pyo3_path::impl_::pyclass::OffsetCalculator<#cls, #ty> for Offset { fn offset() -> usize { #pyo3_path::impl_::pyclass::class_offset::<#cls>() + @@ -779,6 +780,7 @@ pub fn impl_py_getter_def( } } + #[allow(unsafe_code)] const GENERATOR: #pyo3_path::impl_::pyclass::PyClassGetterGenerator::< #cls, #ty, diff --git a/src/impl_/pyclass.rs b/src/impl_/pyclass.rs index 19242fce2ac..fdc7c2edd25 100644 --- a/src/impl_/pyclass.rs +++ b/src/impl_/pyclass.rs @@ -346,6 +346,7 @@ slot_fragment_trait! { #[macro_export] macro_rules! generate_pyclass_getattro_slot { ($cls:ty) => {{ + #[allow(unsafe_code)] unsafe extern "C" fn __wrap( _slf: *mut $crate::ffi::PyObject, attr: *mut $crate::ffi::PyObject, @@ -429,6 +430,7 @@ macro_rules! define_pyclass_setattr_slot { #[macro_export] macro_rules! $generate_macro { ($cls:ty) => {{ + #[allow(unsafe_code)] unsafe extern "C" fn __wrap( _slf: *mut $crate::ffi::PyObject, attr: *mut $crate::ffi::PyObject, @@ -545,6 +547,7 @@ macro_rules! define_pyclass_binary_operator_slot { #[macro_export] macro_rules! $generate_macro { ($cls:ty) => {{ + #[allow(unsafe_code)] unsafe extern "C" fn __wrap( _slf: *mut $crate::ffi::PyObject, _other: *mut $crate::ffi::PyObject, @@ -737,6 +740,7 @@ slot_fragment_trait! { #[macro_export] macro_rules! generate_pyclass_pow_slot { ($cls:ty) => {{ + #[allow(unsafe_code)] unsafe extern "C" fn __wrap( _slf: *mut $crate::ffi::PyObject, _other: *mut $crate::ffi::PyObject, @@ -861,7 +865,7 @@ macro_rules! generate_pyclass_richcompare_slot { ($cls:ty) => {{ #[allow(unknown_lints, non_local_definitions)] impl $cls { - #[allow(non_snake_case)] + #[allow(non_snake_case, unsafe_code)] unsafe extern "C" fn __pymethod___richcmp____( slf: *mut $crate::ffi::PyObject, other: *mut $crate::ffi::PyObject, diff --git a/src/macros.rs b/src/macros.rs index 60d146c3b22..d2aff4bcbf1 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -184,6 +184,7 @@ macro_rules! wrap_pymodule { #[macro_export] macro_rules! append_to_inittab { ($module:ident) => { + #[allow(unsafe_code)] unsafe { if $crate::ffi::Py_IsInitialized() != 0 { ::std::panic!( diff --git a/src/tests/hygiene/mod.rs b/src/tests/hygiene/mod.rs index c950e18da94..9bf89161b24 100644 --- a/src/tests/hygiene/mod.rs +++ b/src/tests/hygiene/mod.rs @@ -1,5 +1,6 @@ #![no_implicit_prelude] #![allow(dead_code, unused_variables, clippy::unnecessary_wraps)] +#![deny(unsafe_code)] // The modules in this test are used to check PyO3 macro expansion is hygienic. By locating the test // inside the crate the global `::pyo3` namespace is not available, so in combination with diff --git a/src/types/mod.rs b/src/types/mod.rs index fdf73434864..2ba64f46566 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -133,6 +133,7 @@ macro_rules! pyobject_native_type_named ( } } + #[allow(unsafe_code)] unsafe impl<$($generics,)*> $crate::AsPyPointer for $name { /// Gets the underlying FFI pointer, returns a borrowed pointer. #[inline] @@ -160,6 +161,7 @@ macro_rules! pyobject_native_static_type_object( #[macro_export] macro_rules! pyobject_native_type_info( ($name:ty, $typeobject:expr, $module:expr $(, #checkfunction=$checkfunction:path)? $(;$generics:ident)*) => { + #[allow(unsafe_code)] unsafe impl<$($generics,)*> $crate::type_object::PyTypeInfo for $name { const NAME: &'static str = stringify!($name); const MODULE: ::std::option::Option<&'static str> = $module; From 30dfa23d3d56f08283fade14ab53c24c15148e6c Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Fri, 2 Aug 2024 12:48:31 +0100 Subject: [PATCH 194/495] add back new `marshal` APIs (#4398) * add back new `marshal` APIs * update example Co-authored-by: Icxolu <10486322+Icxolu@users.noreply.github.com> --------- Co-authored-by: Icxolu <10486322+Icxolu@users.noreply.github.com> --- src/marshal.rs | 45 ++++++++++++++++++++++++++++----------------- 1 file changed, 28 insertions(+), 17 deletions(-) diff --git a/src/marshal.rs b/src/marshal.rs index 197302891e5..d541da7d695 100644 --- a/src/marshal.rs +++ b/src/marshal.rs @@ -6,7 +6,7 @@ use crate::ffi_ptr_ext::FfiPtrExt; use crate::py_result_ext::PyResultExt; use crate::types::{PyAny, PyBytes}; use crate::{ffi, Bound}; -use crate::{AsPyPointer, PyResult, Python}; +use crate::{PyResult, Python}; use std::os::raw::c_int; /// The current version of the marshal binary format. @@ -29,23 +29,32 @@ pub const VERSION: i32 = 4; /// dict.set_item("mies", "wim").unwrap(); /// dict.set_item("zus", "jet").unwrap(); /// -/// let bytes = marshal::dumps_bound(py, &dict, marshal::VERSION); +/// let bytes = marshal::dumps(&dict, marshal::VERSION); /// # }); /// ``` -pub fn dumps_bound<'py>( - py: Python<'py>, - object: &impl AsPyPointer, - version: i32, -) -> PyResult> { +pub fn dumps<'py>(object: &Bound<'py, PyAny>, version: i32) -> PyResult> { unsafe { ffi::PyMarshal_WriteObjectToString(object.as_ptr(), version as c_int) - .assume_owned_or_err(py) + .assume_owned_or_err(object.py()) .downcast_into_unchecked() } } +/// Deprecated form of [`dumps`]. +#[deprecated(since = "0.23.0", note = "use `dumps` instead")] +pub fn dumps_bound<'py>( + py: Python<'py>, + object: &impl crate::AsPyPointer, + version: i32, +) -> PyResult> { + dumps( + unsafe { object.as_ptr().assume_borrowed(py) }.as_any(), + version, + ) +} + /// Deserialize an object from bytes using the Python built-in marshal module. -pub fn loads_bound<'py, B>(py: Python<'py>, data: &B) -> PyResult> +pub fn loads<'py, B>(py: Python<'py>, data: &B) -> PyResult> where B: AsRef<[u8]> + ?Sized, { @@ -56,10 +65,16 @@ where } } +/// Deprecated form of [`loads`]. +#[deprecated(since = "0.23.0", note = "renamed to `loads`")] +pub fn loads_bound<'py>(py: Python<'py>, data: &[u8]) -> PyResult> { + loads(py, data) +} + #[cfg(test)] mod tests { use super::*; - use crate::types::{bytes::PyBytesMethods, dict::PyDictMethods, PyDict}; + use crate::types::{bytes::PyBytesMethods, dict::PyDictMethods, PyAnyMethods, PyDict}; #[test] fn marshal_roundtrip() { @@ -69,14 +84,10 @@ mod tests { dict.set_item("mies", "wim").unwrap(); dict.set_item("zus", "jet").unwrap(); - let pybytes = dumps_bound(py, &dict, VERSION).expect("marshalling failed"); - let deserialized = loads_bound(py, pybytes.as_bytes()).expect("unmarshalling failed"); + let pybytes = dumps(&dict, VERSION).expect("marshalling failed"); + let deserialized = loads(py, pybytes.as_bytes()).expect("unmarshalling failed"); - assert!(equal(py, &dict, &deserialized)); + assert!(dict.eq(&deserialized).unwrap()); }); } - - fn equal(_py: Python<'_>, a: &impl AsPyPointer, b: &impl AsPyPointer) -> bool { - unsafe { ffi::PyObject_RichCompareBool(a.as_ptr(), b.as_ptr(), ffi::Py_EQ) != 0 } - } } From 319a497e670ed3c2e4aadedd2a9a7da089e21f5c Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Fri, 2 Aug 2024 14:38:15 +0100 Subject: [PATCH 195/495] remove some unneeded `AsPyPointer` implementations (#4399) --- src/conversion.rs | 17 ----------------- src/types/any.rs | 7 ------- src/types/mod.rs | 10 ---------- 3 files changed, 34 deletions(-) diff --git a/src/conversion.rs b/src/conversion.rs index d50d8f45ae9..5fa4abb78e6 100644 --- a/src/conversion.rs +++ b/src/conversion.rs @@ -319,23 +319,6 @@ impl ToPyObject for &'_ T { } } -impl IntoPy for &'_ PyAny { - #[inline] - fn into_py(self, py: Python<'_>) -> PyObject { - unsafe { PyObject::from_borrowed_ptr(py, self.as_ptr()) } - } -} - -impl IntoPy for &'_ T -where - T: AsRef, -{ - #[inline] - fn into_py(self, py: Python<'_>) -> PyObject { - unsafe { PyObject::from_borrowed_ptr(py, self.as_ref().as_ptr()) } - } -} - impl FromPyObject<'_> for T where T: PyClass + Clone, diff --git a/src/types/any.rs b/src/types/any.rs index 165816c13fd..d6d1c29cbae 100644 --- a/src/types/any.rs +++ b/src/types/any.rs @@ -29,13 +29,6 @@ use std::os::raw::c_int; #[repr(transparent)] pub struct PyAny(UnsafeCell); -unsafe impl AsPyPointer for PyAny { - #[inline] - fn as_ptr(&self) -> *mut ffi::PyObject { - self.0.get() - } -} - #[allow(non_snake_case)] // Copied here as the macro does not accept deprecated functions. // Originally ffi::object::PyObject_Check, but this is not in the Python C API. diff --git a/src/types/mod.rs b/src/types/mod.rs index 2ba64f46566..114bb116626 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -116,7 +116,6 @@ pub trait DerefToPyAny { #[macro_export] macro_rules! pyobject_native_type_named ( ($name:ty $(;$generics:ident)*) => { - impl<$($generics,)*> ::std::convert::AsRef<$crate::PyAny> for $name { #[inline] fn as_ref(&self) -> &$crate::PyAny { @@ -133,15 +132,6 @@ macro_rules! pyobject_native_type_named ( } } - #[allow(unsafe_code)] - unsafe impl<$($generics,)*> $crate::AsPyPointer for $name { - /// Gets the underlying FFI pointer, returns a borrowed pointer. - #[inline] - fn as_ptr(&self) -> *mut $crate::ffi::PyObject { - self.0.as_ptr() - } - } - impl $crate::types::DerefToPyAny for $name {} }; ); From 3bf2f1f4b412c6d5eafb1308c682a314e88919e1 Mon Sep 17 00:00:00 2001 From: Alexander Kuhn-Regnier Date: Sat, 3 Aug 2024 11:57:03 +0100 Subject: [PATCH 196/495] Fix Typo in types.md (#4416) Fixes a typo in `types.md` regarding the use of the `Bound<'py, T>` smart pointer. --- guide/src/types.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/guide/src/types.md b/guide/src/types.md index 131cb0ed119..bbeb1dae9df 100644 --- a/guide/src/types.md +++ b/guide/src/types.md @@ -32,7 +32,7 @@ The lack of binding to the `'py` lifetime also carries drawbacks: - Almost all methods on `Py` require a `Python<'py>` token as the first argument - Other functionality, such as [`Drop`][Drop], needs to check at runtime for attachment to the Python GIL, at a small performance cost -Because of the drawbacks `Bound<'py, T>` is preferred for many of PyO3's APIs. In particular, `Bound<'py, T>` is the better for function arguments. +Because of the drawbacks `Bound<'py, T>` is preferred for many of PyO3's APIs. In particular, `Bound<'py, T>` is better for function arguments. To convert a `Py` into a `Bound<'py, T>`, the `Py::bind` and `Py::into_bound` methods are available. `Bound<'py, T>` can be converted back into `Py` using [`Bound::unbind`]. From 0a185cd77f852bfeb19dc294df12385bb382e75b Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Sat, 3 Aug 2024 16:09:09 +0200 Subject: [PATCH 197/495] Experiment: Fallible conversion trait (#4060) * add trait * add `chrono` impls * impl autoref specialization to prefer `IntoPyObject` over `IntoPy` * relax `map_into_ptr` trait bounds allow error type to be convertible into `PyErr` instead of requiring `PyErr` * fix clippy * switch to deref-specialization and specialize `()` convert `PyNone` * more conversions * assoc type * even more conversions * macro impls * fix clippy * add `Output` type allowing optimizing impls which borrow from the input * loose type restrictions on `Either` impl * move, seal and rename `AnyBound` to `BoundObject` * add newsfragments * fix diagnostic on `IntoPyObject` unimplemented * fix clippy * specialize `#[classattr]` --------- Co-authored-by: David Hewitt --- newsfragments/4060.added.md | 1 + newsfragments/4060.changed.md | 1 + pyo3-macros-backend/src/method.rs | 5 +- pyo3-macros-backend/src/pyclass.rs | 40 +++++ pyo3-macros-backend/src/pymethod.rs | 3 +- pyo3-macros-backend/src/quotes.rs | 13 +- src/conversion.rs | 93 ++++++++++- src/conversions/chrono.rs | 248 ++++++++++++++++++++++++++-- src/conversions/chrono_tz.rs | 16 +- src/conversions/either.rs | 32 +++- src/conversions/hashbrown.rs | 61 ++++++- src/conversions/indexmap.rs | 26 ++- src/conversions/num_bigint.rs | 45 ++++- src/conversions/num_complex.rs | 17 ++ src/conversions/rust_decimal.rs | 19 ++- src/conversions/smallvec.rs | 33 +++- src/conversions/std/array.rs | 38 ++++- src/conversions/std/cell.rs | 14 +- src/conversions/std/ipaddr.rs | 44 ++++- src/conversions/std/map.rs | 51 +++++- src/conversions/std/num.rs | 40 +++++ src/conversions/std/option.rs | 24 ++- src/conversions/std/osstr.rs | 58 ++++++- src/conversions/std/path.rs | 43 +++++ src/conversions/std/set.rs | 58 ++++++- src/conversions/std/slice.rs | 25 ++- src/conversions/std/string.rs | 54 +++++- src/conversions/std/time.rs | 48 ++++++ src/conversions/std/vec.rs | 27 ++- src/impl_/wrap.rs | 204 ++++++++++++++++------- src/instance.rs | 102 +++++++++++- src/lib.rs | 2 +- src/pycell.rs | 43 ++++- src/types/list.rs | 15 +- src/types/set.rs | 36 ++-- tests/ui/missing_intopy.rs | 4 +- tests/ui/missing_intopy.stderr | 37 ++++- 37 files changed, 1462 insertions(+), 158 deletions(-) create mode 100644 newsfragments/4060.added.md create mode 100644 newsfragments/4060.changed.md diff --git a/newsfragments/4060.added.md b/newsfragments/4060.added.md new file mode 100644 index 00000000000..2734df34bc9 --- /dev/null +++ b/newsfragments/4060.added.md @@ -0,0 +1 @@ +New `IntoPyObject` (fallible) conversion trait to convert from Rust to Python values. \ No newline at end of file diff --git a/newsfragments/4060.changed.md b/newsfragments/4060.changed.md new file mode 100644 index 00000000000..8d104a05cae --- /dev/null +++ b/newsfragments/4060.changed.md @@ -0,0 +1 @@ +`#[pyfunction]` and `#[pymethods]` return types will prefer `IntoPyObject` over `IntoPy` \ No newline at end of file diff --git a/pyo3-macros-backend/src/method.rs b/pyo3-macros-backend/src/method.rs index 1b8f5f5c66b..c850f67b2b9 100644 --- a/pyo3-macros-backend/src/method.rs +++ b/pyo3-macros-backend/src/method.rs @@ -695,7 +695,10 @@ impl<'a> FnSpec<'a> { #pyo3_path::intern!(py, stringify!(#python_name)), #qualname_prefix, #throw_callback, - async move { #pyo3_path::impl_::wrap::OkWrap::wrap(future.await) }, + async move { + let fut = future.await; + #pyo3_path::impl_::wrap::converter(&fut).wrap(fut) + }, ) }}; if cancel_handle.is_some() { diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index 48c86ac53e0..50c123a90fb 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -1043,10 +1043,40 @@ fn impl_complex_enum( } }; + let enum_into_pyobject_impl = { + let match_arms = variants + .iter() + .map(|variant| { + let variant_ident = variant.get_ident(); + let variant_cls = gen_complex_enum_variant_class_ident(cls, variant.get_ident()); + quote! { + #cls::#variant_ident { .. } => { + let pyclass_init = <#pyo3_path::PyClassInitializer as ::std::convert::From>::from(self).add_subclass(#variant_cls); + unsafe { #pyo3_path::Bound::new(py, pyclass_init).map(|b| #pyo3_path::types::PyAnyMethods::downcast_into_unchecked(b.into_any())) } + } + } + }); + + quote! { + impl<'py> #pyo3_path::conversion::IntoPyObject<'py> for #cls { + type Target = Self; + type Output = #pyo3_path::Bound<'py, Self::Target>; + type Error = #pyo3_path::PyErr; + + fn into_pyobject(self, py: #pyo3_path::Python<'py>) -> ::std::result::Result { + match self { + #(#match_arms)* + } + } + } + } + }; + let pyclass_impls: TokenStream = [ impl_builder.impl_pyclass(ctx), impl_builder.impl_extractext(ctx), enum_into_py_impl, + enum_into_pyobject_impl, impl_builder.impl_pyclassimpl(ctx)?, impl_builder.impl_add_to_module(ctx), impl_builder.impl_freelist(ctx), @@ -2086,6 +2116,16 @@ impl<'a> PyClassImplsBuilder<'a> { #pyo3_path::IntoPy::into_py(#pyo3_path::Py::new(py, self).unwrap(), py) } } + + impl<'py> #pyo3_path::conversion::IntoPyObject<'py> for #cls { + type Target = Self; + type Output = #pyo3_path::Bound<'py, Self::Target>; + type Error = #pyo3_path::PyErr; + + fn into_pyobject(self, py: #pyo3_path::Python<'py>) -> ::std::result::Result { + #pyo3_path::Bound::new(py, self) + } + } } } else { quote! {} diff --git a/pyo3-macros-backend/src/pymethod.rs b/pyo3-macros-backend/src/pymethod.rs index 584dd8f04dd..78d7dac2330 100644 --- a/pyo3-macros-backend/src/pymethod.rs +++ b/pyo3-macros-backend/src/pymethod.rs @@ -503,7 +503,8 @@ fn impl_py_class_attribute( let associated_method = quote! { fn #wrapper_ident(py: #pyo3_path::Python<'_>) -> #pyo3_path::PyResult<#pyo3_path::PyObject> { let function = #cls::#name; // Shadow the method name to avoid #3017 - #pyo3_path::impl_::wrap::map_result_into_py(py, #body) + let result = #body; + #pyo3_path::impl_::wrap::converter(&result).map_into_pyobject(py, result) } }; diff --git a/pyo3-macros-backend/src/quotes.rs b/pyo3-macros-backend/src/quotes.rs index fd7a59991cb..47b82605bd1 100644 --- a/pyo3-macros-backend/src/quotes.rs +++ b/pyo3-macros-backend/src/quotes.rs @@ -15,10 +15,10 @@ pub(crate) fn ok_wrap(obj: TokenStream, ctx: &Ctx) -> TokenStream { output_span, } = ctx; let pyo3_path = pyo3_path.to_tokens_spanned(*output_span); - quote_spanned! {*output_span=> - #pyo3_path::impl_::wrap::OkWrap::wrap(#obj) - .map_err(::core::convert::Into::<#pyo3_path::PyErr>::into) - } + quote_spanned! { *output_span => { + let obj = #obj; + #pyo3_path::impl_::wrap::converter(&obj).wrap(obj).map_err(::core::convert::Into::<#pyo3_path::PyErr>::into) + }} } pub(crate) fn map_result_into_ptr(result: TokenStream, ctx: &Ctx) -> TokenStream { @@ -28,5 +28,8 @@ pub(crate) fn map_result_into_ptr(result: TokenStream, ctx: &Ctx) -> TokenStream } = ctx; let pyo3_path = pyo3_path.to_tokens_spanned(*output_span); let py = syn::Ident::new("py", proc_macro2::Span::call_site()); - quote_spanned! {*output_span=> #pyo3_path::impl_::wrap::map_result_into_ptr(#py, #result) } + quote_spanned! { *output_span => { + let result = #result; + #pyo3_path::impl_::wrap::converter(&result).map_into_ptr(#py, result) + }} } diff --git a/src/conversion.rs b/src/conversion.rs index 5fa4abb78e6..bb4bc51fd07 100644 --- a/src/conversion.rs +++ b/src/conversion.rs @@ -5,7 +5,10 @@ use crate::inspect::types::TypeInfo; use crate::pyclass::boolean_struct::False; use crate::types::any::PyAnyMethods; use crate::types::PyTuple; -use crate::{ffi, Borrowed, Bound, Py, PyAny, PyClass, PyObject, PyRef, PyRefMut, Python}; +use crate::{ + ffi, Borrowed, Bound, BoundObject, Py, PyAny, PyClass, PyObject, PyRef, PyRefMut, Python, +}; +use std::convert::Infallible; /// Returns a borrowed pointer to a Python object. /// @@ -171,6 +174,84 @@ pub trait IntoPy: Sized { } } +/// Defines a conversion from a Rust type to a Python object, which may fail. +/// +/// It functions similarly to std's [`TryInto`] trait, but requires a [GIL token](Python) +/// as an argument. +#[cfg_attr( + diagnostic_namespace, + diagnostic::on_unimplemented( + message = "`{Self}` cannot be converted to a Python object", + note = "`IntoPyObject` is automatically implemented by the `#[pyclass]` macro", + note = "if you do not wish to have a corresponding Python type, implement it manually", + note = "if you do not own `{Self}` you can perform a manual conversion to one of the types in `pyo3::types::*`" + ) +)] +pub trait IntoPyObject<'py>: Sized { + /// The Python output type + type Target; + /// The smart pointer type to use. + /// + /// This will usually be [`Bound<'py, Target>`], but can special cases `&'a Bound<'py, Target>` + /// or [`Borrowed<'a, 'py, Target>`] can be used to minimize reference counting overhead. + type Output: BoundObject<'py, Self::Target>; + /// The type returned in the event of a conversion error. + type Error; + + /// Performs the conversion. + fn into_pyobject(self, py: Python<'py>) -> Result; +} + +impl<'py, T> IntoPyObject<'py> for Bound<'py, T> { + type Target = T; + type Output = Bound<'py, Self::Target>; + type Error = Infallible; + + fn into_pyobject(self, _py: Python<'py>) -> Result { + Ok(self) + } +} + +impl<'a, 'py, T> IntoPyObject<'py> for &'a Bound<'py, T> { + type Target = T; + type Output = &'a Bound<'py, Self::Target>; + type Error = Infallible; + + fn into_pyobject(self, _py: Python<'py>) -> Result { + Ok(self) + } +} + +impl<'a, 'py, T> IntoPyObject<'py> for Borrowed<'a, 'py, T> { + type Target = T; + type Output = Borrowed<'a, 'py, Self::Target>; + type Error = Infallible; + + fn into_pyobject(self, _py: Python<'py>) -> Result { + Ok(self) + } +} + +impl<'py, T> IntoPyObject<'py> for Py { + type Target = T; + type Output = Bound<'py, Self::Target>; + type Error = Infallible; + + fn into_pyobject(self, py: Python<'py>) -> Result { + Ok(self.into_bound(py)) + } +} + +impl<'a, 'py, T> IntoPyObject<'py> for &'a Py { + type Target = T; + type Output = Borrowed<'a, 'py, Self::Target>; + type Error = Infallible; + + fn into_pyobject(self, py: Python<'py>) -> Result { + Ok(self.bind_borrowed(py)) + } +} + /// Extract a type from a Python object. /// /// @@ -354,6 +435,16 @@ impl IntoPy> for () { } } +impl<'py> IntoPyObject<'py> for () { + type Target = PyTuple; + type Output = Bound<'py, Self::Target>; + type Error = Infallible; + + fn into_pyobject(self, py: Python<'py>) -> Result { + Ok(PyTuple::empty(py)) + } +} + /// ```rust,compile_fail /// use pyo3::prelude::*; /// diff --git a/src/conversions/chrono.rs b/src/conversions/chrono.rs index e7d496d1acd..1da39cdb32b 100644 --- a/src/conversions/chrono.rs +++ b/src/conversions/chrono.rs @@ -40,6 +40,7 @@ //! } //! ``` +use crate::conversion::IntoPyObject; use crate::exceptions::{PyTypeError, PyUserWarning, PyValueError}; #[cfg(Py_LIMITED_API)] use crate::sync::GILOnceCell; @@ -106,6 +107,51 @@ impl IntoPy for Duration { } } +impl<'py> IntoPyObject<'py> for Duration { + #[cfg(Py_LIMITED_API)] + type Target = PyAny; + #[cfg(not(Py_LIMITED_API))] + type Target = PyDelta; + type Output = Bound<'py, Self::Target>; + type Error = PyErr; + + fn into_pyobject(self, py: Python<'py>) -> Result { + // Total number of days + let days = self.num_days(); + // Remainder of seconds + let secs_dur = self - Duration::days(days); + let secs = secs_dur.num_seconds(); + // Fractional part of the microseconds + let micros = (secs_dur - Duration::seconds(secs_dur.num_seconds())) + .num_microseconds() + // This should never panic since we are just getting the fractional + // part of the total microseconds, which should never overflow. + .unwrap(); + + #[cfg(not(Py_LIMITED_API))] + { + // We do not need to check the days i64 to i32 cast from rust because + // python will panic with OverflowError. + // We pass true as the `normalize` parameter since we'd need to do several checks here to + // avoid that, and it shouldn't have a big performance impact. + // The seconds and microseconds cast should never overflow since it's at most the number of seconds per day + PyDelta::new_bound( + py, + days.try_into().unwrap_or(i32::MAX), + secs.try_into()?, + micros.try_into()?, + true, + ) + } + + #[cfg(Py_LIMITED_API)] + { + DatetimeTypes::try_get(py) + .and_then(|dt| dt.timedelta.bind(py).call1((days, secs, micros))) + } + } +} + impl FromPyObject<'_> for Duration { fn extract_bound(ob: &Bound<'_, PyAny>) -> PyResult { // Python size are much lower than rust size so we do not need bound checks. @@ -163,6 +209,28 @@ impl IntoPy for NaiveDate { } } +impl<'py> IntoPyObject<'py> for NaiveDate { + #[cfg(Py_LIMITED_API)] + type Target = PyAny; + #[cfg(not(Py_LIMITED_API))] + type Target = PyDate; + type Output = Bound<'py, Self::Target>; + type Error = PyErr; + + fn into_pyobject(self, py: Python<'py>) -> Result { + let DateArgs { year, month, day } = (&self).into(); + #[cfg(not(Py_LIMITED_API))] + { + PyDate::new_bound(py, year, month, day) + } + + #[cfg(Py_LIMITED_API)] + { + DatetimeTypes::try_get(py).and_then(|dt| dt.date.bind(py).call1((year, month, day))) + } + } +} + impl FromPyObject<'_> for NaiveDate { fn extract_bound(ob: &Bound<'_, PyAny>) -> PyResult { #[cfg(not(Py_LIMITED_API))] @@ -209,6 +277,38 @@ impl IntoPy for NaiveTime { } } +impl<'py> IntoPyObject<'py> for NaiveTime { + #[cfg(Py_LIMITED_API)] + type Target = PyAny; + #[cfg(not(Py_LIMITED_API))] + type Target = PyTime; + type Output = Bound<'py, Self::Target>; + type Error = PyErr; + + fn into_pyobject(self, py: Python<'py>) -> Result { + let TimeArgs { + hour, + min, + sec, + micro, + truncated_leap_second, + } = (&self).into(); + + #[cfg(not(Py_LIMITED_API))] + let time = PyTime::new_bound(py, hour, min, sec, micro, None)?; + + #[cfg(Py_LIMITED_API)] + let time = DatetimeTypes::try_get(py) + .and_then(|dt| dt.time.bind(py).call1((hour, min, sec, micro)))?; + + if truncated_leap_second { + warn_truncated_leap_second(&time); + } + + Ok(time) + } +} + impl FromPyObject<'_> for NaiveTime { fn extract_bound(ob: &Bound<'_, PyAny>) -> PyResult { #[cfg(not(Py_LIMITED_API))] @@ -236,6 +336,42 @@ impl IntoPy for NaiveDateTime { } } +impl<'py> IntoPyObject<'py> for NaiveDateTime { + #[cfg(Py_LIMITED_API)] + type Target = PyAny; + #[cfg(not(Py_LIMITED_API))] + type Target = PyDateTime; + type Output = Bound<'py, Self::Target>; + type Error = PyErr; + + fn into_pyobject(self, py: Python<'py>) -> Result { + let DateArgs { year, month, day } = (&self.date()).into(); + let TimeArgs { + hour, + min, + sec, + micro, + truncated_leap_second, + } = (&self.time()).into(); + + #[cfg(not(Py_LIMITED_API))] + let datetime = PyDateTime::new_bound(py, year, month, day, hour, min, sec, micro, None)?; + + #[cfg(Py_LIMITED_API)] + let datetime = DatetimeTypes::try_get(py).and_then(|dt| { + dt.datetime + .bind(py) + .call1((year, month, day, hour, min, sec, micro)) + })?; + + if truncated_leap_second { + warn_truncated_leap_second(&datetime); + } + + Ok(datetime) + } +} + impl FromPyObject<'_> for NaiveDateTime { fn extract_bound(dt: &Bound<'_, PyAny>) -> PyResult { #[cfg(not(Py_LIMITED_API))] @@ -275,6 +411,44 @@ impl IntoPy for DateTime { } } +impl<'py, Tz: TimeZone> IntoPyObject<'py> for DateTime { + #[cfg(Py_LIMITED_API)] + type Target = PyAny; + #[cfg(not(Py_LIMITED_API))] + type Target = PyDateTime; + type Output = Bound<'py, Self::Target>; + type Error = PyErr; + + fn into_pyobject(self, py: Python<'py>) -> Result { + let tz = self.offset().fix().into_pyobject(py)?; + let DateArgs { year, month, day } = (&self.naive_local().date()).into(); + let TimeArgs { + hour, + min, + sec, + micro, + truncated_leap_second, + } = (&self.naive_local().time()).into(); + + #[cfg(not(Py_LIMITED_API))] + let datetime = + PyDateTime::new_bound(py, year, month, day, hour, min, sec, micro, Some(&tz))?; + + #[cfg(Py_LIMITED_API)] + let datetime = DatetimeTypes::try_get(py).and_then(|dt| { + dt.datetime + .bind(py) + .call1((year, month, day, hour, min, sec, micro, tz)) + })?; + + if truncated_leap_second { + warn_truncated_leap_second(&datetime); + } + + Ok(datetime) + } +} + impl FromPyObject<'py>> FromPyObject<'_> for DateTime { fn extract_bound(dt: &Bound<'_, PyAny>) -> PyResult> { #[cfg(not(Py_LIMITED_API))] @@ -333,6 +507,30 @@ impl IntoPy for FixedOffset { } } +impl<'py> IntoPyObject<'py> for FixedOffset { + #[cfg(Py_LIMITED_API)] + type Target = PyAny; + #[cfg(not(Py_LIMITED_API))] + type Target = PyTzInfo; + type Output = Bound<'py, Self::Target>; + type Error = PyErr; + + fn into_pyobject(self, py: Python<'py>) -> Result { + let seconds_offset = self.local_minus_utc(); + #[cfg(not(Py_LIMITED_API))] + { + let td = PyDelta::new_bound(py, 0, seconds_offset, 0, true)?; + timezone_from_offset(&td) + } + + #[cfg(Py_LIMITED_API)] + { + let td = Duration::seconds(seconds_offset.into()).into_pyobject(py)?; + DatetimeTypes::try_get(py).and_then(|dt| dt.timezone.bind(py).call1((td,))) + } + } +} + impl FromPyObject<'_> for FixedOffset { /// Convert python tzinfo to rust [`FixedOffset`]. /// @@ -376,6 +574,26 @@ impl IntoPy for Utc { } } +impl<'py> IntoPyObject<'py> for Utc { + #[cfg(Py_LIMITED_API)] + type Target = PyAny; + #[cfg(not(Py_LIMITED_API))] + type Target = PyTzInfo; + type Output = Bound<'py, Self::Target>; + type Error = PyErr; + + fn into_pyobject(self, py: Python<'py>) -> Result { + #[cfg(Py_LIMITED_API)] + { + Ok(timezone_utc_bound(py).into_any()) + } + #[cfg(not(Py_LIMITED_API))] + { + Ok(timezone_utc_bound(py)) + } + } +} + impl FromPyObject<'_> for Utc { fn extract_bound(ob: &Bound<'_, PyAny>) -> PyResult { let py_utc = timezone_utc_bound(ob.py()); @@ -538,22 +756,24 @@ struct DatetimeTypes { #[cfg(Py_LIMITED_API)] impl DatetimeTypes { fn get(py: Python<'_>) -> &Self { + Self::try_get(py).expect("failed to load datetime module") + } + + fn try_get(py: Python<'_>) -> PyResult<&Self> { static TYPES: GILOnceCell = GILOnceCell::new(); - TYPES - .get_or_try_init(py, || { - let datetime = py.import_bound("datetime")?; - let timezone = datetime.getattr("timezone")?; - Ok::<_, PyErr>(Self { - date: datetime.getattr("date")?.into(), - datetime: datetime.getattr("datetime")?.into(), - time: datetime.getattr("time")?.into(), - timedelta: datetime.getattr("timedelta")?.into(), - timezone_utc: timezone.getattr("utc")?.into(), - timezone: timezone.into(), - tzinfo: datetime.getattr("tzinfo")?.into(), - }) + TYPES.get_or_try_init(py, || { + let datetime = py.import_bound("datetime")?; + let timezone = datetime.getattr("timezone")?; + Ok::<_, PyErr>(Self { + date: datetime.getattr("date")?.into(), + datetime: datetime.getattr("datetime")?.into(), + time: datetime.getattr("time")?.into(), + timedelta: datetime.getattr("timedelta")?.into(), + timezone_utc: timezone.getattr("utc")?.into(), + timezone: timezone.into(), + tzinfo: datetime.getattr("tzinfo")?.into(), }) - .expect("failed to load datetime module") + }) } } diff --git a/src/conversions/chrono_tz.rs b/src/conversions/chrono_tz.rs index 845814c4dab..1b5e3cd95f7 100644 --- a/src/conversions/chrono_tz.rs +++ b/src/conversions/chrono_tz.rs @@ -33,12 +33,13 @@ //! }); //! } //! ``` +use crate::conversion::IntoPyObject; use crate::exceptions::PyValueError; use crate::pybacked::PyBackedStr; use crate::sync::GILOnceCell; use crate::types::{any::PyAnyMethods, PyType}; use crate::{ - intern, Bound, FromPyObject, IntoPy, Py, PyAny, PyObject, PyResult, Python, ToPyObject, + intern, Bound, FromPyObject, IntoPy, Py, PyAny, PyErr, PyObject, PyResult, Python, ToPyObject, }; use chrono_tz::Tz; use std::str::FromStr; @@ -61,6 +62,19 @@ impl IntoPy for Tz { } } +impl<'py> IntoPyObject<'py> for Tz { + type Target = PyAny; + type Output = Bound<'py, Self::Target>; + type Error = PyErr; + + fn into_pyobject(self, py: Python<'py>) -> Result { + static ZONE_INFO: GILOnceCell> = GILOnceCell::new(); + ZONE_INFO + .get_or_try_init_type_ref(py, "zoneinfo", "ZoneInfo") + .and_then(|obj| obj.call1((self.name(),))) + } +} + impl FromPyObject<'_> for Tz { fn extract_bound(ob: &Bound<'_, PyAny>) -> PyResult { Tz::from_str( diff --git a/src/conversions/either.rs b/src/conversions/either.rs index 84ec88ea009..2dd79ba3807 100644 --- a/src/conversions/either.rs +++ b/src/conversions/either.rs @@ -46,8 +46,8 @@ #[cfg(feature = "experimental-inspect")] use crate::inspect::types::TypeInfo; use crate::{ - exceptions::PyTypeError, types::any::PyAnyMethods, Bound, FromPyObject, IntoPy, PyAny, - PyObject, PyResult, Python, ToPyObject, + conversion::IntoPyObject, exceptions::PyTypeError, types::any::PyAnyMethods, Bound, + BoundObject, FromPyObject, IntoPy, PyAny, PyErr, PyObject, PyResult, Python, ToPyObject, }; use either::Either; @@ -66,6 +66,34 @@ where } } +#[cfg_attr(docsrs, doc(cfg(feature = "either")))] +impl<'py, L, R, E1, E2> IntoPyObject<'py> for Either +where + L: IntoPyObject<'py, Error = E1>, + R: IntoPyObject<'py, Error = E2>, + E1: Into, + E2: Into, +{ + type Target = PyAny; + type Output = Bound<'py, Self::Target>; + type Error = PyErr; + + fn into_pyobject(self, py: Python<'py>) -> Result { + match self { + Either::Left(l) => l + .into_pyobject(py) + .map(BoundObject::into_any) + .map(BoundObject::into_bound) + .map_err(Into::into), + Either::Right(r) => r + .into_pyobject(py) + .map(BoundObject::into_any) + .map(BoundObject::into_bound) + .map_err(Into::into), + } + } +} + #[cfg_attr(docsrs, doc(cfg(feature = "either")))] impl ToPyObject for Either where diff --git a/src/conversions/hashbrown.rs b/src/conversions/hashbrown.rs index 753b52a965c..d09205fce40 100644 --- a/src/conversions/hashbrown.rs +++ b/src/conversions/hashbrown.rs @@ -17,12 +17,15 @@ //! Note that you must use compatible versions of hashbrown and PyO3. //! The required hashbrown version may vary based on the version of PyO3. use crate::{ - types::any::PyAnyMethods, - types::dict::PyDictMethods, - types::frozenset::PyFrozenSetMethods, - types::set::{new_from_iter, PySetMethods}, - types::{IntoPyDict, PyDict, PyFrozenSet, PySet}, - Bound, FromPyObject, IntoPy, PyAny, PyErr, PyObject, PyResult, Python, ToPyObject, + conversion::IntoPyObject, + types::{ + any::PyAnyMethods, + dict::PyDictMethods, + frozenset::PyFrozenSetMethods, + set::{new_from_iter, try_new_from_iter, PySetMethods}, + IntoPyDict, PyDict, PyFrozenSet, PySet, + }, + Bound, BoundObject, FromPyObject, IntoPy, PyAny, PyErr, PyObject, PyResult, Python, ToPyObject, }; use std::{cmp, hash}; @@ -51,6 +54,29 @@ where } } +impl<'py, K, V, H> IntoPyObject<'py> for hashbrown::HashMap +where + K: hash::Hash + cmp::Eq + IntoPyObject<'py>, + V: IntoPyObject<'py>, + H: hash::BuildHasher, + PyErr: From + From, +{ + type Target = PyDict; + type Output = Bound<'py, Self::Target>; + type Error = PyErr; + + fn into_pyobject(self, py: Python<'py>) -> Result { + let dict = PyDict::new(py); + for (k, v) in self { + dict.set_item( + k.into_pyobject(py)?.into_bound(), + v.into_pyobject(py)?.into_bound(), + )?; + } + Ok(dict) + } +} + impl<'py, K, V, S> FromPyObject<'py> for hashbrown::HashMap where K: FromPyObject<'py> + cmp::Eq + hash::Hash, @@ -90,6 +116,29 @@ where } } +impl<'py, K, H> IntoPyObject<'py> for hashbrown::HashSet +where + K: hash::Hash + cmp::Eq + IntoPyObject<'py>, + H: hash::BuildHasher, + PyErr: From, +{ + type Target = PySet; + type Output = Bound<'py, Self::Target>; + type Error = PyErr; + + fn into_pyobject(self, py: Python<'py>) -> Result { + try_new_from_iter( + py, + self.into_iter().map(|item| { + item.into_pyobject(py) + .map(BoundObject::into_any) + .map(BoundObject::unbind) + .map_err(Into::into) + }), + ) + } +} + impl<'py, K, S> FromPyObject<'py> for hashbrown::HashSet where K: FromPyObject<'py> + cmp::Eq + hash::Hash, diff --git a/src/conversions/indexmap.rs b/src/conversions/indexmap.rs index 2d79bb8fba1..3725bab70fc 100644 --- a/src/conversions/indexmap.rs +++ b/src/conversions/indexmap.rs @@ -87,8 +87,9 @@ //! # if another hash table was used, the order could be random //! ``` +use crate::conversion::IntoPyObject; use crate::types::*; -use crate::{Bound, FromPyObject, IntoPy, PyErr, PyObject, Python, ToPyObject}; +use crate::{Bound, BoundObject, FromPyObject, IntoPy, PyErr, PyObject, Python, ToPyObject}; use std::{cmp, hash}; impl ToPyObject for indexmap::IndexMap @@ -116,6 +117,29 @@ where } } +impl<'py, K, V, H> IntoPyObject<'py> for indexmap::IndexMap +where + K: hash::Hash + cmp::Eq + IntoPyObject<'py>, + V: IntoPyObject<'py>, + H: hash::BuildHasher, + PyErr: From + From, +{ + type Target = PyDict; + type Output = Bound<'py, Self::Target>; + type Error = PyErr; + + fn into_pyobject(self, py: Python<'py>) -> Result { + let dict = PyDict::new(py); + for (k, v) in self { + dict.set_item( + k.into_pyobject(py)?.into_bound(), + v.into_pyobject(py)?.into_bound(), + )?; + } + Ok(dict) + } +} + impl<'py, K, V, S> FromPyObject<'py> for indexmap::IndexMap where K: FromPyObject<'py> + cmp::Eq + hash::Hash, diff --git a/src/conversions/num_bigint.rs b/src/conversions/num_bigint.rs index 3fc5952540c..74322bde482 100644 --- a/src/conversions/num_bigint.rs +++ b/src/conversions/num_bigint.rs @@ -52,10 +52,11 @@ use crate::ffi_ptr_ext::FfiPtrExt; #[cfg(Py_LIMITED_API)] use crate::types::{bytes::PyBytesMethods, PyBytes}; use crate::{ + conversion::IntoPyObject, ffi, instance::Bound, types::{any::PyAnyMethods, PyInt}, - FromPyObject, IntoPy, Py, PyAny, PyObject, PyResult, Python, ToPyObject, + FromPyObject, IntoPy, Py, PyAny, PyErr, PyObject, PyResult, Python, ToPyObject, }; use num_bigint::{BigInt, BigUint}; @@ -133,6 +134,48 @@ macro_rules! bigint_conversion { self.to_object(py) } } + + #[cfg_attr(docsrs, doc(cfg(feature = "num-bigint")))] + impl<'py> IntoPyObject<'py> for $rust_ty { + type Target = PyInt; + type Output = Bound<'py, Self::Target>; + type Error = PyErr; + + #[cfg(not(Py_LIMITED_API))] + fn into_pyobject(self, py: Python<'py>) -> Result { + use crate::ffi_ptr_ext::FfiPtrExt; + let bytes = $to_bytes(&self); + unsafe { + Ok(ffi::_PyLong_FromByteArray( + bytes.as_ptr().cast(), + bytes.len(), + 1, + $is_signed.into(), + ) + .assume_owned(py) + .downcast_into_unchecked()) + } + } + + #[cfg(Py_LIMITED_API)] + fn into_pyobject(self, py: Python<'py>) -> Result { + use $crate::py_result_ext::PyResultExt; + let bytes = $to_bytes(&self); + let bytes_obj = PyBytes::new(py, &bytes); + let kwargs = if $is_signed { + let kwargs = crate::types::PyDict::new(py); + kwargs.set_item(crate::intern!(py, "signed"), true)?; + Some(kwargs) + } else { + None + }; + unsafe { + py.get_type_bound::() + .call_method("from_bytes", (bytes_obj, "little"), kwargs.as_ref()) + .downcast_into_unchecked() + } + } + } }; } diff --git a/src/conversions/num_complex.rs b/src/conversions/num_complex.rs index caae94fb9e0..0b2e714b995 100644 --- a/src/conversions/num_complex.rs +++ b/src/conversions/num_complex.rs @@ -137,6 +137,23 @@ macro_rules! complex_conversion { } } + #[cfg_attr(docsrs, doc(cfg(feature = "num-complex")))] + impl<'py> crate::conversion::IntoPyObject<'py> for Complex<$float> { + type Target = PyComplex; + type Output = Bound<'py, Self::Target>; + type Error = std::convert::Infallible; + + fn into_pyobject(self, py: Python<'py>) -> Result { + unsafe { + Ok( + ffi::PyComplex_FromDoubles(self.re as c_double, self.im as c_double) + .assume_owned(py) + .downcast_into_unchecked(), + ) + } + } + } + #[cfg_attr(docsrs, doc(cfg(feature = "num-complex")))] impl FromPyObject<'_> for Complex<$float> { fn extract_bound(obj: &Bound<'_, PyAny>) -> PyResult> { diff --git a/src/conversions/rust_decimal.rs b/src/conversions/rust_decimal.rs index 31178850318..4b8a2083ab6 100644 --- a/src/conversions/rust_decimal.rs +++ b/src/conversions/rust_decimal.rs @@ -49,12 +49,15 @@ //! assert d + 1 == value //! ``` +use crate::conversion::IntoPyObject; use crate::exceptions::PyValueError; use crate::sync::GILOnceCell; use crate::types::any::PyAnyMethods; use crate::types::string::PyStringMethods; use crate::types::PyType; -use crate::{Bound, FromPyObject, IntoPy, Py, PyAny, PyObject, PyResult, Python, ToPyObject}; +use crate::{ + Bound, FromPyObject, IntoPy, Py, PyAny, PyErr, PyObject, PyResult, Python, ToPyObject, +}; use rust_decimal::Decimal; use std::str::FromStr; @@ -99,10 +102,22 @@ impl IntoPy for Decimal { } } +impl<'py> IntoPyObject<'py> for Decimal { + type Target = PyAny; + type Output = Bound<'py, Self::Target>; + type Error = PyErr; + + fn into_pyobject(self, py: Python<'py>) -> Result { + let dec_cls = get_decimal_cls(py)?; + // now call the constructor with the Rust Decimal string-ified + // to not be lossy + dec_cls.call1((self.to_string(),)) + } +} + #[cfg(test)] mod test_rust_decimal { use super::*; - use crate::err::PyErr; use crate::types::dict::PyDictMethods; use crate::types::PyDict; diff --git a/src/conversions/smallvec.rs b/src/conversions/smallvec.rs index 7a6d5a4a78d..090944d4412 100644 --- a/src/conversions/smallvec.rs +++ b/src/conversions/smallvec.rs @@ -15,15 +15,17 @@ //! //! 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::conversion::IntoPyObject; use crate::exceptions::PyTypeError; #[cfg(feature = "experimental-inspect")] use crate::inspect::types::TypeInfo; use crate::types::any::PyAnyMethods; -use crate::types::list::new_from_iter; -use crate::types::{PySequence, PyString}; +use crate::types::list::{new_from_iter, try_new_from_iter}; +use crate::types::{PyList, PySequence, PyString}; +use crate::PyErr; use crate::{ - err::DowncastError, ffi, Bound, FromPyObject, IntoPy, PyAny, PyObject, PyResult, Python, - ToPyObject, + err::DowncastError, ffi, Bound, BoundObject, FromPyObject, IntoPy, PyAny, PyObject, PyResult, + Python, ToPyObject, }; use smallvec::{Array, SmallVec}; @@ -54,6 +56,27 @@ where } } +impl<'py, A> IntoPyObject<'py> for SmallVec
+where + A: Array, + A::Item: IntoPyObject<'py>, + PyErr: From<>::Error>, +{ + type Target = PyList; + type Output = Bound<'py, Self::Target>; + type Error = PyErr; + + fn into_pyobject(self, py: Python<'py>) -> Result { + let mut iter = self.into_iter().map(|e| { + e.into_pyobject(py) + .map(BoundObject::into_any) + .map(BoundObject::unbind) + .map_err(Into::into) + }); + try_new_from_iter(py, &mut iter) + } +} + impl<'py, A> FromPyObject<'py> for SmallVec where A: Array, @@ -97,7 +120,7 @@ where #[cfg(test)] mod tests { use super::*; - use crate::types::{PyDict, PyList}; + use crate::types::PyDict; #[test] fn test_smallvec_into_py() { diff --git a/src/conversions/std/array.rs b/src/conversions/std/array.rs index 3e575127793..3af13f20532 100644 --- a/src/conversions/std/array.rs +++ b/src/conversions/std/array.rs @@ -1,6 +1,8 @@ +use crate::conversion::IntoPyObject; +use crate::ffi_ptr_ext::FfiPtrExt; use crate::instance::Bound; use crate::types::any::PyAnyMethods; -use crate::types::PySequence; +use crate::types::{PyList, PySequence}; use crate::{ err::DowncastError, ffi, FromPyObject, IntoPy, Py, PyAny, PyObject, PyResult, Python, ToPyObject, @@ -36,6 +38,40 @@ where } } +impl<'py, T, const N: usize> IntoPyObject<'py> for [T; N] +where + T: IntoPyObject<'py>, +{ + type Target = PyList; + type Output = Bound<'py, Self::Target>; + type Error = T::Error; + + fn into_pyobject(self, py: Python<'py>) -> Result { + use crate::BoundObject; + unsafe { + let len = N as ffi::Py_ssize_t; + + let ptr = ffi::PyList_New(len); + + // We create the `Bound` pointer here for two reasons: + // - panics if the ptr is null + // - its Drop cleans up the list if user code errors or panics. + let list = ptr.assume_owned(py).downcast_into_unchecked::(); + + for (i, obj) in (0..len).zip(self) { + let obj = obj.into_pyobject(py)?.into_ptr(); + + #[cfg(not(Py_LIMITED_API))] + ffi::PyList_SET_ITEM(ptr, i, obj); + #[cfg(Py_LIMITED_API)] + ffi::PyList_SetItem(ptr, i, obj); + } + + Ok(list) + } + } +} + impl ToPyObject for [T; N] where T: ToPyObject, diff --git a/src/conversions/std/cell.rs b/src/conversions/std/cell.rs index 69d990910d3..c3ea4d97bab 100644 --- a/src/conversions/std/cell.rs +++ b/src/conversions/std/cell.rs @@ -1,8 +1,8 @@ use std::cell::Cell; use crate::{ - types::any::PyAnyMethods, Bound, FromPyObject, IntoPy, PyAny, PyObject, PyResult, Python, - ToPyObject, + conversion::IntoPyObject, types::any::PyAnyMethods, Bound, FromPyObject, IntoPy, PyAny, + PyObject, PyResult, Python, ToPyObject, }; impl ToPyObject for Cell { @@ -17,6 +17,16 @@ impl> IntoPy for Cell { } } +impl<'py, T: Copy + IntoPyObject<'py>> IntoPyObject<'py> for Cell { + type Target = T::Target; + type Output = T::Output; + type Error = T::Error; + + fn into_pyobject(self, py: Python<'py>) -> Result { + self.get().into_pyobject(py) + } +} + impl<'py, T: FromPyObject<'py>> FromPyObject<'py> for Cell { fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult { ob.extract().map(Cell::new) diff --git a/src/conversions/std/ipaddr.rs b/src/conversions/std/ipaddr.rs index 5d030b445d8..688ec78ba1a 100755 --- a/src/conversions/std/ipaddr.rs +++ b/src/conversions/std/ipaddr.rs @@ -1,12 +1,15 @@ use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; +use crate::conversion::IntoPyObject; use crate::exceptions::PyValueError; use crate::instance::Bound; use crate::sync::GILOnceCell; use crate::types::any::PyAnyMethods; use crate::types::string::PyStringMethods; use crate::types::PyType; -use crate::{intern, FromPyObject, IntoPy, Py, PyAny, PyObject, PyResult, Python, ToPyObject}; +use crate::{ + intern, FromPyObject, IntoPy, Py, PyAny, PyErr, PyObject, PyResult, Python, ToPyObject, +}; impl FromPyObject<'_> for IpAddr { fn extract_bound(obj: &Bound<'_, PyAny>) -> PyResult { @@ -40,6 +43,19 @@ impl ToPyObject for Ipv4Addr { } } +impl<'py> IntoPyObject<'py> for Ipv4Addr { + type Target = PyAny; + type Output = Bound<'py, Self::Target>; + type Error = PyErr; + + fn into_pyobject(self, py: Python<'py>) -> Result { + static IPV4_ADDRESS: GILOnceCell> = GILOnceCell::new(); + IPV4_ADDRESS + .get_or_try_init_type_ref(py, "ipaddress", "IPv4Address")? + .call1((u32::from_be_bytes(self.octets()),)) + } +} + impl ToPyObject for Ipv6Addr { fn to_object(&self, py: Python<'_>) -> PyObject { static IPV6_ADDRESS: GILOnceCell> = GILOnceCell::new(); @@ -52,6 +68,19 @@ impl ToPyObject for Ipv6Addr { } } +impl<'py> IntoPyObject<'py> for Ipv6Addr { + type Target = PyAny; + type Output = Bound<'py, Self::Target>; + type Error = PyErr; + + fn into_pyobject(self, py: Python<'py>) -> Result { + static IPV6_ADDRESS: GILOnceCell> = GILOnceCell::new(); + IPV6_ADDRESS + .get_or_try_init_type_ref(py, "ipaddress", "IPv6Address")? + .call1((u128::from_be_bytes(self.octets()),)) + } +} + impl ToPyObject for IpAddr { fn to_object(&self, py: Python<'_>) -> PyObject { match self { @@ -67,6 +96,19 @@ impl IntoPy for IpAddr { } } +impl<'py> IntoPyObject<'py> for IpAddr { + type Target = PyAny; + type Output = Bound<'py, Self::Target>; + type Error = PyErr; + + fn into_pyobject(self, py: Python<'py>) -> Result { + match self { + IpAddr::V4(ip) => ip.into_pyobject(py), + IpAddr::V6(ip) => ip.into_pyobject(py), + } + } +} + #[cfg(test)] mod test_ipaddr { use std::str::FromStr; diff --git a/src/conversions/std/map.rs b/src/conversions/std/map.rs index 4343b28fb78..2a966177693 100644 --- a/src/conversions/std/map.rs +++ b/src/conversions/std/map.rs @@ -3,10 +3,10 @@ use std::{cmp, collections, hash}; #[cfg(feature = "experimental-inspect")] use crate::inspect::types::TypeInfo; use crate::{ + conversion::IntoPyObject, instance::Bound, - types::dict::PyDictMethods, - types::{any::PyAnyMethods, IntoPyDict, PyDict}, - FromPyObject, IntoPy, PyAny, PyErr, PyObject, Python, ToPyObject, + types::{any::PyAnyMethods, dict::PyDictMethods, IntoPyDict, PyDict}, + BoundObject, FromPyObject, IntoPy, PyAny, PyErr, PyObject, Python, ToPyObject, }; impl ToPyObject for collections::HashMap @@ -49,6 +49,29 @@ where } } +impl<'py, K, V, H> IntoPyObject<'py> for collections::HashMap +where + K: hash::Hash + cmp::Eq + IntoPyObject<'py>, + V: IntoPyObject<'py>, + H: hash::BuildHasher, + PyErr: From + From, +{ + type Target = PyDict; + type Output = Bound<'py, Self::Target>; + type Error = PyErr; + + fn into_pyobject(self, py: Python<'py>) -> Result { + let dict = PyDict::new(py); + for (k, v) in self { + dict.set_item( + k.into_pyobject(py)?.into_bound(), + v.into_pyobject(py)?.into_bound(), + )?; + } + Ok(dict) + } +} + impl IntoPy for collections::BTreeMap where K: cmp::Eq + IntoPy, @@ -67,6 +90,28 @@ where } } +impl<'py, K, V> IntoPyObject<'py> for collections::BTreeMap +where + K: cmp::Eq + IntoPyObject<'py>, + V: IntoPyObject<'py>, + PyErr: From + From, +{ + type Target = PyDict; + type Output = Bound<'py, Self::Target>; + type Error = PyErr; + + fn into_pyobject(self, py: Python<'py>) -> Result { + let dict = PyDict::new(py); + for (k, v) in self { + dict.set_item( + k.into_pyobject(py)?.into_bound(), + v.into_pyobject(py)?.into_bound(), + )?; + } + Ok(dict) + } +} + impl<'py, K, V, S> FromPyObject<'py> for collections::HashMap where K: FromPyObject<'py> + cmp::Eq + hash::Hash, diff --git a/src/conversions/std/num.rs b/src/conversions/std/num.rs index 2648f1d9ee8..aaaac8734b2 100644 --- a/src/conversions/std/num.rs +++ b/src/conversions/std/num.rs @@ -1,11 +1,14 @@ +use crate::conversion::IntoPyObject; use crate::ffi_ptr_ext::FfiPtrExt; #[cfg(feature = "experimental-inspect")] use crate::inspect::types::TypeInfo; use crate::types::any::PyAnyMethods; +use crate::types::PyInt; use crate::{ exceptions, ffi, Bound, FromPyObject, IntoPy, PyAny, PyErr, PyObject, PyResult, Python, ToPyObject, }; +use std::convert::Infallible; use std::num::{ NonZeroI128, NonZeroI16, NonZeroI32, NonZeroI64, NonZeroI8, NonZeroIsize, NonZeroU128, NonZeroU16, NonZeroU32, NonZeroU64, NonZeroU8, NonZeroUsize, @@ -31,6 +34,16 @@ macro_rules! int_fits_larger_int { } } + impl<'py> IntoPyObject<'py> for $rust_type { + type Target = PyInt; + type Output = Bound<'py, Self::Target>; + type Error = Infallible; + + fn into_pyobject(self, py: Python<'py>) -> Result { + (self as $larger_type).into_pyobject(py) + } + } + impl FromPyObject<'_> for $rust_type { fn extract_bound(obj: &Bound<'_, PyAny>) -> PyResult { let val: $larger_type = obj.extract()?; @@ -90,6 +103,19 @@ macro_rules! int_convert_u64_or_i64 { TypeInfo::builtin("int") } } + impl<'py> IntoPyObject<'py> for $rust_type { + type Target = PyInt; + type Output = Bound<'py, Self::Target>; + type Error = Infallible; + + fn into_pyobject(self, py: Python<'py>) -> Result { + unsafe { + Ok($pylong_from_ll_or_ull(self) + .assume_owned(py) + .downcast_into_unchecked()) + } + } + } impl FromPyObject<'_> for $rust_type { fn extract_bound(obj: &Bound<'_, PyAny>) -> PyResult<$rust_type> { extract_int!(obj, !0, $pylong_as_ll_or_ull, $force_index_call) @@ -121,6 +147,20 @@ macro_rules! int_fits_c_long { } } + impl<'py> IntoPyObject<'py> for $rust_type { + type Target = PyInt; + type Output = Bound<'py, Self::Target>; + type Error = Infallible; + + fn into_pyobject(self, py: Python<'py>) -> Result { + unsafe { + Ok(ffi::PyLong_FromLong(self as c_long) + .assume_owned(py) + .downcast_into_unchecked()) + } + } + } + impl<'py> FromPyObject<'py> for $rust_type { fn extract_bound(obj: &Bound<'_, PyAny>) -> PyResult { let val: c_long = extract_int!(obj, -1, ffi::PyLong_AsLong)?; diff --git a/src/conversions/std/option.rs b/src/conversions/std/option.rs index 13527315e70..2ef00acb550 100644 --- a/src/conversions/std/option.rs +++ b/src/conversions/std/option.rs @@ -1,6 +1,6 @@ use crate::{ - ffi, types::any::PyAnyMethods, AsPyPointer, Bound, FromPyObject, IntoPy, PyAny, PyObject, - PyResult, Python, ToPyObject, + conversion::IntoPyObject, ffi, types::any::PyAnyMethods, AsPyPointer, Bound, BoundObject, + FromPyObject, IntoPy, PyAny, PyObject, PyResult, Python, ToPyObject, }; /// `Option::Some` is converted like `T`. @@ -24,6 +24,26 @@ where } } +impl<'py, T> IntoPyObject<'py> for Option +where + T: IntoPyObject<'py>, +{ + type Target = PyAny; + type Output = Bound<'py, Self::Target>; + type Error = T::Error; + + fn into_pyobject(self, py: Python<'py>) -> Result { + self.map_or_else( + || Ok(py.None().into_bound(py)), + |val| { + val.into_pyobject(py) + .map(BoundObject::into_any) + .map(BoundObject::into_bound) + }, + ) + } +} + impl<'py, T> FromPyObject<'py> for Option where T: FromPyObject<'py>, diff --git a/src/conversions/std/osstr.rs b/src/conversions/std/osstr.rs index 8616a11689c..3904010d35a 100644 --- a/src/conversions/std/osstr.rs +++ b/src/conversions/std/osstr.rs @@ -1,15 +1,28 @@ +use crate::conversion::IntoPyObject; +use crate::ffi_ptr_ext::FfiPtrExt; use crate::instance::Bound; use crate::types::any::PyAnyMethods; use crate::types::PyString; use crate::{ffi, FromPyObject, IntoPy, PyAny, PyObject, PyResult, Python, ToPyObject}; use std::borrow::Cow; +use std::convert::Infallible; use std::ffi::{OsStr, OsString}; impl ToPyObject for OsStr { fn to_object(&self, py: Python<'_>) -> PyObject { + self.into_pyobject(py).unwrap().into_any().unbind() + } +} + +impl<'py> IntoPyObject<'py> for &OsStr { + type Target = PyString; + type Output = Bound<'py, Self::Target>; + type Error = Infallible; + + fn into_pyobject(self, py: Python<'py>) -> Result { // If the string is UTF-8, take the quick and easy shortcut if let Some(valid_utf8_path) = self.to_str() { - return valid_utf8_path.to_object(py); + return valid_utf8_path.into_pyobject(py); } // All targets besides windows support the std::os::unix::ffi::OsStrExt API: @@ -26,8 +39,9 @@ impl ToPyObject for OsStr { unsafe { // DecodeFSDefault automatically chooses an appropriate decoding mechanism to // parse os strings losslessly (i.e. surrogateescape most of the time) - let pystring = ffi::PyUnicode_DecodeFSDefaultAndSize(ptr, len); - PyObject::from_owned_ptr(py, pystring) + Ok(ffi::PyUnicode_DecodeFSDefaultAndSize(ptr, len) + .assume_owned(py) + .downcast_into_unchecked::()) } } @@ -38,9 +52,11 @@ impl ToPyObject for OsStr { unsafe { // This will not panic because the data from encode_wide is well-formed Windows // string data - PyObject::from_owned_ptr( - py, - ffi::PyUnicode_FromWideChar(wstr.as_ptr(), wstr.len() as ffi::Py_ssize_t), + + Ok( + ffi::PyUnicode_FromWideChar(wstr.as_ptr(), wstr.len() as ffi::Py_ssize_t) + .assume_owned(py) + .downcast_into_unchecked::(), ) } } @@ -124,6 +140,16 @@ impl IntoPy for Cow<'_, OsStr> { } } +impl<'py> IntoPyObject<'py> for Cow<'_, OsStr> { + type Target = PyString; + type Output = Bound<'py, Self::Target>; + type Error = Infallible; + + fn into_pyobject(self, py: Python<'py>) -> Result { + (*self).into_pyobject(py) + } +} + impl ToPyObject for OsString { #[inline] fn to_object(&self, py: Python<'_>) -> PyObject { @@ -137,12 +163,32 @@ impl IntoPy for OsString { } } +impl<'py> IntoPyObject<'py> for OsString { + type Target = PyString; + type Output = Bound<'py, Self::Target>; + type Error = Infallible; + + fn into_pyobject(self, py: Python<'py>) -> Result { + self.as_os_str().into_pyobject(py) + } +} + impl<'a> IntoPy for &'a OsString { fn into_py(self, py: Python<'_>) -> PyObject { self.to_object(py) } } +impl<'py> IntoPyObject<'py> for &OsString { + type Target = PyString; + type Output = Bound<'py, Self::Target>; + type Error = Infallible; + + fn into_pyobject(self, py: Python<'py>) -> Result { + self.as_os_str().into_pyobject(py) + } +} + #[cfg(test)] mod tests { use crate::types::{PyAnyMethods, PyStringMethods}; diff --git a/src/conversions/std/path.rs b/src/conversions/std/path.rs index d7f3121ea10..1540c852f05 100644 --- a/src/conversions/std/path.rs +++ b/src/conversions/std/path.rs @@ -1,8 +1,11 @@ +use crate::conversion::IntoPyObject; use crate::ffi_ptr_ext::FfiPtrExt; use crate::instance::Bound; use crate::types::any::PyAnyMethods; +use crate::types::PyString; use crate::{ffi, FromPyObject, IntoPy, PyAny, PyObject, PyResult, Python, ToPyObject}; use std::borrow::Cow; +use std::convert::Infallible; use std::ffi::OsString; use std::path::{Path, PathBuf}; @@ -29,6 +32,16 @@ impl<'a> IntoPy for &'a Path { } } +impl<'py> IntoPyObject<'py> for &Path { + type Target = PyString; + type Output = Bound<'py, Self::Target>; + type Error = Infallible; + + fn into_pyobject(self, py: Python<'py>) -> Result { + self.as_os_str().into_pyobject(py) + } +} + impl<'a> ToPyObject for Cow<'a, Path> { #[inline] fn to_object(&self, py: Python<'_>) -> PyObject { @@ -43,6 +56,16 @@ impl<'a> IntoPy for Cow<'a, Path> { } } +impl<'py> IntoPyObject<'py> for Cow<'_, Path> { + type Target = PyString; + type Output = Bound<'py, Self::Target>; + type Error = Infallible; + + fn into_pyobject(self, py: Python<'py>) -> Result { + self.as_os_str().into_pyobject(py) + } +} + impl ToPyObject for PathBuf { #[inline] fn to_object(&self, py: Python<'_>) -> PyObject { @@ -56,12 +79,32 @@ impl IntoPy for PathBuf { } } +impl<'py> IntoPyObject<'py> for PathBuf { + type Target = PyString; + type Output = Bound<'py, Self::Target>; + type Error = Infallible; + + fn into_pyobject(self, py: Python<'py>) -> Result { + self.as_os_str().into_pyobject(py) + } +} + impl<'a> IntoPy for &'a PathBuf { fn into_py(self, py: Python<'_>) -> PyObject { self.as_os_str().to_object(py) } } +impl<'py> IntoPyObject<'py> for &PathBuf { + type Target = PyString; + type Output = Bound<'py, Self::Target>; + type Error = Infallible; + + fn into_pyobject(self, py: Python<'py>) -> Result { + self.as_os_str().into_pyobject(py) + } +} + #[cfg(test)] mod tests { use crate::types::{PyAnyMethods, PyStringMethods}; diff --git a/src/conversions/std/set.rs b/src/conversions/std/set.rs index e98a10e5fad..165194bbafc 100644 --- a/src/conversions/std/set.rs +++ b/src/conversions/std/set.rs @@ -3,12 +3,15 @@ use std::{cmp, collections, hash}; #[cfg(feature = "experimental-inspect")] use crate::inspect::types::TypeInfo; use crate::{ + conversion::IntoPyObject, instance::Bound, - types::any::PyAnyMethods, - types::frozenset::PyFrozenSetMethods, - types::set::{new_from_iter, PySetMethods}, - types::{PyFrozenSet, PySet}, - FromPyObject, IntoPy, PyAny, PyErr, PyObject, PyResult, Python, ToPyObject, + types::{ + any::PyAnyMethods, + frozenset::PyFrozenSetMethods, + set::{new_from_iter, try_new_from_iter, PySetMethods}, + PyFrozenSet, PySet, + }, + BoundObject, FromPyObject, IntoPy, PyAny, PyErr, PyObject, PyResult, Python, ToPyObject, }; impl ToPyObject for collections::HashSet @@ -51,6 +54,29 @@ where } } +impl<'py, K, S> IntoPyObject<'py> for collections::HashSet +where + K: IntoPyObject<'py> + Eq + hash::Hash, + S: hash::BuildHasher + Default, + PyErr: From, +{ + type Target = PySet; + type Output = Bound<'py, Self::Target>; + type Error = PyErr; + + fn into_pyobject(self, py: Python<'py>) -> Result { + try_new_from_iter( + py, + self.into_iter().map(|item| { + item.into_pyobject(py) + .map(BoundObject::into_any) + .map(BoundObject::unbind) + .map_err(Into::into) + }), + ) + } +} + impl<'py, K, S> FromPyObject<'py> for collections::HashSet where K: FromPyObject<'py> + cmp::Eq + hash::Hash, @@ -91,6 +117,28 @@ where } } +impl<'py, K> IntoPyObject<'py> for collections::BTreeSet +where + K: IntoPyObject<'py> + Eq + hash::Hash, + PyErr: From, +{ + type Target = PySet; + type Output = Bound<'py, Self::Target>; + type Error = PyErr; + + fn into_pyobject(self, py: Python<'py>) -> Result { + try_new_from_iter( + py, + self.into_iter().map(|item| { + item.into_pyobject(py) + .map(BoundObject::into_any) + .map(BoundObject::unbind) + .map_err(Into::into) + }), + ) + } +} + impl<'py, K> FromPyObject<'py> for collections::BTreeSet where K: FromPyObject<'py> + cmp::Ord, diff --git a/src/conversions/std/slice.rs b/src/conversions/std/slice.rs index e2353a5d320..107b0ad5ce3 100644 --- a/src/conversions/std/slice.rs +++ b/src/conversions/std/slice.rs @@ -1,10 +1,11 @@ -use std::borrow::Cow; +use std::{borrow::Cow, convert::Infallible}; #[cfg(feature = "experimental-inspect")] use crate::inspect::types::TypeInfo; use crate::{ + conversion::IntoPyObject, types::{PyByteArray, PyByteArrayMethods, PyBytes}, - IntoPy, Py, PyAny, PyObject, PyResult, Python, ToPyObject, + Bound, IntoPy, Py, PyAny, PyObject, PyResult, Python, ToPyObject, }; impl<'a> IntoPy for &'a [u8] { @@ -18,6 +19,16 @@ impl<'a> IntoPy for &'a [u8] { } } +impl<'py> IntoPyObject<'py> for &[u8] { + type Target = PyBytes; + type Output = Bound<'py, Self::Target>; + type Error = Infallible; + + fn into_pyobject(self, py: Python<'py>) -> Result { + Ok(PyBytes::new(py, self)) + } +} + impl<'a> crate::conversion::FromPyObjectBound<'a, '_> for &'a [u8] { fn from_py_object_bound(obj: crate::Borrowed<'a, '_, PyAny>) -> PyResult { Ok(obj.downcast::()?.as_bytes()) @@ -62,6 +73,16 @@ impl IntoPy> for Cow<'_, [u8]> { } } +impl<'py> IntoPyObject<'py> for Cow<'_, [u8]> { + type Target = PyBytes; + type Output = Bound<'py, Self::Target>; + type Error = Infallible; + + fn into_pyobject(self, py: Python<'py>) -> Result { + Ok(PyBytes::new(py, &self)) + } +} + #[cfg(test)] mod tests { use std::borrow::Cow; diff --git a/src/conversions/std/string.rs b/src/conversions/std/string.rs index 91b50aa1096..87408030182 100644 --- a/src/conversions/std/string.rs +++ b/src/conversions/std/string.rs @@ -1,8 +1,9 @@ -use std::borrow::Cow; +use std::{borrow::Cow, convert::Infallible}; #[cfg(feature = "experimental-inspect")] use crate::inspect::types::TypeInfo; use crate::{ + conversion::IntoPyObject, instance::Bound, types::{any::PyAnyMethods, string::PyStringMethods, PyString}, FromPyObject, IntoPy, Py, PyAny, PyObject, PyResult, Python, ToPyObject, @@ -41,6 +42,16 @@ impl<'a> IntoPy> for &'a str { } } +impl<'py> IntoPyObject<'py> for &str { + type Target = PyString; + type Output = Bound<'py, Self::Target>; + type Error = Infallible; + + fn into_pyobject(self, py: Python<'py>) -> Result { + Ok(PyString::new_bound(py, self)) + } +} + /// Converts a Rust `Cow<'_, str>` to a Python object. /// See `PyString::new` for details on the conversion. impl ToPyObject for Cow<'_, str> { @@ -62,6 +73,16 @@ impl IntoPy for Cow<'_, str> { } } +impl<'py> IntoPyObject<'py> for Cow<'_, str> { + type Target = PyString; + type Output = Bound<'py, Self::Target>; + type Error = Infallible; + + fn into_pyobject(self, py: Python<'py>) -> Result { + (*self).into_pyobject(py) + } +} + /// Converts a Rust `String` to a Python object. /// See `PyString::new` for details on the conversion. impl ToPyObject for String { @@ -89,6 +110,17 @@ impl IntoPy for char { } } +impl<'py> IntoPyObject<'py> for char { + type Target = PyString; + type Output = Bound<'py, Self::Target>; + type Error = Infallible; + + fn into_pyobject(self, py: Python<'py>) -> Result { + let mut bytes = [0u8; 4]; + Ok(PyString::new_bound(py, self.encode_utf8(&mut bytes))) + } +} + impl IntoPy for String { fn into_py(self, py: Python<'_>) -> PyObject { PyString::new_bound(py, &self).into() @@ -100,6 +132,16 @@ impl IntoPy for String { } } +impl<'py> IntoPyObject<'py> for String { + type Target = PyString; + type Output = Bound<'py, Self::Target>; + type Error = Infallible; + + fn into_pyobject(self, py: Python<'py>) -> Result { + Ok(PyString::new_bound(py, &self)) + } +} + impl<'a> IntoPy for &'a String { #[inline] fn into_py(self, py: Python<'_>) -> PyObject { @@ -112,6 +154,16 @@ impl<'a> IntoPy for &'a String { } } +impl<'py> IntoPyObject<'py> for &String { + type Target = PyString; + type Output = Bound<'py, Self::Target>; + type Error = Infallible; + + fn into_pyobject(self, py: Python<'py>) -> Result { + Ok(PyString::new_bound(py, self)) + } +} + #[cfg(any(Py_3_10, not(Py_LIMITED_API)))] impl<'a> crate::conversion::FromPyObjectBound<'a, '_> for &'a str { fn from_py_object_bound(ob: crate::Borrowed<'a, '_, PyAny>) -> PyResult { diff --git a/src/conversions/std/time.rs b/src/conversions/std/time.rs index c35cb914064..31d7f965883 100755 --- a/src/conversions/std/time.rs +++ b/src/conversions/std/time.rs @@ -1,3 +1,4 @@ +use crate::conversion::IntoPyObject; use crate::exceptions::{PyOverflowError, PyValueError}; use crate::sync::GILOnceCell; use crate::types::any::PyAnyMethods; @@ -89,6 +90,39 @@ impl IntoPy for Duration { } } +impl<'py> IntoPyObject<'py> for Duration { + #[cfg(not(Py_LIMITED_API))] + type Target = PyDelta; + #[cfg(Py_LIMITED_API)] + type Target = PyAny; + type Output = Bound<'py, Self::Target>; + type Error = PyErr; + + fn into_pyobject(self, py: Python<'py>) -> Result { + let days = self.as_secs() / SECONDS_PER_DAY; + let seconds = self.as_secs() % SECONDS_PER_DAY; + let microseconds = self.subsec_micros(); + + #[cfg(not(Py_LIMITED_API))] + { + PyDelta::new_bound( + py, + days.try_into()?, + seconds.try_into().unwrap(), + microseconds.try_into().unwrap(), + false, + ) + } + #[cfg(Py_LIMITED_API)] + { + static TIMEDELTA: GILOnceCell> = GILOnceCell::new(); + TIMEDELTA + .get_or_try_init_type_ref(py, "datetime", "timedelta")? + .call1((days, seconds, microseconds)) + } + } +} + // Conversions between SystemTime and datetime do not rely on the floating point timestamp of the // timestamp/fromtimestamp APIs to avoid possible precision loss but goes through the // timedelta/std::time::Duration types by taking for reference point the UNIX epoch. @@ -123,6 +157,20 @@ impl IntoPy for SystemTime { } } +impl<'py> IntoPyObject<'py> for SystemTime { + type Target = PyAny; + type Output = Bound<'py, Self::Target>; + type Error = PyErr; + + fn into_pyobject(self, py: Python<'py>) -> Result { + let duration_since_unix_epoch = + self.duration_since(UNIX_EPOCH).unwrap().into_pyobject(py)?; + unix_epoch_py(py) + .bind(py) + .call_method1(intern!(py, "__add__"), (duration_since_unix_epoch,)) + } +} + fn unix_epoch_py(py: Python<'_>) -> &PyObject { static UNIX_EPOCH: GILOnceCell = GILOnceCell::new(); UNIX_EPOCH diff --git a/src/conversions/std/vec.rs b/src/conversions/std/vec.rs index df9e4c4f819..9a759baeecf 100644 --- a/src/conversions/std/vec.rs +++ b/src/conversions/std/vec.rs @@ -1,7 +1,9 @@ +use crate::conversion::IntoPyObject; #[cfg(feature = "experimental-inspect")] use crate::inspect::types::TypeInfo; -use crate::types::list::new_from_iter; -use crate::{IntoPy, PyObject, Python, ToPyObject}; +use crate::types::list::{new_from_iter, try_new_from_iter}; +use crate::types::PyList; +use crate::{Bound, BoundObject, IntoPy, PyErr, PyObject, Python, ToPyObject}; impl ToPyObject for [T] where @@ -38,3 +40,24 @@ where TypeInfo::list_of(T::type_output()) } } + +impl<'py, T> IntoPyObject<'py> for Vec +where + T: IntoPyObject<'py>, + PyErr: From, +{ + type Target = PyList; + type Output = Bound<'py, Self::Target>; + type Error = PyErr; + + fn into_pyobject(self, py: Python<'py>) -> Result { + let mut iter = self.into_iter().map(|e| { + e.into_pyobject(py) + .map(BoundObject::into_any) + .map(BoundObject::unbind) + .map_err(Into::into) + }); + + try_new_from_iter(py, &mut iter) + } +} diff --git a/src/impl_/wrap.rs b/src/impl_/wrap.rs index b6a6a4a804b..3ab53cf5239 100644 --- a/src/impl_/wrap.rs +++ b/src/impl_/wrap.rs @@ -1,6 +1,9 @@ -use std::convert::Infallible; +use std::{convert::Infallible, marker::PhantomData, ops::Deref}; -use crate::{ffi, IntoPy, PyObject, PyResult, Python}; +use crate::{ + conversion::IntoPyObject, ffi, types::PyNone, Bound, BoundObject, IntoPy, PyErr, PyObject, + PyResult, Python, +}; /// Used to wrap values in `Option` for default arguments. pub trait SomeWrap { @@ -19,61 +22,156 @@ impl SomeWrap for Option { } } -/// Used to wrap the result of `#[pyfunction]` and `#[pymethods]`. -#[cfg_attr( - diagnostic_namespace, - diagnostic::on_unimplemented( - message = "`{Self}` cannot be converted to a Python object", - note = "`IntoPy` is automatically implemented by the `#[pyclass]` macro", - note = "if you do not wish to have a corresponding Python type, implement `IntoPy` manually", - note = "if you do not own `{Self}` you can perform a manual conversion to one of the types in `pyo3::types::*`" - ) -)] -pub trait OkWrap { - type Error; - fn wrap(self) -> Result; -} - -// The T: IntoPy bound here is necessary to prevent the -// implementation for Result from conflicting -impl OkWrap for T -where - T: IntoPy, -{ - type Error = Infallible; +// Hierarchy of conversions used in the `IntoPy` implementation +pub struct Converter(EmptyTupleConverter); +pub struct EmptyTupleConverter(IntoPyObjectConverter); +pub struct IntoPyObjectConverter(IntoPyConverter); +pub struct IntoPyConverter(UnknownReturnResultType); +pub struct UnknownReturnResultType(UnknownReturnType); +pub struct UnknownReturnType(PhantomData); + +pub fn converter(_: &T) -> Converter { + Converter(EmptyTupleConverter(IntoPyObjectConverter(IntoPyConverter( + UnknownReturnResultType(UnknownReturnType(PhantomData)), + )))) +} + +impl Deref for Converter { + type Target = EmptyTupleConverter; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Deref for EmptyTupleConverter { + type Target = IntoPyObjectConverter; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Deref for IntoPyObjectConverter { + type Target = IntoPyConverter; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Deref for IntoPyConverter { + type Target = UnknownReturnResultType; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Deref for UnknownReturnResultType { + type Target = UnknownReturnType; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl EmptyTupleConverter> { #[inline] - fn wrap(self) -> Result { - Ok(self) + pub fn map_into_ptr(&self, py: Python<'_>, obj: PyResult<()>) -> PyResult<*mut ffi::PyObject> { + obj.map(|_| PyNone::get_bound(py).to_owned().into_ptr()) } } -impl OkWrap for Result -where - T: IntoPy, -{ - type Error = E; +impl<'py, T: IntoPyObject<'py>> IntoPyObjectConverter { #[inline] - fn wrap(self) -> Result { - self + pub fn wrap(&self, obj: T) -> Result { + Ok(obj) + } +} + +impl<'py, T: IntoPyObject<'py>, E> IntoPyObjectConverter> { + #[inline] + pub fn wrap(&self, obj: Result) -> Result { + obj + } + + #[inline] + pub fn map_into_pyobject(&self, py: Python<'py>, obj: PyResult) -> PyResult + where + T: IntoPyObject<'py>, + PyErr: From, + { + obj.and_then(|obj| obj.into_pyobject(py).map_err(Into::into)) + .map(BoundObject::into_any) + .map(BoundObject::unbind) + } + + #[inline] + pub fn map_into_ptr(&self, py: Python<'py>, obj: PyResult) -> PyResult<*mut ffi::PyObject> + where + T: IntoPyObject<'py>, + PyErr: From, + { + obj.and_then(|obj| obj.into_pyobject(py).map_err(Into::into)) + .map(BoundObject::into_bound) + .map(Bound::into_ptr) } } -/// This is a follow-up function to `OkWrap::wrap` that converts the result into -/// a `*mut ffi::PyObject` pointer. -pub fn map_result_into_ptr>( - py: Python<'_>, - result: PyResult, -) -> PyResult<*mut ffi::PyObject> { - result.map(|obj| obj.into_py(py).into_ptr()) +impl> IntoPyConverter { + #[inline] + pub fn wrap(&self, obj: T) -> Result { + Ok(obj) + } } -/// This is a follow-up function to `OkWrap::wrap` that converts the result into -/// a safe wrapper. -pub fn map_result_into_py>( - py: Python<'_>, - result: PyResult, -) -> PyResult { - result.map(|err| err.into_py(py)) +impl, E> IntoPyConverter> { + #[inline] + pub fn wrap(&self, obj: Result) -> Result { + obj + } + + #[inline] + pub fn map_into_pyobject(&self, py: Python<'_>, obj: PyResult) -> PyResult { + obj.map(|obj| obj.into_py(py)) + } + + #[inline] + pub fn map_into_ptr(&self, py: Python<'_>, obj: PyResult) -> PyResult<*mut ffi::PyObject> { + obj.map(|obj| obj.into_py(py).into_ptr()) + } +} + +impl UnknownReturnResultType> { + #[inline] + pub fn wrap<'py>(&self, _: Result) -> Result + where + T: IntoPyObject<'py>, + { + unreachable!("should be handled by IntoPyObjectConverter") + } +} + +impl UnknownReturnType { + #[inline] + pub fn wrap<'py>(&self, _: T) -> T + where + T: IntoPyObject<'py>, + { + unreachable!("should be handled by IntoPyObjectConverter") + } + + #[inline] + pub fn map_into_pyobject<'py>(&self, _: Python<'py>, _: PyResult) -> PyResult + where + T: IntoPyObject<'py>, + { + unreachable!("should be handled by IntoPyObjectConverter") + } + + #[inline] + pub fn map_into_ptr<'py>(&self, _: Python<'py>, _: PyResult) -> PyResult<*mut ffi::PyObject> + where + T: IntoPyObject<'py>, + { + unreachable!("should be handled by IntoPyObjectConverter") + } } #[cfg(test)] @@ -88,16 +186,4 @@ mod tests { let b: Option = SomeWrap::wrap(None); assert_eq!(b, None); } - - #[test] - fn wrap_result() { - let a: Result = OkWrap::wrap(42u8); - assert!(matches!(a, Ok(42))); - - let b: PyResult = OkWrap::wrap(Ok(42u8)); - assert!(matches!(b, Ok(42))); - - let c: Result = OkWrap::wrap(Err("error")); - assert_eq!(c, Err("error")); - } } diff --git a/src/instance.rs b/src/instance.rs index 8a89a2db935..1adc70d6fa4 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -15,6 +15,30 @@ use std::mem::ManuallyDrop; use std::ops::Deref; use std::ptr::NonNull; +/// Owned or borrowed gil-bound Python smart pointer +pub trait BoundObject<'py, T>: bound_object_sealed::Sealed { + /// Type erased version of `Self` + type Any: BoundObject<'py, PyAny>; + /// Borrow this smart pointer. + fn as_borrowed(&self) -> Borrowed<'_, 'py, T>; + /// Turns this smart pointer into an owned [`Bound<'py, T>`] + fn into_bound(self) -> Bound<'py, T>; + /// Upcast the target type of this smart pointer + fn into_any(self) -> Self::Any; + /// Turn this smart pointer into a strong reference pointer + fn into_ptr(self) -> *mut ffi::PyObject; + /// Turn this smart pointer into an owned [`Py`] + fn unbind(self) -> Py; +} + +mod bound_object_sealed { + pub trait Sealed {} + + impl<'py, T> Sealed for super::Bound<'py, T> {} + impl<'a, 'py, T> Sealed for &'a super::Bound<'py, T> {} + impl<'a, 'py, T> Sealed for super::Borrowed<'a, 'py, T> {} +} + /// A GIL-attached equivalent to [`Py`]. /// /// This type can be thought of as equivalent to the tuple `(Py, Python<'py>)`. By having the `'py` @@ -566,6 +590,54 @@ unsafe impl AsPyPointer for Bound<'_, T> { } } +impl<'py, T> BoundObject<'py, T> for Bound<'py, T> { + type Any = Bound<'py, PyAny>; + + fn as_borrowed(&self) -> Borrowed<'_, 'py, T> { + Bound::as_borrowed(self) + } + + fn into_bound(self) -> Bound<'py, T> { + self + } + + fn into_any(self) -> Self::Any { + self.into_any() + } + + fn into_ptr(self) -> *mut ffi::PyObject { + self.into_ptr() + } + + fn unbind(self) -> Py { + self.unbind() + } +} + +impl<'a, 'py, T> BoundObject<'py, T> for &'a Bound<'py, T> { + type Any = &'a Bound<'py, PyAny>; + + fn as_borrowed(&self) -> Borrowed<'a, 'py, T> { + Bound::as_borrowed(self) + } + + fn into_bound(self) -> Bound<'py, T> { + self.clone() + } + + fn into_any(self) -> Self::Any { + self.as_any() + } + + fn into_ptr(self) -> *mut ffi::PyObject { + self.clone().into_ptr() + } + + fn unbind(self) -> Py { + self.clone().unbind() + } +} + /// A borrowed equivalent to `Bound`. /// /// The advantage of this over `&Bound` is that it avoids the need to have a pointer-to-pointer, as Bound @@ -575,7 +647,7 @@ unsafe impl AsPyPointer for Bound<'_, T> { #[repr(transparent)] pub struct Borrowed<'a, 'py, T>(NonNull, PhantomData<&'a Py>, Python<'py>); -impl<'py, T> Borrowed<'_, 'py, T> { +impl<'a, 'py, T> Borrowed<'a, 'py, T> { /// Creates a new owned [`Bound`] from this borrowed reference by /// increasing the reference count. /// @@ -603,6 +675,10 @@ impl<'py, T> Borrowed<'_, 'py, T> { pub fn to_owned(self) -> Bound<'py, T> { (*self).clone() } + + pub(crate) fn to_any(self) -> Borrowed<'a, 'py, PyAny> { + Borrowed(self.0, PhantomData, self.2) + } } impl<'a, 'py> Borrowed<'a, 'py, PyAny> { @@ -744,6 +820,30 @@ impl IntoPy for Borrowed<'_, '_, T> { } } +impl<'a, 'py, T> BoundObject<'py, T> for Borrowed<'a, 'py, T> { + type Any = Borrowed<'a, 'py, PyAny>; + + fn as_borrowed(&self) -> Borrowed<'a, 'py, T> { + *self + } + + fn into_bound(self) -> Bound<'py, T> { + (*self).to_owned() + } + + fn into_any(self) -> Self::Any { + self.to_any() + } + + fn into_ptr(self) -> *mut ffi::PyObject { + (*self).to_owned().into_ptr() + } + + fn unbind(self) -> Py { + (*self).to_owned().unbind() + } +} + /// A GIL-independent reference to an object allocated on the Python heap. /// /// This type does not auto-dereference to the inner object because you must prove you hold the GIL to access it. diff --git a/src/lib.rs b/src/lib.rs index 12a0ff9916f..82f1d271cf7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -320,7 +320,7 @@ pub use crate::conversion::{AsPyPointer, FromPyObject, IntoPy, ToPyObject}; pub use crate::err::{DowncastError, DowncastIntoError, PyErr, PyErrArguments, PyResult, ToPyErr}; #[cfg(not(any(PyPy, GraalPy)))] pub use crate::gil::{prepare_freethreaded_python, with_embedded_python_interpreter}; -pub use crate::instance::{Borrowed, Bound, Py, PyObject}; +pub use crate::instance::{Borrowed, Bound, BoundObject, Py, PyObject}; pub use crate::marker::Python; pub use crate::pycell::{PyRef, PyRefMut}; pub use crate::pyclass::PyClass; diff --git a/src/pycell.rs b/src/pycell.rs index 6b501ad0401..0a05acd0f02 100644 --- a/src/pycell.rs +++ b/src/pycell.rs @@ -193,13 +193,14 @@ //! [guide]: https://pyo3.rs/latest/class.html#pycell-and-interior-mutability "PyCell and interior mutability" //! [Interior Mutability]: https://doc.rust-lang.org/book/ch15-05-interior-mutability.html "RefCell and the Interior Mutability Pattern - The Rust Programming Language" -use crate::conversion::AsPyPointer; +use crate::conversion::{AsPyPointer, IntoPyObject}; use crate::exceptions::PyRuntimeError; use crate::ffi_ptr_ext::FfiPtrExt; use crate::internal_tricks::{ptr_from_mut, ptr_from_ref}; use crate::pyclass::{boolean_struct::False, PyClass}; use crate::types::any::PyAnyMethods; use crate::{ffi, Bound, IntoPy, PyErr, PyObject, Python}; +use std::convert::Infallible; use std::fmt; use std::mem::ManuallyDrop; use std::ops::{Deref, DerefMut}; @@ -466,6 +467,26 @@ impl IntoPy for &'_ PyRef<'_, T> { } } +impl<'py, T: PyClass> IntoPyObject<'py> for PyRef<'py, T> { + type Target = T; + type Output = Bound<'py, T>; + type Error = Infallible; + + fn into_pyobject(self, _py: Python<'py>) -> Result { + Ok(self.inner.clone()) + } +} + +impl<'a, 'py, T: PyClass> IntoPyObject<'py> for &'a PyRef<'py, T> { + type Target = T; + type Output = &'a Bound<'py, T>; + type Error = Infallible; + + fn into_pyobject(self, _py: Python<'py>) -> Result { + Ok(&self.inner) + } +} + unsafe impl<'a, T: PyClass> AsPyPointer for PyRef<'a, T> { fn as_ptr(&self) -> *mut ffi::PyObject { self.inner.as_ptr() @@ -635,9 +656,23 @@ impl> IntoPy for &'_ PyRefMut<'_, T> { } } -unsafe impl<'a, T: PyClass> AsPyPointer for PyRefMut<'a, T> { - fn as_ptr(&self) -> *mut ffi::PyObject { - self.inner.as_ptr() +impl<'py, T: PyClass> IntoPyObject<'py> for PyRefMut<'py, T> { + type Target = T; + type Output = Bound<'py, T>; + type Error = Infallible; + + fn into_pyobject(self, _py: Python<'py>) -> Result { + Ok(self.inner.clone()) + } +} + +impl<'a, 'py, T: PyClass> IntoPyObject<'py> for &'a PyRefMut<'py, T> { + type Target = T; + type Output = &'a Bound<'py, T>; + type Error = Infallible; + + fn into_pyobject(self, _py: Python<'py>) -> Result { + Ok(&self.inner) } } diff --git a/src/types/list.rs b/src/types/list.rs index f12a621e95c..8f8bd2eb1a4 100644 --- a/src/types/list.rs +++ b/src/types/list.rs @@ -29,6 +29,15 @@ pub(crate) fn new_from_iter<'py>( py: Python<'py>, elements: &mut dyn ExactSizeIterator, ) -> Bound<'py, PyList> { + try_new_from_iter(py, &mut elements.map(Ok)).unwrap() +} + +#[inline] +#[track_caller] +pub(crate) fn try_new_from_iter<'py>( + py: Python<'py>, + elements: &mut dyn ExactSizeIterator>, +) -> PyResult> { unsafe { // PyList_New checks for overflow but has a bad error message, so we check ourselves let len: Py_ssize_t = elements @@ -47,16 +56,16 @@ pub(crate) fn new_from_iter<'py>( for obj in elements.take(len as usize) { #[cfg(not(Py_LIMITED_API))] - ffi::PyList_SET_ITEM(ptr, counter, obj.into_ptr()); + ffi::PyList_SET_ITEM(ptr, counter, obj?.into_ptr()); #[cfg(Py_LIMITED_API)] - ffi::PyList_SetItem(ptr, counter, obj.into_ptr()); + ffi::PyList_SetItem(ptr, counter, obj?.into_ptr()); counter += 1; } assert!(elements.next().is_none(), "Attempted to create PyList but `elements` was larger than reported by its `ExactSizeIterator` implementation."); assert_eq!(len, counter, "Attempted to create PyList but `elements` was smaller than reported by its `ExactSizeIterator` implementation."); - list + Ok(list) } } diff --git a/src/types/set.rs b/src/types/set.rs index d1b514264d7..4f04a4e4e8f 100644 --- a/src/types/set.rs +++ b/src/types/set.rs @@ -248,27 +248,29 @@ pub(crate) fn new_from_iter( py: Python<'_>, elements: impl IntoIterator, ) -> PyResult> { - fn inner<'py>( - py: Python<'py>, - elements: &mut dyn Iterator, - ) -> PyResult> { - let set = unsafe { - // We create the `Py` pointer because its Drop cleans up the set if user code panics. - ffi::PySet_New(std::ptr::null_mut()) - .assume_owned_or_err(py)? - .downcast_into_unchecked() - }; - let ptr = set.as_ptr(); + let mut iter = elements.into_iter().map(|e| Ok(e.to_object(py))); + try_new_from_iter(py, &mut iter) +} - for obj in elements { - err::error_on_minusone(py, unsafe { ffi::PySet_Add(ptr, obj.as_ptr()) })?; - } +#[inline] +pub(crate) fn try_new_from_iter( + py: Python<'_>, + elements: impl IntoIterator>, +) -> PyResult> { + let set = unsafe { + // We create the `Bound` pointer because its Drop cleans up the set if + // user code errors or panics. + ffi::PySet_New(std::ptr::null_mut()) + .assume_owned_or_err(py)? + .downcast_into_unchecked() + }; + let ptr = set.as_ptr(); - Ok(set) + for obj in elements { + err::error_on_minusone(py, unsafe { ffi::PySet_Add(ptr, obj?.as_ptr()) })?; } - let mut iter = elements.into_iter().map(|e| e.to_object(py)); - inner(py, &mut iter) + Ok(set) } #[cfg(test)] diff --git a/tests/ui/missing_intopy.rs b/tests/ui/missing_intopy.rs index 7a465b14221..36de7bdee78 100644 --- a/tests/ui/missing_intopy.rs +++ b/tests/ui/missing_intopy.rs @@ -1,8 +1,8 @@ struct Blah; #[pyo3::pyfunction] -fn blah() -> Blah{ +fn blah() -> Blah { Blah } -fn main(){} \ No newline at end of file +fn main() {} diff --git a/tests/ui/missing_intopy.stderr b/tests/ui/missing_intopy.stderr index e781b38fc86..e0818ec42ef 100644 --- a/tests/ui/missing_intopy.stderr +++ b/tests/ui/missing_intopy.stderr @@ -1,11 +1,36 @@ error[E0277]: `Blah` cannot be converted to a Python object --> tests/ui/missing_intopy.rs:4:14 | -4 | fn blah() -> Blah{ - | ^^^^ the trait `IntoPy>` is not implemented for `Blah`, which is required by `Blah: OkWrap<_>` +4 | fn blah() -> Blah { + | ^^^^ the trait `IntoPyObject<'_>` is not implemented for `Blah` | - = note: `IntoPy` is automatically implemented by the `#[pyclass]` macro - = note: if you do not wish to have a corresponding Python type, implement `IntoPy` manually + = note: `IntoPyObject` is automatically implemented by the `#[pyclass]` macro + = note: if you do not wish to have a corresponding Python type, implement it manually = note: if you do not own `Blah` you can perform a manual conversion to one of the types in `pyo3::types::*` - = help: the trait `OkWrap` is implemented for `Result` - = note: required for `Blah` to implement `OkWrap` + = help: the following other types implement trait `IntoPyObject<'py>`: + &'a Py + &'a PyRef<'py, T> + &'a PyRefMut<'py, T> + &'a pyo3::Bound<'py, T> + &OsStr + &OsString + &Path + &PathBuf + and $N others +note: required by a bound in `UnknownReturnType::::wrap` + --> src/impl_/wrap.rs + | + | pub fn wrap<'py>(&self, _: T) -> T + | ---- required by a bound in this associated function + | where + | T: IntoPyObject<'py>, + | ^^^^^^^^^^^^^^^^^ required by this bound in `UnknownReturnType::::wrap` + +error[E0599]: no method named `map_err` found for struct `Blah` in the current scope + --> tests/ui/missing_intopy.rs:4:14 + | +1 | struct Blah; + | ----------- method `map_err` not found for this struct +... +4 | fn blah() -> Blah { + | ^^^^ method not found in `Blah` From ba93835311bae5b5ac23e18f3e90b87a2dc89d96 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Fri, 9 Aug 2024 20:38:06 +0100 Subject: [PATCH 198/495] ci: pin `zope.interface<7` (#4429) * ci: pin `zope.interface<7` * ci: explicit `cargo careful` setup * ci: build `cargo-careful` from source while waiting for updated binary --- .github/workflows/ci.yml | 4 +++- noxfile.py | 2 ++ pytests/noxfile.py | 5 ++++- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c8f4b5dcc67..a1b2ddbf51b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -349,7 +349,9 @@ jobs: - uses: dtolnay/rust-toolchain@nightly with: components: rust-src - - uses: taiki-e/install-action@cargo-careful + # FIXME: workaround https://github.com/RalfJung/cargo-careful/issues/36 + # - uses: taiki-e/install-action@cargo-careful + - run: cargo install cargo-careful - run: python -m pip install --upgrade pip && pip install nox - run: nox -s test-rust -- careful skip-full env: diff --git a/noxfile.py b/noxfile.py index 0267531cb7d..7f012ce2fc5 100644 --- a/noxfile.py +++ b/noxfile.py @@ -869,6 +869,8 @@ def _run_cargo_test( ) -> None: command = ["cargo"] if "careful" in session.posargs: + # do explicit setup so failures in setup can be seen + _run_cargo(session, "careful", "setup") command.append("careful") command.extend(("test", "--no-fail-fast")) if "release" in session.posargs: diff --git a/pytests/noxfile.py b/pytests/noxfile.py index af2eb0d3a75..f5a332c7a3c 100644 --- a/pytests/noxfile.py +++ b/pytests/noxfile.py @@ -12,12 +12,15 @@ def test(session: nox.Session): def try_install_binary(package: str, constraint: str): try: - session.install(f"--only-binary={package}", f"{package}{constraint}") + session.install("--only-binary=:all:", f"{package}{constraint}") except CommandFailed: # No binary wheel available on this platform pass try_install_binary("numpy", ">=1.16") + # https://github.com/zopefoundation/zope.interface/issues/316 + # - is a dependency of gevent + try_install_binary("zope.interface", "<7") try_install_binary("gevent", ">=22.10.2") ignored_paths = [] if sys.version_info < (3, 10): From 847fba47e4c4afd691623db32a6b08d32ae79001 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Fri, 9 Aug 2024 14:26:20 -0600 Subject: [PATCH 199/495] Update test_misc.py for 3.13 subinterpreter changes (#4426) --- pytests/tests/test_misc.py | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/pytests/tests/test_misc.py b/pytests/tests/test_misc.py index 7f43fbf11e0..dd7f8007e81 100644 --- a/pytests/tests/test_misc.py +++ b/pytests/tests/test_misc.py @@ -6,7 +6,7 @@ import pytest if sys.version_info >= (3, 13): - subinterpreters = pytest.importorskip("subinterpreters") + subinterpreters = pytest.importorskip("_interpreters") else: subinterpreters = pytest.importorskip("_xxsubinterpreters") @@ -36,17 +36,27 @@ def test_multiple_imports_same_interpreter_ok(): reason="PyPy and GraalPy do not support subinterpreters", ) def test_import_in_subinterpreter_forbidden(): + sub_interpreter = subinterpreters.create() if sys.version_info < (3, 12): expected_error = "PyO3 modules do not yet support subinterpreters, see https://github.com/PyO3/pyo3/issues/576" else: expected_error = "module pyo3_pytests.pyo3_pytests does not support loading in subinterpreters" - sub_interpreter = subinterpreters.create() - with pytest.raises( - subinterpreters.RunFailedError, - match=expected_error, - ): - subinterpreters.run_string(sub_interpreter, "import pyo3_pytests.pyo3_pytests") + if sys.version_info < (3, 13): + # Python 3.12 subinterpreters had a special error for this + with pytest.raises( + subinterpreters.RunFailedError, + match=expected_error, + ): + subinterpreters.run_string( + sub_interpreter, "import pyo3_pytests.pyo3_pytests" + ) + else: + res = subinterpreters.run_string( + sub_interpreter, "import pyo3_pytests.pyo3_pytests" + ) + assert res.type.__name__ == "ImportError" + assert res.msg == expected_error subinterpreters.destroy(sub_interpreter) From 26d28831b7a8ccaa5843bd6db7aa0c72237ec5d9 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Fri, 9 Aug 2024 21:26:32 +0100 Subject: [PATCH 200/495] ffi: update `modsupport.rs` for Python 3.13 (#4420) --- newsfragments/4420.fixed.md | 1 + newsfragments/4420.removed.md | 1 + pyo3-ffi/src/modsupport.rs | 39 +++++++-------------------------- tests/test_pyfunction.rs | 41 +++++++++++++++++++++-------------- 4 files changed, 35 insertions(+), 47 deletions(-) create mode 100644 newsfragments/4420.fixed.md create mode 100644 newsfragments/4420.removed.md diff --git a/newsfragments/4420.fixed.md b/newsfragments/4420.fixed.md new file mode 100644 index 00000000000..dec974555d9 --- /dev/null +++ b/newsfragments/4420.fixed.md @@ -0,0 +1 @@ +Correct FFI definition `PyArg_ParseTupleAndKeywords` to take `*const *const c_char` instead of `*mut *mut c_char` on Python 3.13 and up. diff --git a/newsfragments/4420.removed.md b/newsfragments/4420.removed.md new file mode 100644 index 00000000000..9d17c33e143 --- /dev/null +++ b/newsfragments/4420.removed.md @@ -0,0 +1 @@ +Remove FFI definition of private variable `_Py_PackageContext`. diff --git a/pyo3-ffi/src/modsupport.rs b/pyo3-ffi/src/modsupport.rs index b259c70059e..4a18d30f97c 100644 --- a/pyo3-ffi/src/modsupport.rs +++ b/pyo3-ffi/src/modsupport.rs @@ -14,9 +14,14 @@ extern "C" { arg1: *mut PyObject, arg2: *mut PyObject, arg3: *const c_char, - arg4: *mut *mut c_char, + #[cfg(not(Py_3_13))] arg4: *mut *mut c_char, + #[cfg(Py_3_13)] arg4: *const *const c_char, ... ) -> c_int; + + // skipped PyArg_VaParse + // skipped PyArg_VaParseTupleAndKeywords + pub fn PyArg_ValidateKeywordArguments(arg1: *mut PyObject) -> c_int; #[cfg_attr(PyPy, link_name = "PyPyArg_UnpackTuple")] pub fn PyArg_UnpackTuple( @@ -26,32 +31,10 @@ extern "C" { arg4: Py_ssize_t, ... ) -> c_int; + #[cfg_attr(PyPy, link_name = "PyPy_BuildValue")] pub fn Py_BuildValue(arg1: *const c_char, ...) -> *mut PyObject; - // #[cfg_attr(PyPy, link_name = "_PyPy_BuildValue_SizeT")] - //pub fn _Py_BuildValue_SizeT(arg1: *const c_char, ...) - // -> *mut PyObject; - // #[cfg_attr(PyPy, link_name = "PyPy_VaBuildValue")] - - // skipped non-limited _PyArg_UnpackStack - // skipped non-limited _PyArg_NoKeywords - // skipped non-limited _PyArg_NoKwnames - // skipped non-limited _PyArg_NoPositional - // skipped non-limited _PyArg_BadArgument - // skipped non-limited _PyArg_CheckPositional - - //pub fn Py_VaBuildValue(arg1: *const c_char, arg2: va_list) - // -> *mut PyObject; - - // skipped non-limited _Py_VaBuildStack - // skipped non-limited _PyArg_Parser - - // skipped non-limited _PyArg_ParseTupleAndKeywordsFast - // skipped non-limited _PyArg_ParseStack - // skipped non-limited _PyArg_ParseStackAndKeywords - // skipped non-limited _PyArg_VaParseTupleAndKeywordsFast - // skipped non-limited _PyArg_UnpackKeywords - // skipped non-limited _PyArg_Fini + // skipped Py_VaBuildValue #[cfg(Py_3_10)] #[cfg_attr(PyPy, link_name = "PyPyModule_AddObjectRef")] @@ -159,9 +142,3 @@ pub unsafe fn PyModule_FromDefAndSpec(def: *mut PyModuleDef, spec: *mut PyObject }, ) } - -#[cfg(not(Py_LIMITED_API))] -#[cfg_attr(windows, link(name = "pythonXY"))] -extern "C" { - pub static mut _Py_PackageContext: *const c_char; -} diff --git a/tests/test_pyfunction.rs b/tests/test_pyfunction.rs index 903db689527..24ef32d6994 100644 --- a/tests/test_pyfunction.rs +++ b/tests/test_pyfunction.rs @@ -363,8 +363,7 @@ fn test_pycfunction_new() { #[test] fn test_pycfunction_new_with_keywords() { use pyo3::ffi; - use std::ffi::CString; - use std::os::raw::{c_char, c_long}; + use std::os::raw::c_long; use std::ptr; Python::with_gil(|py| { @@ -375,28 +374,38 @@ fn test_pycfunction_new_with_keywords() { ) -> *mut ffi::PyObject { let mut foo: c_long = 0; let mut bar: c_long = 0; - let foo_ptr: *mut c_long = &mut foo; - let bar_ptr: *mut c_long = &mut bar; - let foo_name = CString::new("foo").unwrap(); - let foo_name_raw: *mut c_char = foo_name.into_raw(); - let kw_bar_name = CString::new("kw_bar").unwrap(); - let kw_bar_name_raw: *mut c_char = kw_bar_name.into_raw(); + #[cfg(not(Py_3_13))] + let foo_name = std::ffi::CString::new("foo").unwrap(); + #[cfg(not(Py_3_13))] + let kw_bar_name = std::ffi::CString::new("kw_bar").unwrap(); + #[cfg(not(Py_3_13))] + let mut args_names = [foo_name.into_raw(), kw_bar_name.into_raw(), ptr::null_mut()]; - let mut arglist = vec![foo_name_raw, kw_bar_name_raw, ptr::null_mut()]; - let arglist_ptr: *mut *mut c_char = arglist.as_mut_ptr(); - - let arg_pattern: *const c_char = CString::new("l|l").unwrap().into_raw(); + #[cfg(Py_3_13)] + let args_names = [ + c_str!("foo").as_ptr(), + c_str!("kw_bar").as_ptr(), + ptr::null_mut(), + ]; ffi::PyArg_ParseTupleAndKeywords( args, kwds, - arg_pattern, - arglist_ptr, - foo_ptr, - bar_ptr, + c_str!("l|l").as_ptr(), + #[cfg(Py_3_13)] + args_names.as_ptr(), + #[cfg(not(Py_3_13))] + args_names.as_mut_ptr(), + &mut foo, + &mut bar, ); + #[cfg(not(Py_3_13))] + drop(std::ffi::CString::from_raw(args_names[0])); + #[cfg(not(Py_3_13))] + drop(std::ffi::CString::from_raw(args_names[1])); + ffi::PyLong_FromLong(foo * bar) } From be8ab5fe794a7e59699e76eac1fb2607ccc75279 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Fri, 9 Aug 2024 14:26:53 -0600 Subject: [PATCH 201/495] Add PyList_GetItemRef and use it in list.get_item (#4410) * Add PyList_GetItemRef bindings and compat shim * Use PyList_GetItemRef in list.get_item() * add release notes * apply code review comments * Update newsfragments/4410.added.md Co-authored-by: David Hewitt * apply `all()` doc cfg hints --------- Co-authored-by: David Hewitt --- newsfragments/4410.added.md | 1 + newsfragments/4410.fixed.md | 3 +++ pyo3-ffi/src/compat.rs | 20 ++++++++++++++++++-- pyo3-ffi/src/listobject.rs | 2 ++ src/types/list.rs | 7 ++----- 5 files changed, 26 insertions(+), 7 deletions(-) create mode 100644 newsfragments/4410.added.md create mode 100644 newsfragments/4410.fixed.md diff --git a/newsfragments/4410.added.md b/newsfragments/4410.added.md new file mode 100644 index 00000000000..350a54531bd --- /dev/null +++ b/newsfragments/4410.added.md @@ -0,0 +1 @@ +Add ffi binding `PyList_GetItemRef` and `pyo3_ffi::compat::PyList_GetItemRef` diff --git a/newsfragments/4410.fixed.md b/newsfragments/4410.fixed.md new file mode 100644 index 00000000000..f4403409aea --- /dev/null +++ b/newsfragments/4410.fixed.md @@ -0,0 +1,3 @@ +* Avoid creating temporary borrowed reference in list.get_item + bindings. Temporary borrowed references are unsafe in the free-threaded python + build if the list is shared between threads. diff --git a/pyo3-ffi/src/compat.rs b/pyo3-ffi/src/compat.rs index ef9a3fbe1c9..85986817d93 100644 --- a/pyo3-ffi/src/compat.rs +++ b/pyo3-ffi/src/compat.rs @@ -12,13 +12,15 @@ #[cfg(not(Py_3_13))] use crate::object::PyObject; #[cfg(not(Py_3_13))] +use crate::pyport::Py_ssize_t; +#[cfg(not(Py_3_13))] use std::os::raw::c_int; -#[cfg_attr(docsrs, doc(cfg(all)))] +#[cfg_attr(docsrs, doc(cfg(all())))] #[cfg(Py_3_13)] pub use crate::dictobject::PyDict_GetItemRef; -#[cfg_attr(docsrs, doc(cfg(all)))] +#[cfg_attr(docsrs, doc(cfg(all())))] #[cfg(not(Py_3_13))] pub unsafe fn PyDict_GetItemRef( dp: *mut PyObject, @@ -42,3 +44,17 @@ pub unsafe fn PyDict_GetItemRef( -1 } } + +#[cfg_attr(docsrs, doc(cfg(all())))] +#[cfg(Py_3_13)] +pub use crate::PyList_GetItemRef; + +#[cfg(not(Py_3_13))] +#[cfg_attr(docsrs, doc(cfg(all())))] +pub unsafe fn PyList_GetItemRef(arg1: *mut PyObject, arg2: Py_ssize_t) -> *mut PyObject { + use crate::{PyList_GetItem, Py_XINCREF}; + + let item: *mut PyObject = PyList_GetItem(arg1, arg2); + Py_XINCREF(item); + item +} diff --git a/pyo3-ffi/src/listobject.rs b/pyo3-ffi/src/listobject.rs index a6f47eadf83..1096f2fe0c8 100644 --- a/pyo3-ffi/src/listobject.rs +++ b/pyo3-ffi/src/listobject.rs @@ -28,6 +28,8 @@ extern "C" { pub fn PyList_Size(arg1: *mut PyObject) -> Py_ssize_t; #[cfg_attr(PyPy, link_name = "PyPyList_GetItem")] pub fn PyList_GetItem(arg1: *mut PyObject, arg2: Py_ssize_t) -> *mut PyObject; + #[cfg(Py_3_13)] + pub fn PyList_GetItemRef(arg1: *mut PyObject, arg2: Py_ssize_t) -> *mut PyObject; #[cfg_attr(PyPy, link_name = "PyPyList_SetItem")] pub fn PyList_SetItem(arg1: *mut PyObject, arg2: Py_ssize_t, arg3: *mut PyObject) -> c_int; #[cfg_attr(PyPy, link_name = "PyPyList_Insert")] diff --git a/src/types/list.rs b/src/types/list.rs index 8f8bd2eb1a4..66e5f4661a3 100644 --- a/src/types/list.rs +++ b/src/types/list.rs @@ -3,7 +3,6 @@ use std::iter::FusedIterator; use crate::err::{self, PyResult}; use crate::ffi::{self, Py_ssize_t}; use crate::ffi_ptr_ext::FfiPtrExt; -use crate::instance::Borrowed; use crate::internal_tricks::get_ssize_index; use crate::types::{PySequence, PyTuple}; use crate::{Bound, PyAny, PyObject, Python, ToPyObject}; @@ -289,10 +288,8 @@ impl<'py> PyListMethods<'py> for Bound<'py, PyList> { /// ``` fn get_item(&self, index: usize) -> PyResult> { unsafe { - // PyList_GetItem return borrowed ptr; must make owned for safety (see #890). - ffi::PyList_GetItem(self.as_ptr(), index as Py_ssize_t) - .assume_borrowed_or_err(self.py()) - .map(Borrowed::to_owned) + ffi::compat::PyList_GetItemRef(self.as_ptr(), index as Py_ssize_t) + .assume_owned_or_err(self.py()) } } From cdc728ad22b4779e0b65ab3ebc073cbc382c395a Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Sat, 10 Aug 2024 07:21:58 +0100 Subject: [PATCH 202/495] ci: install cargo-codspeed binary (#4430) --- .github/workflows/benches.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/benches.yml b/.github/workflows/benches.yml index 29bdabf5f7c..04b362947f5 100644 --- a/.github/workflows/benches.yml +++ b/.github/workflows/benches.yml @@ -30,8 +30,9 @@ jobs: pyo3-benches continue-on-error: true - - name: Install cargo-codspeed - run: cargo install cargo-codspeed + - uses: taiki-e/install-action@v2 + with: + tool: cargo-codspeed - name: Install nox run: pip install nox From cc4b1083a333e1b25a55a99c534aa9b60f882cf3 Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Sat, 10 Aug 2024 20:44:53 +0200 Subject: [PATCH 203/495] reintroduce `PyString` constructors (#4431) --- guide/src/conversions/traits.md | 4 +-- guide/src/types.md | 4 +-- src/conversion.rs | 2 +- src/conversions/std/ipaddr.rs | 4 +-- src/conversions/std/string.rs | 24 +++++++-------- src/err/mod.rs | 2 +- src/ffi/tests.rs | 6 ++-- src/instance.rs | 8 ++--- src/marker.rs | 4 +-- src/pybacked.rs | 26 ++++++++-------- src/sync.rs | 2 +- src/types/any.rs | 2 +- src/types/string.rs | 53 ++++++++++++++++++++++++--------- src/types/typeobject.rs | 2 +- tests/test_pyself.rs | 2 +- tests/ui/not_send2.rs | 2 +- 16 files changed, 86 insertions(+), 61 deletions(-) diff --git a/guide/src/conversions/traits.md b/guide/src/conversions/traits.md index 8c813c8fe1b..5e8c3fe4202 100644 --- a/guide/src/conversions/traits.md +++ b/guide/src/conversions/traits.md @@ -236,7 +236,7 @@ struct RustyTransparentStruct { # use pyo3::types::PyString; # fn main() -> PyResult<()> { # Python::with_gil(|py| -> PyResult<()> { -# let s = PyString::new_bound(py, "test"); +# let s = PyString::new(py, "test"); # # let tup: RustyTransparentTupleStruct = s.extract()?; # assert_eq!(tup.0, "test"); @@ -303,7 +303,7 @@ enum RustyEnum<'py> { # ); # } # { -# let thing = PyString::new_bound(py, "text"); +# let thing = PyString::new(py, "text"); # let rust_thing: RustyEnum<'_> = thing.extract()?; # # assert_eq!( diff --git a/guide/src/types.md b/guide/src/types.md index bbeb1dae9df..b975e8400a7 100644 --- a/guide/src/types.md +++ b/guide/src/types.md @@ -112,7 +112,7 @@ fn add<'py>( left.add(right) } # Python::with_gil(|py| { -# let s = pyo3::types::PyString::new_bound(py, "s"); +# let s = pyo3::types::PyString::new(py, "s"); # assert!(add(&s, &s).unwrap().eq("ss").unwrap()); # }) ``` @@ -126,7 +126,7 @@ fn add(left: &Bound<'_, PyAny>, right: &Bound<'_, PyAny>) -> PyResult Ok(output.unbind()) } # Python::with_gil(|py| { -# let s = pyo3::types::PyString::new_bound(py, "s"); +# let s = pyo3::types::PyString::new(py, "s"); # assert!(add(&s, &s).unwrap().bind(py).eq("ss").unwrap()); # }) ``` diff --git a/src/conversion.rs b/src/conversion.rs index bb4bc51fd07..a5844bd5140 100644 --- a/src/conversion.rs +++ b/src/conversion.rs @@ -266,7 +266,7 @@ impl<'a, 'py, T> IntoPyObject<'py> for &'a Py { /// # fn main() -> PyResult<()> { /// Python::with_gil(|py| { /// // Calling `.extract()` on a `Bound` smart pointer -/// let obj: Bound<'_, PyString> = PyString::new_bound(py, "blah"); +/// let obj: Bound<'_, PyString> = PyString::new(py, "blah"); /// let s: String = obj.extract()?; /// # assert_eq!(s, "blah"); /// diff --git a/src/conversions/std/ipaddr.rs b/src/conversions/std/ipaddr.rs index 688ec78ba1a..a7fd7fde241 100755 --- a/src/conversions/std/ipaddr.rs +++ b/src/conversions/std/ipaddr.rs @@ -145,11 +145,11 @@ mod test_ipaddr { #[test] fn test_from_pystring() { Python::with_gil(|py| { - let py_str = PyString::new_bound(py, "0:0:0:0:0:0:0:1"); + let py_str = PyString::new(py, "0:0:0:0:0:0:0:1"); let ip: IpAddr = py_str.to_object(py).extract(py).unwrap(); assert_eq!(ip, IpAddr::from_str("::1").unwrap()); - let py_str = PyString::new_bound(py, "invalid"); + let py_str = PyString::new(py, "invalid"); assert!(py_str.to_object(py).extract::(py).is_err()); }); } diff --git a/src/conversions/std/string.rs b/src/conversions/std/string.rs index 87408030182..92d0f13babe 100644 --- a/src/conversions/std/string.rs +++ b/src/conversions/std/string.rs @@ -14,14 +14,14 @@ use crate::{ impl ToPyObject for str { #[inline] fn to_object(&self, py: Python<'_>) -> PyObject { - PyString::new_bound(py, self).into() + PyString::new(py, self).into() } } impl<'a> IntoPy for &'a str { #[inline] fn into_py(self, py: Python<'_>) -> PyObject { - PyString::new_bound(py, self).into() + PyString::new(py, self).into() } #[cfg(feature = "experimental-inspect")] @@ -33,7 +33,7 @@ impl<'a> IntoPy for &'a str { impl<'a> IntoPy> for &'a str { #[inline] fn into_py(self, py: Python<'_>) -> Py { - PyString::new_bound(py, self).into() + PyString::new(py, self).into() } #[cfg(feature = "experimental-inspect")] @@ -48,7 +48,7 @@ impl<'py> IntoPyObject<'py> for &str { type Error = Infallible; fn into_pyobject(self, py: Python<'py>) -> Result { - Ok(PyString::new_bound(py, self)) + Ok(PyString::new(py, self)) } } @@ -57,7 +57,7 @@ impl<'py> IntoPyObject<'py> for &str { impl ToPyObject for Cow<'_, str> { #[inline] fn to_object(&self, py: Python<'_>) -> PyObject { - PyString::new_bound(py, self).into() + PyString::new(py, self).into() } } @@ -88,7 +88,7 @@ impl<'py> IntoPyObject<'py> for Cow<'_, str> { impl ToPyObject for String { #[inline] fn to_object(&self, py: Python<'_>) -> PyObject { - PyString::new_bound(py, self).into() + PyString::new(py, self).into() } } @@ -101,7 +101,7 @@ impl ToPyObject for char { impl IntoPy for char { fn into_py(self, py: Python<'_>) -> PyObject { let mut bytes = [0u8; 4]; - PyString::new_bound(py, self.encode_utf8(&mut bytes)).into() + PyString::new(py, self.encode_utf8(&mut bytes)).into() } #[cfg(feature = "experimental-inspect")] @@ -117,13 +117,13 @@ impl<'py> IntoPyObject<'py> for char { fn into_pyobject(self, py: Python<'py>) -> Result { let mut bytes = [0u8; 4]; - Ok(PyString::new_bound(py, self.encode_utf8(&mut bytes))) + Ok(PyString::new(py, self.encode_utf8(&mut bytes))) } } impl IntoPy for String { fn into_py(self, py: Python<'_>) -> PyObject { - PyString::new_bound(py, &self).into() + PyString::new(py, &self).into() } #[cfg(feature = "experimental-inspect")] @@ -138,14 +138,14 @@ impl<'py> IntoPyObject<'py> for String { type Error = Infallible; fn into_pyobject(self, py: Python<'py>) -> Result { - Ok(PyString::new_bound(py, &self)) + Ok(PyString::new(py, &self)) } } impl<'a> IntoPy for &'a String { #[inline] fn into_py(self, py: Python<'_>) -> PyObject { - PyString::new_bound(py, self).into() + PyString::new(py, self).into() } #[cfg(feature = "experimental-inspect")] @@ -160,7 +160,7 @@ impl<'py> IntoPyObject<'py> for &String { type Error = Infallible; fn into_pyobject(self, py: Python<'py>) -> Result { - Ok(PyString::new_bound(py, self)) + Ok(PyString::new(py, self)) } } diff --git a/src/err/mod.rs b/src/err/mod.rs index b51b9defc38..69180cca7b5 100644 --- a/src/err/mod.rs +++ b/src/err/mod.rs @@ -207,7 +207,7 @@ impl PyErr { /// assert_eq!(err.to_string(), "TypeError: "); /// /// // Case #3: Invalid exception value - /// let err = PyErr::from_value_bound(PyString::new_bound(py, "foo").into_any()); + /// let err = PyErr::from_value_bound(PyString::new(py, "foo").into_any()); /// assert_eq!( /// err.to_string(), /// "TypeError: exceptions must derive from BaseException" diff --git a/src/ffi/tests.rs b/src/ffi/tests.rs index f2629ea5667..63a7a4ef352 100644 --- a/src/ffi/tests.rs +++ b/src/ffi/tests.rs @@ -99,7 +99,7 @@ fn test_timezone_from_offset_and_name() { Python::with_gil(|py| { let delta = PyDelta::new_bound(py, 0, 100, 0, false).unwrap(); - let tzname = PyString::new_bound(py, "testtz"); + let tzname = PyString::new(py, "testtz"); let tz = unsafe { PyTimeZone_FromOffsetAndName(delta.as_ptr(), tzname.as_ptr()).assume_owned(py) }; @@ -164,7 +164,7 @@ fn ascii_object_bitfield() { fn ascii() { Python::with_gil(|py| { // This test relies on implementation details of PyString. - let s = PyString::new_bound(py, "hello, world"); + let s = PyString::new(py, "hello, world"); let ptr = s.as_ptr(); unsafe { @@ -205,7 +205,7 @@ fn ascii() { fn ucs4() { Python::with_gil(|py| { let s = "哈哈🐈"; - let py_string = PyString::new_bound(py, s); + let py_string = PyString::new(py, s); let ptr = py_string.as_ptr(); unsafe { diff --git a/src/instance.rs b/src/instance.rs index 1adc70d6fa4..96b3f3e554c 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -1981,7 +1981,7 @@ a = A() assert!(instance .getattr(py, "foo")? .bind(py) - .eq(PyString::new_bound(py, "bar"))?); + .eq(PyString::new(py, "bar"))?); instance.getattr(py, "foo")?; Ok(()) @@ -2067,7 +2067,7 @@ a = A() #[test] fn test_bound_as_any() { Python::with_gil(|py| { - let obj = PyString::new_bound(py, "hello world"); + let obj = PyString::new(py, "hello world"); let any = obj.as_any(); assert_eq!(any.as_ptr(), obj.as_ptr()); }); @@ -2076,7 +2076,7 @@ a = A() #[test] fn test_bound_into_any() { Python::with_gil(|py| { - let obj = PyString::new_bound(py, "hello world"); + let obj = PyString::new(py, "hello world"); let any = obj.clone().into_any(); assert_eq!(any.as_ptr(), obj.as_ptr()); }); @@ -2085,7 +2085,7 @@ a = A() #[test] fn test_bound_py_conversions() { Python::with_gil(|py| { - let obj: Bound<'_, PyString> = PyString::new_bound(py, "hello world"); + let obj: Bound<'_, PyString> = PyString::new(py, "hello world"); let obj_unbound: &Py = obj.as_unbound(); let _: &Bound<'_, PyString> = obj_unbound.bind(py); diff --git a/src/marker.rs b/src/marker.rs index 67a7381974f..84ad22e931c 100644 --- a/src/marker.rs +++ b/src/marker.rs @@ -73,7 +73,7 @@ //! use send_wrapper::SendWrapper; //! //! Python::with_gil(|py| { -//! let string = PyString::new_bound(py, "foo"); +//! let string = PyString::new(py, "foo"); //! //! let wrapped = SendWrapper::new(string); //! @@ -167,7 +167,7 @@ use std::os::raw::c_int; /// use send_wrapper::SendWrapper; /// /// Python::with_gil(|py| { -/// let string = PyString::new_bound(py, "foo"); +/// let string = PyString::new(py, "foo"); /// /// let wrapped = SendWrapper::new(string); /// diff --git a/src/pybacked.rs b/src/pybacked.rs index f9249978b5f..b50416062b5 100644 --- a/src/pybacked.rs +++ b/src/pybacked.rs @@ -92,7 +92,7 @@ impl ToPyObject for PyBackedStr { } #[cfg(not(any(Py_3_10, not(Py_LIMITED_API))))] fn to_object(&self, py: Python<'_>) -> Py { - PyString::new_bound(py, self).into_any().unbind() + PyString::new(py, self).into_any().unbind() } } @@ -103,7 +103,7 @@ impl IntoPy> for PyBackedStr { } #[cfg(not(any(Py_3_10, not(Py_LIMITED_API))))] fn into_py(self, py: Python<'_>) -> Py { - PyString::new_bound(py, &self).into_any().unbind() + PyString::new(py, &self).into_any().unbind() } } @@ -304,7 +304,7 @@ mod test { #[test] fn py_backed_str_empty() { Python::with_gil(|py| { - let s = PyString::new_bound(py, ""); + let s = PyString::new(py, ""); let py_backed_str = s.extract::().unwrap(); assert_eq!(&*py_backed_str, ""); }); @@ -313,7 +313,7 @@ mod test { #[test] fn py_backed_str() { Python::with_gil(|py| { - let s = PyString::new_bound(py, "hello"); + let s = PyString::new(py, "hello"); let py_backed_str = s.extract::().unwrap(); assert_eq!(&*py_backed_str, "hello"); }); @@ -322,7 +322,7 @@ mod test { #[test] fn py_backed_str_try_from() { Python::with_gil(|py| { - let s = PyString::new_bound(py, "hello"); + let s = PyString::new(py, "hello"); let py_backed_str = PyBackedStr::try_from(s).unwrap(); assert_eq!(&*py_backed_str, "hello"); }); @@ -331,7 +331,7 @@ mod test { #[test] fn py_backed_str_to_object() { Python::with_gil(|py| { - let orig_str = PyString::new_bound(py, "hello"); + let orig_str = PyString::new(py, "hello"); let py_backed_str = orig_str.extract::().unwrap(); let new_str = py_backed_str.to_object(py); assert_eq!(new_str.extract::(py).unwrap(), "hello"); @@ -343,7 +343,7 @@ mod test { #[test] fn py_backed_str_into_py() { Python::with_gil(|py| { - let orig_str = PyString::new_bound(py, "hello"); + let orig_str = PyString::new(py, "hello"); let py_backed_str = orig_str.extract::().unwrap(); let new_str = py_backed_str.into_py(py); assert_eq!(new_str.extract::(py).unwrap(), "hello"); @@ -432,7 +432,7 @@ mod test { #[test] fn test_backed_str_clone() { Python::with_gil(|py| { - let s1: PyBackedStr = PyString::new_bound(py, "hello").try_into().unwrap(); + let s1: PyBackedStr = PyString::new(py, "hello").try_into().unwrap(); let s2 = s1.clone(); assert_eq!(s1, s2); @@ -444,12 +444,12 @@ mod test { #[test] fn test_backed_str_eq() { Python::with_gil(|py| { - let s1: PyBackedStr = PyString::new_bound(py, "hello").try_into().unwrap(); - let s2: PyBackedStr = PyString::new_bound(py, "hello").try_into().unwrap(); + let s1: PyBackedStr = PyString::new(py, "hello").try_into().unwrap(); + let s2: PyBackedStr = PyString::new(py, "hello").try_into().unwrap(); assert_eq!(s1, "hello"); assert_eq!(s1, s2); - let s3: PyBackedStr = PyString::new_bound(py, "abcde").try_into().unwrap(); + let s3: PyBackedStr = PyString::new(py, "abcde").try_into().unwrap(); assert_eq!("abcde", s3); assert_ne!(s1, s3); }); @@ -464,7 +464,7 @@ mod test { hasher.finish() }; - let s1: PyBackedStr = PyString::new_bound(py, "abcde").try_into().unwrap(); + let s1: PyBackedStr = PyString::new(py, "abcde").try_into().unwrap(); let h1 = { let mut hasher = DefaultHasher::new(); s1.hash(&mut hasher); @@ -481,7 +481,7 @@ mod test { let mut a = vec!["a", "c", "d", "b", "f", "g", "e"]; let mut b = a .iter() - .map(|s| PyString::new_bound(py, s).try_into().unwrap()) + .map(|s| PyString::new(py, s).try_into().unwrap()) .collect::>(); a.sort(); diff --git a/src/sync.rs b/src/sync.rs index 4e327e88a67..441afa08cc2 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -278,7 +278,7 @@ impl Interned { #[inline] pub fn get<'py>(&self, py: Python<'py>) -> &Bound<'py, PyString> { self.1 - .get_or_init(py, || PyString::intern_bound(py, self.0).into()) + .get_or_init(py, || PyString::intern(py, self.0).into()) .bind(py) } } diff --git a/src/types/any.rs b/src/types/any.rs index d6d1c29cbae..9ce1f8759e7 100644 --- a/src/types/any.rs +++ b/src/types/any.rs @@ -186,7 +186,7 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed { /// # fn main() -> PyResult<()> { /// Python::with_gil(|py| -> PyResult<()> { /// let a = PyFloat::new(py, 0_f64); - /// let b = PyString::new_bound(py, "zero"); + /// let b = PyString::new(py, "zero"); /// assert!(a.compare(b).is_err()); /// Ok(()) /// })?; diff --git a/src/types/string.rs b/src/types/string.rs index 97ede1a94b1..2701e331ed7 100644 --- a/src/types/string.rs +++ b/src/types/string.rs @@ -145,7 +145,7 @@ impl<'a> PyStringData<'a> { /// use pyo3::types::PyString; /// /// # Python::with_gil(|py| { -/// let py_string = PyString::new_bound(py, "foo"); +/// let py_string = PyString::new(py, "foo"); /// // via PartialEq /// assert_eq!(py_string, "foo"); /// @@ -162,7 +162,7 @@ impl PyString { /// Creates a new Python string object. /// /// Panics if out of memory. - pub fn new_bound<'py>(py: Python<'py>, s: &str) -> Bound<'py, PyString> { + pub fn new<'py>(py: Python<'py>, s: &str) -> Bound<'py, PyString> { let ptr = s.as_ptr().cast(); let len = s.len() as ffi::Py_ssize_t; unsafe { @@ -172,6 +172,13 @@ impl PyString { } } + /// Deprecated name for [`PyString::new`]. + #[deprecated(since = "0.23.0", note = "renamed to `PyString::new`")] + #[inline] + pub fn new_bound<'py>(py: Python<'py>, s: &str) -> Bound<'py, PyString> { + Self::new(py, s) + } + /// Intern the given string /// /// This will return a reference to the same Python string object if called repeatedly with the same string. @@ -180,7 +187,7 @@ impl PyString { /// temporary Python string object and is thereby slower than [`PyString::new_bound`]. /// /// Panics if out of memory. - pub fn intern_bound<'py>(py: Python<'py>, s: &str) -> Bound<'py, PyString> { + pub fn intern<'py>(py: Python<'py>, s: &str) -> Bound<'py, PyString> { let ptr = s.as_ptr().cast(); let len = s.len() as ffi::Py_ssize_t; unsafe { @@ -192,10 +199,17 @@ impl PyString { } } + /// Deprecated name for [`PyString::intern`]. + #[deprecated(since = "0.23.0", note = "renamed to `PyString::intern`")] + #[inline] + pub fn intern_bound<'py>(py: Python<'py>, s: &str) -> Bound<'py, PyString> { + Self::intern(py, s) + } + /// Attempts to create a Python string from a Python [bytes-like object]. /// /// [bytes-like object]: (https://docs.python.org/3/glossary.html#term-bytes-like-object). - pub fn from_object_bound<'py>( + pub fn from_object<'py>( src: &Bound<'py, PyAny>, encoding: &str, errors: &str, @@ -210,6 +224,17 @@ impl PyString { .downcast_into_unchecked() } } + + /// Deprecated name for [`PyString::from_object`]. + #[deprecated(since = "0.23.0", note = "renamed to `PyString::from_object`")] + #[inline] + pub fn from_object_bound<'py>( + src: &Bound<'py, PyAny>, + encoding: &str, + errors: &str, + ) -> PyResult> { + Self::from_object(src, encoding, errors) + } } /// Implementation of functionality for [`PyString`]. @@ -558,7 +583,7 @@ mod tests { fn test_to_cow_utf8() { Python::with_gil(|py| { let s = "ascii 🐈"; - let py_string = PyString::new_bound(py, s); + let py_string = PyString::new(py, s); assert_eq!(s, py_string.to_cow().unwrap()); }) } @@ -579,7 +604,7 @@ mod tests { fn test_to_cow_unicode() { Python::with_gil(|py| { let s = "哈哈🐈"; - let py_string = PyString::new_bound(py, s); + let py_string = PyString::new(py, s); assert_eq!(s, py_string.to_cow().unwrap()); }) } @@ -588,7 +613,7 @@ mod tests { fn test_encode_utf8_unicode() { Python::with_gil(|py| { let s = "哈哈🐈"; - let obj = PyString::new_bound(py, s); + let obj = PyString::new(py, s); assert_eq!(s.as_bytes(), obj.encode_utf8().unwrap().as_bytes()); }) } @@ -641,7 +666,7 @@ mod tests { #[cfg(not(any(Py_LIMITED_API, PyPy)))] fn test_string_data_ucs1() { Python::with_gil(|py| { - let s = PyString::new_bound(py, "hello, world"); + let s = PyString::new(py, "hello, world"); let data = unsafe { s.data().unwrap() }; assert_eq!(data, PyStringData::Ucs1(b"hello, world")); @@ -727,7 +752,7 @@ mod tests { fn test_string_data_ucs4() { Python::with_gil(|py| { let s = "哈哈🐈"; - let py_string = PyString::new_bound(py, s); + let py_string = PyString::new(py, s); let data = unsafe { py_string.data().unwrap() }; assert_eq!(data, PyStringData::Ucs4(&[21704, 21704, 128008])); @@ -766,15 +791,15 @@ mod tests { #[test] fn test_intern_string() { Python::with_gil(|py| { - let py_string1 = PyString::intern_bound(py, "foo"); + let py_string1 = PyString::intern(py, "foo"); assert_eq!(py_string1, "foo"); - let py_string2 = PyString::intern_bound(py, "foo"); + let py_string2 = PyString::intern(py, "foo"); assert_eq!(py_string2, "foo"); assert_eq!(py_string1.as_ptr(), py_string2.as_ptr()); - let py_string3 = PyString::intern_bound(py, "bar"); + let py_string3 = PyString::intern(py, "bar"); assert_eq!(py_string3, "bar"); assert_ne!(py_string1.as_ptr(), py_string3.as_ptr()); @@ -785,7 +810,7 @@ mod tests { fn test_py_to_str_utf8() { Python::with_gil(|py| { let s = "ascii 🐈"; - let py_string: Py = PyString::new_bound(py, s).into_py(py); + let py_string: Py = PyString::new(py, s).into_py(py); #[cfg(any(Py_3_10, not(Py_LIMITED_API)))] assert_eq!(s, py_string.to_str(py).unwrap()); @@ -826,7 +851,7 @@ mod tests { fn test_comparisons() { Python::with_gil(|py| { let s = "hello, world"; - let py_string = PyString::new_bound(py, s); + let py_string = PyString::new(py, s); assert_eq!(py_string, "hello, world"); diff --git a/src/types/typeobject.rs b/src/types/typeobject.rs index 609e12089ce..c31dccb565e 100644 --- a/src/types/typeobject.rs +++ b/src/types/typeobject.rs @@ -163,7 +163,7 @@ impl<'py> PyTypeMethods<'py> for Bound<'py, PyType> { if module_str == "builtins" || module_str == "__main__" { qualname.downcast_into()? } else { - PyString::new_bound(self.py(), &format!("{}.{}", module, qualname)) + PyString::new(self.py(), &format!("{}.{}", module, qualname)) } }; diff --git a/tests/test_pyself.rs b/tests/test_pyself.rs index 901f52de530..a4899131c41 100644 --- a/tests/test_pyself.rs +++ b/tests/test_pyself.rs @@ -75,7 +75,7 @@ impl Iter { let res = reader_ref .inner .get(&b) - .map(|s| PyString::new_bound(py, s).into()); + .map(|s| PyString::new(py, s).into()); Ok(res) } None => Ok(None), diff --git a/tests/ui/not_send2.rs b/tests/ui/not_send2.rs index fa99e602ba0..32d95c1a231 100644 --- a/tests/ui/not_send2.rs +++ b/tests/ui/not_send2.rs @@ -3,7 +3,7 @@ use pyo3::types::PyString; fn main() { Python::with_gil(|py| { - let string = PyString::new_bound(py, "foo"); + let string = PyString::new(py, "foo"); py.allow_threads(|| { println!("{:?}", string); From f53971e1d063e038f7602f1d1ad398ef1b354f96 Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Sat, 10 Aug 2024 21:11:08 +0200 Subject: [PATCH 204/495] reintroduce a bunch of type contructors (#4432) * reintroduce `PyCFunction` constructors * reintroduce `PyIterator` contructors * reintroduce `PyMemoryView` contructors * reintroduce `PyNone` contructors * reintroduce `PyNotImplemented` contructors * reintroduce `PySuper` contructors * reintroduce `PySlice` contructors * reintroduce `PyType` contructors --- pytests/src/buf_and_str.rs | 2 +- src/coroutine.rs | 2 +- src/err/mod.rs | 2 +- src/impl_/wrap.rs | 2 +- src/marker.rs | 4 +-- src/types/any.rs | 4 +-- src/types/frozenset.rs | 2 +- src/types/function.rs | 50 ++++++++++++++++++++++++++++++--- src/types/iterator.rs | 11 ++++++-- src/types/memoryview.rs | 11 ++++++-- src/types/none.rs | 26 +++++++++-------- src/types/notimplemented.rs | 17 +++++++---- src/types/pysuper.rs | 12 +++++++- src/types/set.rs | 2 +- src/types/slice.rs | 24 ++++++++++++---- src/types/typeobject.rs | 9 +++++- tests/test_class_basics.rs | 2 +- tests/test_exceptions.rs | 2 +- tests/test_pyfunction.rs | 16 ++++------- tests/test_super.rs | 2 +- tests/ui/invalid_closure.rs | 2 +- tests/ui/invalid_closure.stderr | 4 +-- 22 files changed, 151 insertions(+), 57 deletions(-) diff --git a/pytests/src/buf_and_str.rs b/pytests/src/buf_and_str.rs index 4a7add32bc2..f9b55efb74f 100644 --- a/pytests/src/buf_and_str.rs +++ b/pytests/src/buf_and_str.rs @@ -44,7 +44,7 @@ impl BytesExtractor { #[pyfunction] fn return_memoryview(py: Python<'_>) -> PyResult> { let bytes = PyBytes::new(py, b"hello world"); - PyMemoryView::from_bound(&bytes) + PyMemoryView::from(&bytes) } #[pymodule] diff --git a/src/coroutine.rs b/src/coroutine.rs index 652292c7892..169d28d635f 100644 --- a/src/coroutine.rs +++ b/src/coroutine.rs @@ -107,7 +107,7 @@ impl Coroutine { if let Some(future) = self.waker.as_ref().unwrap().initialize_future(py)? { // `asyncio.Future` must be awaited; fortunately, it implements `__iter__ = __await__` // and will yield itself if its result has not been set in polling above - if let Some(future) = PyIterator::from_bound_object(&future.as_borrowed()) + if let Some(future) = PyIterator::from_object(&future.as_borrowed()) .unwrap() .next() { diff --git a/src/err/mod.rs b/src/err/mod.rs index 69180cca7b5..3e344c4a66f 100644 --- a/src/err/mod.rs +++ b/src/err/mod.rs @@ -237,7 +237,7 @@ impl PyErr { /// /// Python::with_gil(|py| { /// let err: PyErr = PyTypeError::new_err(("some type error",)); - /// assert!(err.get_type_bound(py).is(&PyType::new_bound::(py))); + /// assert!(err.get_type_bound(py).is(&PyType::new::(py))); /// }); /// ``` pub fn get_type_bound<'py>(&self, py: Python<'py>) -> Bound<'py, PyType> { diff --git a/src/impl_/wrap.rs b/src/impl_/wrap.rs index 3ab53cf5239..f32faaf5b2b 100644 --- a/src/impl_/wrap.rs +++ b/src/impl_/wrap.rs @@ -74,7 +74,7 @@ impl Deref for UnknownReturnResultType { impl EmptyTupleConverter> { #[inline] pub fn map_into_ptr(&self, py: Python<'_>, obj: PyResult<()>) -> PyResult<*mut ffi::PyObject> { - obj.map(|_| PyNone::get_bound(py).to_owned().into_ptr()) + obj.map(|_| PyNone::get(py).to_owned().into_ptr()) } } diff --git a/src/marker.rs b/src/marker.rs index 84ad22e931c..e2d9a7938ec 100644 --- a/src/marker.rs +++ b/src/marker.rs @@ -670,7 +670,7 @@ impl<'py> Python<'py> { #[allow(non_snake_case)] // the Python keyword starts with uppercase #[inline] pub fn None(self) -> PyObject { - PyNone::get_bound(self).into_py(self) + PyNone::get(self).into_py(self) } /// Gets the Python builtin value `Ellipsis`, or `...`. @@ -684,7 +684,7 @@ impl<'py> Python<'py> { #[allow(non_snake_case)] // the Python keyword starts with uppercase #[inline] pub fn NotImplemented(self) -> PyObject { - PyNotImplemented::get_bound(self).into_py(self) + PyNotImplemented::get(self).into_py(self) } /// Gets the running Python interpreter version as a string. diff --git a/src/types/any.rs b/src/types/any.rs index 9ce1f8759e7..72f141eec02 100644 --- a/src/types/any.rs +++ b/src/types/any.rs @@ -1305,7 +1305,7 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { } fn iter(&self) -> PyResult> { - PyIterator::from_bound_object(self) + PyIterator::from_object(self) } fn get_type(&self) -> Bound<'py, PyType> { @@ -1466,7 +1466,7 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { #[cfg(not(any(PyPy, GraalPy)))] fn py_super(&self) -> PyResult> { - PySuper::new_bound(&self.get_type(), self) + PySuper::new(&self.get_type(), self) } } diff --git a/src/types/frozenset.rs b/src/types/frozenset.rs index b25f0173e4d..6a0cdca89d5 100644 --- a/src/types/frozenset.rs +++ b/src/types/frozenset.rs @@ -200,7 +200,7 @@ pub struct BoundFrozenSetIterator<'p> { impl<'py> BoundFrozenSetIterator<'py> { pub(super) fn new(set: Bound<'py, PyFrozenSet>) -> Self { Self { - it: PyIterator::from_bound_object(&set).unwrap(), + it: PyIterator::from_object(&set).unwrap(), remaining: set.len(), } } diff --git a/src/types/function.rs b/src/types/function.rs index 38d21e32428..8d226a9e792 100644 --- a/src/types/function.rs +++ b/src/types/function.rs @@ -25,7 +25,7 @@ impl PyCFunction { /// /// To create `name` and `doc` static strings on Rust versions older than 1.77 (which added c"" literals), /// use the [`c_str!`](crate::ffi::c_str) macro. - pub fn new_with_keywords_bound<'py>( + pub fn new_with_keywords<'py>( py: Python<'py>, fun: ffi::PyCFunctionWithKeywords, name: &'static CStr, @@ -39,11 +39,24 @@ impl PyCFunction { ) } + /// Deprecated name for [`PyCFunction::new_with_keywords`]. + #[deprecated(since = "0.23.0", note = "renamed to `PyCFunction::new_with_keywords`")] + #[inline] + pub fn new_with_keywords_bound<'py>( + py: Python<'py>, + fun: ffi::PyCFunctionWithKeywords, + name: &'static CStr, + doc: &'static CStr, + module: Option<&Bound<'py, PyModule>>, + ) -> PyResult> { + Self::new_with_keywords(py, fun, name, doc, module) + } + /// Create a new built-in function which takes no arguments. /// /// To create `name` and `doc` static strings on Rust versions older than 1.77 (which added c"" literals), /// use the [`c_str!`](crate::ffi::c_str) macro. - pub fn new_bound<'py>( + pub fn new<'py>( py: Python<'py>, fun: ffi::PyCFunction, name: &'static CStr, @@ -53,6 +66,19 @@ impl PyCFunction { Self::internal_new(py, &PyMethodDef::noargs(name, fun, doc), module) } + /// Deprecated name for [`PyCFunction::new`]. + #[deprecated(since = "0.23.0", note = "renamed to `PyCFunction::new`")] + #[inline] + pub fn new_bound<'py>( + py: Python<'py>, + fun: ffi::PyCFunction, + name: &'static CStr, + doc: &'static CStr, + module: Option<&Bound<'py, PyModule>>, + ) -> PyResult> { + Self::new(py, fun, name, doc, module) + } + /// Create a new function from a closure. /// /// # Examples @@ -66,11 +92,11 @@ impl PyCFunction { /// let i = args.extract::<(i64,)>()?.0; /// Ok(i+1) /// }; - /// let add_one = PyCFunction::new_closure_bound(py, None, None, add_one).unwrap(); + /// let add_one = PyCFunction::new_closure(py, None, None, add_one).unwrap(); /// py_run!(py, add_one, "assert add_one(42) == 43"); /// }); /// ``` - pub fn new_closure_bound<'py, F, R>( + pub fn new_closure<'py, F, R>( py: Python<'py>, name: Option<&'static CStr>, doc: Option<&'static CStr>, @@ -105,6 +131,22 @@ impl PyCFunction { } } + /// Deprecated name for [`PyCFunction::new_closure`]. + #[deprecated(since = "0.23.0", note = "renamed to `PyCFunction::new_closure`")] + #[inline] + pub fn new_closure_bound<'py, F, R>( + py: Python<'py>, + name: Option<&'static CStr>, + doc: Option<&'static CStr>, + closure: F, + ) -> PyResult> + where + F: Fn(&Bound<'_, PyTuple>, Option<&Bound<'_, PyDict>>) -> R + Send + 'static, + R: crate::callback::IntoPyCallbackOutput<*mut ffi::PyObject>, + { + Self::new_closure(py, name, doc, closure) + } + #[doc(hidden)] pub fn internal_new<'py>( py: Python<'py>, diff --git a/src/types/iterator.rs b/src/types/iterator.rs index 453a4882c46..94d0fbf976b 100644 --- a/src/types/iterator.rs +++ b/src/types/iterator.rs @@ -35,13 +35,20 @@ impl PyIterator { /// /// Usually it is more convenient to write [`obj.iter()`][crate::types::any::PyAnyMethods::iter], /// which is a more concise way of calling this function. - pub fn from_bound_object<'py>(obj: &Bound<'py, PyAny>) -> PyResult> { + pub fn from_object<'py>(obj: &Bound<'py, PyAny>) -> PyResult> { unsafe { ffi::PyObject_GetIter(obj.as_ptr()) .assume_owned_or_err(obj.py()) .downcast_into_unchecked() } } + + /// Deprecated name for [`PyIterator::from_object`]. + #[deprecated(since = "0.23.0", note = "renamed to `PyIterator::from_object`")] + #[inline] + pub fn from_bound_object<'py>(obj: &Bound<'py, PyAny>) -> PyResult> { + Self::from_object(obj) + } } impl<'py> Iterator for Bound<'py, PyIterator> { @@ -232,7 +239,7 @@ def fibonacci(target): fn int_not_iterable() { Python::with_gil(|py| { let x = 5.to_object(py); - let err = PyIterator::from_bound_object(x.bind(py)).unwrap_err(); + let err = PyIterator::from_object(x.bind(py)).unwrap_err(); assert!(err.is_instance_of::(py)); }); diff --git a/src/types/memoryview.rs b/src/types/memoryview.rs index 22fbb9ba4e2..81acc5cbb2a 100644 --- a/src/types/memoryview.rs +++ b/src/types/memoryview.rs @@ -15,13 +15,20 @@ pyobject_native_type_core!(PyMemoryView, pyobject_native_static_type_object!(ffi impl PyMemoryView { /// Creates a new Python `memoryview` object from another Python object that /// implements the buffer protocol. - pub fn from_bound<'py>(src: &Bound<'py, PyAny>) -> PyResult> { + pub fn from<'py>(src: &Bound<'py, PyAny>) -> PyResult> { unsafe { ffi::PyMemoryView_FromObject(src.as_ptr()) .assume_owned_or_err(src.py()) .downcast_into_unchecked() } } + + /// Deprecated name for [`PyMemoryView::from`]. + #[deprecated(since = "0.23.0", note = "renamed to `PyMemoryView::from`")] + #[inline] + pub fn from_bound<'py>(src: &Bound<'py, PyAny>) -> PyResult> { + Self::from(src) + } } impl<'py> TryFrom<&Bound<'py, PyAny>> for Bound<'py, PyMemoryView> { @@ -30,6 +37,6 @@ impl<'py> TryFrom<&Bound<'py, PyAny>> for Bound<'py, PyMemoryView> { /// Creates a new Python `memoryview` object from another Python object that /// implements the buffer protocol. fn try_from(value: &Bound<'py, PyAny>) -> Result { - PyMemoryView::from_bound(value) + PyMemoryView::from(value) } } diff --git a/src/types/none.rs b/src/types/none.rs index e9846f21969..2a1c54535d7 100644 --- a/src/types/none.rs +++ b/src/types/none.rs @@ -16,9 +16,16 @@ pyobject_native_type_named!(PyNone); impl PyNone { /// Returns the `None` object. #[inline] - pub fn get_bound(py: Python<'_>) -> Borrowed<'_, '_, PyNone> { + pub fn get(py: Python<'_>) -> Borrowed<'_, '_, PyNone> { unsafe { ffi::Py_None().assume_borrowed(py).downcast_unchecked() } } + + /// Deprecated name for [`PyNone::get`]. + #[deprecated(since = "0.23.0", note = "renamed to `PyNone::get`")] + #[inline] + pub fn get_bound(py: Python<'_>) -> Borrowed<'_, '_, PyNone> { + Self::get(py) + } } unsafe impl PyTypeInfo for PyNone { @@ -38,21 +45,21 @@ unsafe impl PyTypeInfo for PyNone { #[inline] fn is_exact_type_of_bound(object: &Bound<'_, PyAny>) -> bool { - object.is(&**Self::get_bound(object.py())) + object.is(&**Self::get(object.py())) } } /// `()` is converted to Python `None`. impl ToPyObject for () { fn to_object(&self, py: Python<'_>) -> PyObject { - PyNone::get_bound(py).into_py(py) + PyNone::get(py).into_py(py) } } impl IntoPy for () { #[inline] fn into_py(self, py: Python<'_>) -> PyObject { - PyNone::get_bound(py).into_py(py) + PyNone::get(py).into_py(py) } } @@ -64,15 +71,15 @@ mod tests { #[test] fn test_none_is_itself() { Python::with_gil(|py| { - assert!(PyNone::get_bound(py).is_instance_of::()); - assert!(PyNone::get_bound(py).is_exact_instance_of::()); + assert!(PyNone::get(py).is_instance_of::()); + assert!(PyNone::get(py).is_exact_instance_of::()); }) } #[test] fn test_none_type_object_consistent() { Python::with_gil(|py| { - assert!(PyNone::get_bound(py) + assert!(PyNone::get(py) .get_type() .is(&PyNone::type_object_bound(py))); }) @@ -81,10 +88,7 @@ mod tests { #[test] fn test_none_is_none() { Python::with_gil(|py| { - assert!(PyNone::get_bound(py) - .downcast::() - .unwrap() - .is_none()); + assert!(PyNone::get(py).downcast::().unwrap().is_none()); }) } diff --git a/src/types/notimplemented.rs b/src/types/notimplemented.rs index 4f8164d39fb..c1c3d1c393a 100644 --- a/src/types/notimplemented.rs +++ b/src/types/notimplemented.rs @@ -15,13 +15,20 @@ pyobject_native_type_named!(PyNotImplemented); impl PyNotImplemented { /// Returns the `NotImplemented` object. #[inline] - pub fn get_bound(py: Python<'_>) -> Borrowed<'_, '_, PyNotImplemented> { + pub fn get(py: Python<'_>) -> Borrowed<'_, '_, PyNotImplemented> { unsafe { ffi::Py_NotImplemented() .assume_borrowed(py) .downcast_unchecked() } } + + /// Deprecated name for [`PyNotImplemented::get`]. + #[deprecated(since = "0.23.0", note = "renamed to `PyNotImplemented::get`")] + #[inline] + pub fn get_bound(py: Python<'_>) -> Borrowed<'_, '_, PyNotImplemented> { + Self::get(py) + } } unsafe impl PyTypeInfo for PyNotImplemented { @@ -40,7 +47,7 @@ unsafe impl PyTypeInfo for PyNotImplemented { #[inline] fn is_exact_type_of_bound(object: &Bound<'_, PyAny>) -> bool { - object.is(&**Self::get_bound(object.py())) + object.is(&**Self::get(object.py())) } } @@ -53,15 +60,15 @@ mod tests { #[test] fn test_notimplemented_is_itself() { Python::with_gil(|py| { - assert!(PyNotImplemented::get_bound(py).is_instance_of::()); - assert!(PyNotImplemented::get_bound(py).is_exact_instance_of::()); + assert!(PyNotImplemented::get(py).is_instance_of::()); + assert!(PyNotImplemented::get(py).is_exact_instance_of::()); }) } #[test] fn test_notimplemented_type_object_consistent() { Python::with_gil(|py| { - assert!(PyNotImplemented::get_bound(py) + assert!(PyNotImplemented::get(py) .get_type() .is(&PyNotImplemented::type_object_bound(py))); }) diff --git a/src/types/pysuper.rs b/src/types/pysuper.rs index 456a3d83e8c..c4e15baca7c 100644 --- a/src/types/pysuper.rs +++ b/src/types/pysuper.rs @@ -57,7 +57,7 @@ impl PySuper { /// } /// } /// ``` - pub fn new_bound<'py>( + pub fn new<'py>( ty: &Bound<'py, PyType>, obj: &Bound<'py, PyAny>, ) -> PyResult> { @@ -68,4 +68,14 @@ impl PySuper { unsafe { any.downcast_into_unchecked() } }) } + + /// Deprecated name for [`PySuper::new`]. + #[deprecated(since = "0.23.0", note = "renamed to `PySuper::new`")] + #[inline] + pub fn new_bound<'py>( + ty: &Bound<'py, PyType>, + obj: &Bound<'py, PyAny>, + ) -> PyResult> { + Self::new(ty, obj) + } } diff --git a/src/types/set.rs b/src/types/set.rs index 4f04a4e4e8f..4bfc45fe80b 100644 --- a/src/types/set.rs +++ b/src/types/set.rs @@ -217,7 +217,7 @@ pub struct BoundSetIterator<'p> { impl<'py> BoundSetIterator<'py> { pub(super) fn new(set: Bound<'py, PySet>) -> Self { Self { - it: PyIterator::from_bound_object(&set).unwrap(), + it: PyIterator::from_object(&set).unwrap(), remaining: set.len(), } } diff --git a/src/types/slice.rs b/src/types/slice.rs index 1e6e0584eae..b9fad06ebdb 100644 --- a/src/types/slice.rs +++ b/src/types/slice.rs @@ -54,7 +54,7 @@ impl PySliceIndices { impl PySlice { /// Constructs a new slice with the given elements. - pub fn new_bound(py: Python<'_>, start: isize, stop: isize, step: isize) -> Bound<'_, PySlice> { + pub fn new(py: Python<'_>, start: isize, stop: isize, step: isize) -> Bound<'_, PySlice> { unsafe { ffi::PySlice_New( ffi::PyLong_FromSsize_t(start), @@ -66,14 +66,28 @@ impl PySlice { } } + /// Deprecated name for [`PySlice::new`]. + #[deprecated(since = "0.23.0", note = "renamed to `PySlice::new`")] + #[inline] + pub fn new_bound(py: Python<'_>, start: isize, stop: isize, step: isize) -> Bound<'_, PySlice> { + Self::new(py, start, stop, step) + } + /// Constructs a new full slice that is equivalent to `::`. - pub fn full_bound(py: Python<'_>) -> Bound<'_, PySlice> { + pub fn full(py: Python<'_>) -> Bound<'_, PySlice> { unsafe { ffi::PySlice_New(ffi::Py_None(), ffi::Py_None(), ffi::Py_None()) .assume_owned(py) .downcast_into_unchecked() } } + + /// Deprecated name for [`PySlice::full`]. + #[deprecated(since = "0.23.0", note = "renamed to `PySlice::full`")] + #[inline] + pub fn full_bound(py: Python<'_>) -> Bound<'_, PySlice> { + Self::full(py) + } } /// Implementation of functionality for [`PySlice`]. @@ -121,7 +135,7 @@ impl<'py> PySliceMethods<'py> for Bound<'py, PySlice> { impl ToPyObject for PySliceIndices { fn to_object(&self, py: Python<'_>) -> PyObject { - PySlice::new_bound(py, self.start, self.stop, self.step).into() + PySlice::new(py, self.start, self.stop, self.step).into() } } @@ -132,7 +146,7 @@ mod tests { #[test] fn test_py_slice_new() { Python::with_gil(|py| { - let slice = PySlice::new_bound(py, isize::MIN, isize::MAX, 1); + let slice = PySlice::new(py, isize::MIN, isize::MAX, 1); assert_eq!( slice.getattr("start").unwrap().extract::().unwrap(), isize::MIN @@ -151,7 +165,7 @@ mod tests { #[test] fn test_py_slice_full() { Python::with_gil(|py| { - let slice = PySlice::full_bound(py); + let slice = PySlice::full(py); assert!(slice.getattr("start").unwrap().is_none(),); assert!(slice.getattr("stop").unwrap().is_none(),); assert!(slice.getattr("step").unwrap().is_none(),); diff --git a/src/types/typeobject.rs b/src/types/typeobject.rs index c31dccb565e..bbb1e5ef4cb 100644 --- a/src/types/typeobject.rs +++ b/src/types/typeobject.rs @@ -23,10 +23,17 @@ pyobject_native_type_core!(PyType, pyobject_native_static_type_object!(ffi::PyTy impl PyType { /// Creates a new type object. #[inline] - pub fn new_bound(py: Python<'_>) -> Bound<'_, PyType> { + pub fn new(py: Python<'_>) -> Bound<'_, PyType> { T::type_object_bound(py) } + /// Deprecated name for [`PyType::new`]. + #[deprecated(since = "0.23.0", note = "renamed to `PyType::new`")] + #[inline] + pub fn new_bound(py: Python<'_>) -> Bound<'_, PyType> { + Self::new::(py) + } + /// Converts the given FFI pointer into `Bound`, to use in safe code. /// /// The function creates a new reference from the given pointer, and returns diff --git a/tests/test_class_basics.rs b/tests/test_class_basics.rs index a255cc16de5..cb42e532154 100644 --- a/tests/test_class_basics.rs +++ b/tests/test_class_basics.rs @@ -288,7 +288,7 @@ impl UnsendableChild { fn test_unsendable() -> PyResult<()> { let (keep_obj_here, obj) = Python::with_gil(|py| -> PyResult<_> { - let obj: Py = PyType::new_bound::(py).call1((5,))?.extract()?; + let obj: Py = PyType::new::(py).call1((5,))?.extract()?; // Accessing the value inside this thread should not panic let caught_panic = diff --git a/tests/test_exceptions.rs b/tests/test_exceptions.rs index 4cf866f9a0b..cc823136997 100644 --- a/tests/test_exceptions.rs +++ b/tests/test_exceptions.rs @@ -117,7 +117,7 @@ fn test_write_unraisable() { assert!(object.is_none(py)); let err = PyRuntimeError::new_err("bar"); - err.write_unraisable_bound(py, Some(&PyNotImplemented::get_bound(py))); + err.write_unraisable_bound(py, Some(&PyNotImplemented::get(py))); let (err, object) = capture.borrow_mut(py).capture.take().unwrap(); assert_eq!(err.to_string(), "RuntimeError: bar"); diff --git a/tests/test_pyfunction.rs b/tests/test_pyfunction.rs index 24ef32d6994..4896207b0ab 100644 --- a/tests/test_pyfunction.rs +++ b/tests/test_pyfunction.rs @@ -342,7 +342,7 @@ fn test_pycfunction_new() { ffi::PyLong_FromLong(4200) } - let py_fn = PyCFunction::new_bound( + let py_fn = PyCFunction::new( py, c_fn, c_str!("py_fn"), @@ -409,7 +409,7 @@ fn test_pycfunction_new_with_keywords() { ffi::PyLong_FromLong(foo * bar) } - let py_fn = PyCFunction::new_with_keywords_bound( + let py_fn = PyCFunction::new_with_keywords( py, c_fn, c_str!("py_fn"), @@ -453,13 +453,9 @@ fn test_closure() { Ok(res) }) }; - let closure_py = PyCFunction::new_closure_bound( - py, - Some(c_str!("test_fn")), - Some(c_str!("test_fn doc")), - f, - ) - .unwrap(); + let closure_py = + PyCFunction::new_closure(py, Some(c_str!("test_fn")), Some(c_str!("test_fn doc")), f) + .unwrap(); py_assert!(py, closure_py, "closure_py(42) == [43]"); py_assert!(py, closure_py, "closure_py.__name__ == 'test_fn'"); @@ -483,7 +479,7 @@ fn test_closure_counter() { *counter += 1; Ok(*counter) }; - let counter_py = PyCFunction::new_closure_bound(py, None, None, counter_fn).unwrap(); + let counter_py = PyCFunction::new_closure(py, None, None, counter_fn).unwrap(); py_assert!(py, counter_py, "counter_py() == 1"); py_assert!(py, counter_py, "counter_py() == 2"); diff --git a/tests/test_super.rs b/tests/test_super.rs index 3362ad57814..b64fd57e687 100644 --- a/tests/test_super.rs +++ b/tests/test_super.rs @@ -35,7 +35,7 @@ impl SubClass { } fn method_super_new<'py>(self_: &Bound<'py, Self>) -> PyResult> { - let super_ = PySuper::new_bound(&self_.get_type(), self_)?; + let super_ = PySuper::new(&self_.get_type(), self_)?; super_.call_method("method", (), None) } } diff --git a/tests/ui/invalid_closure.rs b/tests/ui/invalid_closure.rs index eca988f1e57..cb302375934 100644 --- a/tests/ui/invalid_closure.rs +++ b/tests/ui/invalid_closure.rs @@ -11,7 +11,7 @@ fn main() { println!("This is five: {:?}", ref_.len()); Ok(()) }; - PyCFunction::new_closure_bound(py, None, None, closure_fn) + PyCFunction::new_closure(py, None, None, closure_fn) .unwrap() .into() }); diff --git a/tests/ui/invalid_closure.stderr b/tests/ui/invalid_closure.stderr index 890d7640502..4791954f3e1 100644 --- a/tests/ui/invalid_closure.stderr +++ b/tests/ui/invalid_closure.stderr @@ -6,8 +6,8 @@ error[E0597]: `local_data` does not live long enough 7 | let ref_: &[u8] = &local_data; | ^^^^^^^^^^^ borrowed value does not live long enough ... -14 | PyCFunction::new_closure_bound(py, None, None, closure_fn) - | ---------------------------------------------------------- argument requires that `local_data` is borrowed for `'static` +14 | PyCFunction::new_closure(py, None, None, closure_fn) + | ---------------------------------------------------- argument requires that `local_data` is borrowed for `'static` ... 17 | }); | - `local_data` dropped here while still borrowed From 0791562b5560d2fbbd7bffaa6d6b2af2c50212aa Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Sat, 10 Aug 2024 22:41:12 +0100 Subject: [PATCH 205/495] ci: install `cargo-careful` from binary again (#4433) --- .github/workflows/ci.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a1b2ddbf51b..c8f4b5dcc67 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -349,9 +349,7 @@ jobs: - uses: dtolnay/rust-toolchain@nightly with: components: rust-src - # FIXME: workaround https://github.com/RalfJung/cargo-careful/issues/36 - # - uses: taiki-e/install-action@cargo-careful - - 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 b520bc1704eb92f77188b49b356dfc29fc5fa42a Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Mon, 12 Aug 2024 23:57:36 +0200 Subject: [PATCH 206/495] Reintroduces `Python::import` and `Python::get_type` (#4436) * reintroduce `Python::import` * reintroduce `Python::get_type` --- README.md | 4 +-- guide/src/class.md | 26 +++++++++---------- guide/src/exception.md | 4 +-- guide/src/migration.md | 6 ++--- .../python-from-rust/calling-existing-code.md | 4 +-- pyo3-macros-backend/src/pyclass.rs | 2 +- src/buffer.rs | 2 +- src/conversions/chrono.rs | 10 +++---- src/conversions/chrono_tz.rs | 5 +--- src/conversions/num_bigint.rs | 4 +-- src/conversions/std/time.rs | 14 +++------- src/coroutine/waker.rs | 2 +- src/err/mod.rs | 6 ++--- src/exceptions.rs | 20 +++++++------- src/gil.rs | 2 +- src/impl_/extract_argument.rs | 5 +--- src/impl_/pymodule.rs | 6 ++--- src/instance.rs | 4 +-- src/lib.rs | 4 +-- src/macros.rs | 2 +- src/marker.rs | 26 +++++++++++++++++-- src/pyclass_init.rs | 2 +- src/sync.rs | 2 +- src/tests/common.rs | 8 +++--- src/types/any.rs | 12 ++++----- src/types/dict.rs | 8 +++--- src/types/string.rs | 6 ++--- src/types/traceback.rs | 2 +- src/types/typeobject.rs | 25 ++++++++---------- tests/test_class_attributes.rs | 20 +++++++------- tests/test_class_basics.rs | 16 ++++++------ tests/test_class_new.rs | 22 ++++++++-------- tests/test_coroutine.rs | 12 ++++----- tests/test_datetime.rs | 2 +- tests/test_datetime_import.rs | 2 +- tests/test_enum.rs | 14 +++++----- tests/test_gc.rs | 16 ++++++------ tests/test_getter_setter.rs | 2 +- tests/test_inheritance.rs | 14 +++++----- tests/test_macro_docs.rs | 2 +- tests/test_macros.rs | 4 +-- tests/test_mapping.rs | 2 +- tests/test_methods.rs | 14 +++++----- tests/test_multiple_pymethods.rs | 2 +- tests/test_proto_methods.rs | 8 +++--- tests/test_sequence.rs | 6 ++--- tests/test_static_slots.rs | 2 +- tests/test_super.rs | 2 +- tests/test_text_signature.rs | 16 ++++++------ tests/test_variable_arguments.rs | 4 +-- tests/ui/invalid_intern_arg.rs | 2 +- tests/ui/invalid_intern_arg.stderr | 16 ++++++------ 52 files changed, 215 insertions(+), 208 deletions(-) diff --git a/README.md b/README.md index 908f184cc6b..d0c53f0d86c 100644 --- a/README.md +++ b/README.md @@ -149,10 +149,10 @@ use pyo3::types::IntoPyDict; fn main() -> PyResult<()> { Python::with_gil(|py| { - let sys = py.import_bound("sys")?; + let sys = py.import("sys")?; let version: String = sys.getattr("version")?.extract()?; - let locals = [("os", py.import_bound("os")?)].into_py_dict(py); + let locals = [("os", py.import("os")?)].into_py_dict(py); let code = "os.getenv('USER') or os.getenv('USERNAME') or 'Unknown'"; let user: String = py.eval_bound(code, None, Some(&locals))?.extract()?; diff --git a/guide/src/class.md b/guide/src/class.md index 216838f779d..9f87f2828b0 100644 --- a/guide/src/class.md +++ b/guide/src/class.md @@ -430,7 +430,7 @@ impl SubSubClass { # pyo3::py_run!(py, subsub, "assert subsub.get_values() == (20, 30, 40)"); # let subsub = SubSubClass::factory_method(py, 2).unwrap(); # let subsubsub = SubSubClass::factory_method(py, 3).unwrap(); -# let cls = py.get_type_bound::(); +# 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)"); # }); @@ -526,7 +526,7 @@ impl MyDict { // some custom methods that use `private` here... } # Python::with_gil(|py| { -# let cls = py.get_type_bound::(); +# let cls = py.get_type::(); # pyo3::py_run!(py, cls, "cls(a=1, b=2)") # }); # } @@ -797,7 +797,7 @@ impl MyClass { } Python::with_gil(|py| { - let my_class = py.get_type_bound::(); + let my_class = py.get_type::(); pyo3::py_run!(py, my_class, "assert my_class.my_attribute == 'hello'") }); ``` @@ -1093,7 +1093,7 @@ enum MyEnum { Python::with_gil(|py| { let x = Py::new(py, MyEnum::Variant).unwrap(); let y = Py::new(py, MyEnum::OtherVariant).unwrap(); - let cls = py.get_type_bound::(); + let cls = py.get_type::(); pyo3::py_run!(py, x y cls, r#" assert x == cls.Variant assert y == cls.OtherVariant @@ -1114,7 +1114,7 @@ enum MyEnum { } Python::with_gil(|py| { - let cls = py.get_type_bound::(); + let cls = py.get_type::(); let x = MyEnum::Variant as i32; // The exact value is assigned by the compiler. pyo3::py_run!(py, cls x, r#" assert int(cls.Variant) == x @@ -1135,7 +1135,7 @@ enum MyEnum{ } Python::with_gil(|py| { - let cls = py.get_type_bound::(); + let cls = py.get_type::(); let x = Py::new(py, MyEnum::Variant).unwrap(); pyo3::py_run!(py, cls x, r#" assert repr(x) == 'MyEnum.Variant' @@ -1162,7 +1162,7 @@ impl MyEnum { } Python::with_gil(|py| { - let cls = py.get_type_bound::(); + let cls = py.get_type::(); pyo3::py_run!(py, cls, "assert repr(cls.Answer) == '42'") }) ``` @@ -1180,7 +1180,7 @@ enum MyEnum { Python::with_gil(|py| { let x = Py::new(py, MyEnum::Variant).unwrap(); - let cls = py.get_type_bound::(); + let cls = py.get_type::(); pyo3::py_run!(py, x cls, r#" assert repr(x) == 'RenamedEnum.UPPERCASE' assert x == cls.UPPERCASE @@ -1202,7 +1202,7 @@ enum MyEnum{ } Python::with_gil(|py| { - let cls = py.get_type_bound::(); + let cls = py.get_type::(); let a = Py::new(py, MyEnum::A).unwrap(); let b = Py::new(py, MyEnum::B).unwrap(); let c = Py::new(py, MyEnum::C).unwrap(); @@ -1260,7 +1260,7 @@ enum Shape { Python::with_gil(|py| { let circle = Shape::Circle { radius: 10.0 }.into_py(py); let square = Shape::RegularPolygon(4, 10.0).into_py(py); - let cls = py.get_type_bound::(); + let cls = py.get_type::(); pyo3::py_run!(py, circle square cls, r#" assert isinstance(circle, cls) assert isinstance(circle, cls.Circle) @@ -1299,7 +1299,7 @@ enum MyEnum { Python::with_gil(|py| { let x = Py::new(py, MyEnum::Variant { i: 42 }).unwrap(); - let cls = py.get_type_bound::(); + let cls = py.get_type::(); pyo3::py_run!(py, x cls, r#" assert isinstance(x, cls) assert not isinstance(x, cls.Variant) @@ -1325,7 +1325,7 @@ enum Shape { # #[cfg(Py_3_10)] Python::with_gil(|py| { - let cls = py.get_type_bound::(); + let cls = py.get_type::(); pyo3::py_run!(py, cls, r#" circle = cls.Circle() assert isinstance(circle, cls) @@ -1446,7 +1446,7 @@ impl pyo3::impl_::pyclass::PyClassImpl for MyClass { } # Python::with_gil(|py| { -# let cls = py.get_type_bound::(); +# let cls = py.get_type::(); # pyo3::py_run!(py, cls, "assert cls.__name__ == 'MyClass'") # }); # } diff --git a/guide/src/exception.md b/guide/src/exception.md index 0235614f6ea..32a56078af5 100644 --- a/guide/src/exception.md +++ b/guide/src/exception.md @@ -24,7 +24,7 @@ use pyo3::exceptions::PyException; create_exception!(mymodule, CustomError, PyException); Python::with_gil(|py| { - let ctx = [("CustomError", py.get_type_bound::())].into_py_dict(py); + let ctx = [("CustomError", py.get_type::())].into_py_dict(py); pyo3::py_run!( py, *ctx, @@ -46,7 +46,7 @@ pyo3::create_exception!(mymodule, CustomError, PyException); #[pymodule] fn mymodule(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { // ... other elements added to module ... - m.add("CustomError", py.get_type_bound::())?; + m.add("CustomError", py.get_type::())?; Ok(()) } diff --git a/guide/src/migration.md b/guide/src/migration.md index c55900651d5..1abb7202e4d 100644 --- a/guide/src/migration.md +++ b/guide/src/migration.md @@ -214,7 +214,7 @@ Python::with_gil(|py| { After: -```rust +```rust,ignore # #![allow(dead_code)] # use pyo3::prelude::*; # use pyo3::types::{PyBool}; @@ -593,7 +593,7 @@ assert_eq!(name, "list"); After: -```rust +```rust,ignore # #[cfg(any(not(Py_LIMITED_API), Py_3_10))] { # use pyo3::prelude::*; # use pyo3::types::{PyList, PyType}; @@ -614,7 +614,7 @@ To avoid needing to worry about lifetimes at all, it is also possible to use the The following example uses the same snippet as those just above, but this time the final extracted type is `PyBackedStr`: -```rust +```rust,ignore # use pyo3::prelude::*; # use pyo3::types::{PyList, PyType}; # fn example<'py>(py: Python<'py>) -> PyResult<()> { diff --git a/guide/src/python-from-rust/calling-existing-code.md b/guide/src/python-from-rust/calling-existing-code.md index dd4a28458ed..997009310b4 100644 --- a/guide/src/python-from-rust/calling-existing-code.md +++ b/guide/src/python-from-rust/calling-existing-code.md @@ -291,7 +291,7 @@ fn main() -> PyResult<()> { let py_app = fs::read_to_string(path.join("app.py"))?; let from_python = Python::with_gil(|py| -> PyResult> { let syspath = py - .import_bound("sys")? + .import("sys")? .getattr("path")? .downcast_into::()?; syspath.insert(0, &path)?; @@ -383,7 +383,7 @@ use pyo3::prelude::*; # fn main() -> PyResult<()> { Python::with_gil(|py| -> PyResult<()> { - let signal = py.import_bound("signal")?; + let signal = py.import("signal")?; // Set SIGINT to have the default action signal .getattr("signal")? diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index 50c123a90fb..164b5f7e921 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -1535,7 +1535,7 @@ pub fn gen_complex_enum_variant_attr( let variant_cls = format_ident!("{}_{}", cls, member); let associated_method = quote! { fn #wrapper_ident(py: #pyo3_path::Python<'_>) -> #pyo3_path::PyResult<#pyo3_path::PyObject> { - ::std::result::Result::Ok(py.get_type_bound::<#variant_cls>().into_any().unbind()) + ::std::result::Result::Ok(py.get_type::<#variant_cls>().into_any().unbind()) } }; diff --git a/src/buffer.rs b/src/buffer.rs index 2a6d602d567..f2e402aecd4 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -880,7 +880,7 @@ mod tests { fn test_array_buffer() { Python::with_gil(|py| { let array = py - .import_bound("array") + .import("array") .unwrap() .call_method("array", ("f", (1.0, 1.5, 2.0, 2.5)), None) .unwrap(); diff --git a/src/conversions/chrono.rs b/src/conversions/chrono.rs index 1da39cdb32b..47c22f0c18f 100644 --- a/src/conversions/chrono.rs +++ b/src/conversions/chrono.rs @@ -678,7 +678,7 @@ fn warn_truncated_leap_second(obj: &Bound<'_, PyAny>) { let py = obj.py(); if let Err(e) = PyErr::warn_bound( py, - &py.get_type_bound::(), + &py.get_type::(), "ignored leap-second, `datetime` does not support leap-seconds", 0, ) { @@ -762,7 +762,7 @@ impl DatetimeTypes { fn try_get(py: Python<'_>) -> PyResult<&Self> { static TYPES: GILOnceCell = GILOnceCell::new(); TYPES.get_or_try_init(py, || { - let datetime = py.import_bound("datetime")?; + let datetime = py.import("datetime")?; let timezone = datetime.getattr("timezone")?; Ok::<_, PyErr>(Self { date: datetime.getattr("date")?.into(), @@ -1297,7 +1297,7 @@ mod tests { name: &str, args: impl IntoPy>, ) -> Bound<'py, PyAny> { - py.import_bound("datetime") + py.import("datetime") .unwrap() .getattr(name) .unwrap() @@ -1306,7 +1306,7 @@ mod tests { } fn python_utc(py: Python<'_>) -> Bound<'_, PyAny> { - py.import_bound("datetime") + py.import("datetime") .unwrap() .getattr("timezone") .unwrap() @@ -1328,7 +1328,7 @@ mod tests { fn test_pyo3_offset_fixed_frompyobject_created_in_python(timestamp in 0..(i32::MAX as i64), timedelta in -86399i32..=86399i32) { Python::with_gil(|py| { - let globals = [("datetime", py.import_bound("datetime").unwrap())].into_py_dict(py); + let globals = [("datetime", py.import("datetime").unwrap())].into_py_dict(py); let code = format!("datetime.datetime.fromtimestamp({}).replace(tzinfo=datetime.timezone(datetime.timedelta(seconds={})))", timestamp, timedelta); let t = py.eval_bound(&code, Some(&globals), None).unwrap(); diff --git a/src/conversions/chrono_tz.rs b/src/conversions/chrono_tz.rs index 1b5e3cd95f7..88bf39de64a 100644 --- a/src/conversions/chrono_tz.rs +++ b/src/conversions/chrono_tz.rs @@ -128,9 +128,6 @@ mod tests { } fn zoneinfo_class(py: Python<'_>) -> Bound<'_, PyAny> { - py.import_bound("zoneinfo") - .unwrap() - .getattr("ZoneInfo") - .unwrap() + py.import("zoneinfo").unwrap().getattr("ZoneInfo").unwrap() } } diff --git a/src/conversions/num_bigint.rs b/src/conversions/num_bigint.rs index 74322bde482..d709824d702 100644 --- a/src/conversions/num_bigint.rs +++ b/src/conversions/num_bigint.rs @@ -121,7 +121,7 @@ macro_rules! bigint_conversion { } else { None }; - py.get_type_bound::() + py.get_type::() .call_method("from_bytes", (bytes_obj, "little"), kwargs.as_ref()) .expect("int.from_bytes() failed during to_object()") // FIXME: #1813 or similar .into() @@ -170,7 +170,7 @@ macro_rules! bigint_conversion { None }; unsafe { - py.get_type_bound::() + py.get_type::() .call_method("from_bytes", (bytes_obj, "little"), kwargs.as_ref()) .downcast_into_unchecked() } diff --git a/src/conversions/std/time.rs b/src/conversions/std/time.rs index 31d7f965883..4c5c56e4cac 100755 --- a/src/conversions/std/time.rs +++ b/src/conversions/std/time.rs @@ -194,7 +194,7 @@ fn unix_epoch_py(py: Python<'_>) -> &PyObject { } #[cfg(Py_LIMITED_API)] { - let datetime = py.import_bound("datetime")?; + let datetime = py.import("datetime")?; let utc = datetime.getattr("timezone")?.getattr("utc")?; Ok::<_, PyErr>( datetime @@ -412,7 +412,7 @@ mod tests { } fn tz_utc(py: Python<'_>) -> Bound<'_, PyAny> { - py.import_bound("datetime") + py.import("datetime") .unwrap() .getattr("timezone") .unwrap() @@ -432,16 +432,10 @@ mod tests { } fn datetime_class(py: Python<'_>) -> Bound<'_, PyAny> { - py.import_bound("datetime") - .unwrap() - .getattr("datetime") - .unwrap() + py.import("datetime").unwrap().getattr("datetime").unwrap() } fn timedelta_class(py: Python<'_>) -> Bound<'_, PyAny> { - py.import_bound("datetime") - .unwrap() - .getattr("timedelta") - .unwrap() + py.import("datetime").unwrap().getattr("timedelta").unwrap() } } diff --git a/src/coroutine/waker.rs b/src/coroutine/waker.rs index bb93974aa1e..1600f56d9c6 100644 --- a/src/coroutine/waker.rs +++ b/src/coroutine/waker.rs @@ -60,7 +60,7 @@ impl LoopAndFuture { fn new(py: Python<'_>) -> PyResult { static GET_RUNNING_LOOP: GILOnceCell = GILOnceCell::new(); let import = || -> PyResult<_> { - let module = py.import_bound("asyncio")?; + let module = py.import("asyncio")?; Ok(module.getattr("get_running_loop")?.into()) }; let event_loop = GET_RUNNING_LOOP.get_or_try_init(py, import)?.call0(py)?; diff --git a/src/err/mod.rs b/src/err/mod.rs index 3e344c4a66f..6e1e00844d8 100644 --- a/src/err/mod.rs +++ b/src/err/mod.rs @@ -596,7 +596,7 @@ impl PyErr { /// # use pyo3::prelude::*; /// # fn main() -> PyResult<()> { /// Python::with_gil(|py| { - /// let user_warning = py.get_type_bound::(); + /// let user_warning = py.get_type::(); /// PyErr::warn_bound(py, &user_warning, "I am warning you", 0)?; /// Ok(()) /// }) @@ -1125,10 +1125,10 @@ mod tests { // GIL locked should prevent effects to be visible to other testing // threads. Python::with_gil(|py| { - let cls = py.get_type_bound::(); + let cls = py.get_type::(); // Reset warning filter to default state - let warnings = py.import_bound("warnings").unwrap(); + let warnings = py.import("warnings").unwrap(); warnings.call_method0("resetwarnings").unwrap(); // First, test the warning is emitted diff --git a/src/exceptions.rs b/src/exceptions.rs index e2649edd08b..ee35e4752e1 100644 --- a/src/exceptions.rs +++ b/src/exceptions.rs @@ -62,7 +62,7 @@ macro_rules! impl_exception_boilerplate_bound { /// import_exception!(socket, gaierror); /// /// Python::with_gil(|py| { -/// let ctx = [("gaierror", py.get_type_bound::())].into_py_dict(py); +/// let ctx = [("gaierror", py.get_type::())].into_py_dict(py); /// pyo3::py_run!(py, *ctx, "import socket; assert gaierror is socket.gaierror"); /// }); /// @@ -168,7 +168,7 @@ macro_rules! import_exception_bound { /// /// #[pymodule] /// fn my_module(m: &Bound<'_, PyModule>) -> PyResult<()> { -/// m.add("MyError", m.py().get_type_bound::())?; +/// m.add("MyError", m.py().get_type::())?; /// m.add_function(wrap_pyfunction!(raise_myerror, m)?)?; /// Ok(()) /// } @@ -176,7 +176,7 @@ macro_rules! import_exception_bound { /// # Python::with_gil(|py| -> PyResult<()> { /// # let fun = wrap_pyfunction!(raise_myerror, py)?; /// # let locals = pyo3::types::PyDict::new(py); -/// # locals.set_item("MyError", py.get_type_bound::())?; +/// # locals.set_item("MyError", py.get_type::())?; /// # locals.set_item("raise_myerror", fun)?; /// # /// # py.run_bound( @@ -258,7 +258,7 @@ macro_rules! create_exception_type_object { py, concat!(stringify!($module), ".", stringify!($name)), $doc, - ::std::option::Option::Some(&py.get_type_bound::<$base>()), + ::std::option::Option::Some(&py.get_type::<$base>()), ::std::option::Option::None, ).expect("Failed to initialize new exception type.") ).as_ptr() as *mut $crate::ffi::PyTypeObject @@ -822,7 +822,7 @@ mod tests { Python::with_gil(|py| { let err: PyErr = gaierror::new_err(()); let socket = py - .import_bound("socket") + .import("socket") .map_err(|e| e.display(py)) .expect("could not import socket"); @@ -846,7 +846,7 @@ mod tests { Python::with_gil(|py| { let err: PyErr = MessageError::new_err(()); let email = py - .import_bound("email") + .import("email") .map_err(|e| e.display(py)) .expect("could not import email"); @@ -873,7 +873,7 @@ mod tests { create_exception!(mymodule, CustomError, PyException); Python::with_gil(|py| { - let error_type = py.get_type_bound::(); + let error_type = py.get_type::(); let ctx = [("CustomError", error_type)].into_py_dict(py); let type_description: String = py .eval_bound("str(CustomError)", None, Some(&ctx)) @@ -896,7 +896,7 @@ mod tests { fn custom_exception_dotted_module() { create_exception!(mymodule.exceptions, CustomError, PyException); Python::with_gil(|py| { - let error_type = py.get_type_bound::(); + let error_type = py.get_type::(); let ctx = [("CustomError", error_type)].into_py_dict(py); let type_description: String = py .eval_bound("str(CustomError)", None, Some(&ctx)) @@ -915,7 +915,7 @@ mod tests { create_exception!(mymodule, CustomError, PyException, "Some docs"); Python::with_gil(|py| { - let error_type = py.get_type_bound::(); + let error_type = py.get_type::(); let ctx = [("CustomError", error_type)].into_py_dict(py); let type_description: String = py .eval_bound("str(CustomError)", None, Some(&ctx)) @@ -948,7 +948,7 @@ mod tests { ); Python::with_gil(|py| { - let error_type = py.get_type_bound::(); + let error_type = py.get_type::(); let ctx = [("CustomError", error_type)].into_py_dict(py); let type_description: String = py .eval_bound("str(CustomError)", None, Some(&ctx)) diff --git a/src/gil.rs b/src/gil.rs index 644fbe6e00a..71b95e55b18 100644 --- a/src/gil.rs +++ b/src/gil.rs @@ -123,7 +123,7 @@ where let py = guard.python(); // Import the threading module - this ensures that it will associate this thread as the "main" // thread, which is important to avoid an `AssertionError` at finalization. - py.import_bound("threading").unwrap(); + py.import("threading").unwrap(); // Execute the closure. f(py) diff --git a/src/impl_/extract_argument.rs b/src/impl_/extract_argument.rs index 3466dc268ea..e2a184ce7dd 100644 --- a/src/impl_/extract_argument.rs +++ b/src/impl_/extract_argument.rs @@ -200,10 +200,7 @@ pub fn from_py_with_with_default<'a, 'py, T>( #[doc(hidden)] #[cold] pub fn argument_extraction_error(py: Python<'_>, arg_name: &str, error: PyErr) -> PyErr { - if error - .get_type_bound(py) - .is(&py.get_type_bound::()) - { + if error.get_type_bound(py).is(&py.get_type::()) { let remapped_error = PyTypeError::new_err(format!( "argument '{}': {}", arg_name, diff --git a/src/impl_/pymodule.rs b/src/impl_/pymodule.rs index 2cdb1bff8f8..08d55bfa5e8 100644 --- a/src/impl_/pymodule.rs +++ b/src/impl_/pymodule.rs @@ -92,11 +92,11 @@ impl ModuleDef { use crate::types::any::PyAnyMethods; const PYPY_GOOD_VERSION: [u8; 3] = [7, 3, 8]; let version = py - .import_bound("sys")? + .import("sys")? .getattr("implementation")? .getattr("version")?; if version.lt(crate::types::PyTuple::new(py, PYPY_GOOD_VERSION))? { - let warn = py.import_bound("warnings")?.getattr("warn")?; + let warn = py.import("warnings")?.getattr("warn")?; warn.call1(( "PyPy 3.7 versions older than 7.3.8 are known to have binary \ compatibility issues which may cause segfaults. Please upgrade.", @@ -286,7 +286,7 @@ mod tests { assert_eq!((*module_def.ffi_def.get()).m_doc, DOC.as_ptr() as _); Python::with_gil(|py| { - module_def.initializer.0(&py.import_bound("builtins").unwrap()).unwrap(); + module_def.initializer.0(&py.import("builtins").unwrap()).unwrap(); assert!(INIT_CALLED.load(Ordering::SeqCst)); }) } diff --git a/src/instance.rs b/src/instance.rs index 96b3f3e554c..773431859c9 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -1422,7 +1422,7 @@ impl Py { /// } /// # /// # Python::with_gil(|py| { - /// # let sys = py.import_bound("sys").unwrap().unbind(); + /// # let sys = py.import("sys").unwrap().unbind(); /// # version(sys, py).unwrap(); /// # }); /// ``` @@ -1906,7 +1906,7 @@ mod tests { #[test] fn test_call() { Python::with_gil(|py| { - let obj = py.get_type_bound::().to_object(py); + let obj = py.get_type::().to_object(py); let assert_repr = |obj: &Bound<'_, PyAny>, expected: &str| { assert_eq!(obj.repr().unwrap(), expected); diff --git a/src/lib.rs b/src/lib.rs index 82f1d271cf7..267639a271d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -235,10 +235,10 @@ //! //! fn main() -> PyResult<()> { //! Python::with_gil(|py| { -//! let sys = py.import_bound("sys")?; +//! let sys = py.import("sys")?; //! let version: String = sys.getattr("version")?.extract()?; //! -//! let locals = [("os", py.import_bound("os")?)].into_py_dict(py); +//! let locals = [("os", py.import("os")?)].into_py_dict(py); //! let code = "os.getenv('USER') or os.getenv('USERNAME') or 'Unknown'"; //! let user: String = py.eval_bound(code, None, Some(&locals))?.extract()?; //! diff --git a/src/macros.rs b/src/macros.rs index d2aff4bcbf1..d121efb367e 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -73,7 +73,7 @@ /// } /// /// Python::with_gil(|py| { -/// let locals = [("C", py.get_type_bound::())].into_py_dict(py); +/// let locals = [("C", py.get_type::())].into_py_dict(py); /// pyo3::py_run!(py, *locals, "c = C()"); /// }); /// ``` diff --git a/src/marker.rs b/src/marker.rs index e2d9a7938ec..778ccf61c7d 100644 --- a/src/marker.rs +++ b/src/marker.rs @@ -651,21 +651,43 @@ impl<'py> Python<'py> { /// Gets the Python type object for type `T`. #[inline] - pub fn get_type_bound(self) -> Bound<'py, PyType> + pub fn get_type(self) -> Bound<'py, PyType> where T: PyTypeInfo, { T::type_object_bound(self) } + /// Deprecated name for [`Python::get_type`]. + #[deprecated(since = "0.23.0", note = "renamed to `Python::get_type`")] + #[track_caller] + #[inline] + pub fn get_type_bound(self) -> Bound<'py, PyType> + where + T: PyTypeInfo, + { + self.get_type::() + } + /// Imports the Python module with the specified name. - pub fn import_bound(self, name: N) -> PyResult> + pub fn import(self, name: N) -> PyResult> where N: IntoPy>, { PyModule::import_bound(self, name) } + /// Deprecated name for [`Python::import`]. + #[deprecated(since = "0.23.0", note = "renamed to `Python::import`")] + #[track_caller] + #[inline] + pub fn import_bound(self, name: N) -> PyResult> + where + N: IntoPy>, + { + self.import(name) + } + /// Gets the Python builtin value `None`. #[allow(non_snake_case)] // the Python keyword starts with uppercase #[inline] diff --git a/src/pyclass_init.rs b/src/pyclass_init.rs index 2e73c1518d8..28b4a1cd719 100644 --- a/src/pyclass_init.rs +++ b/src/pyclass_init.rs @@ -121,7 +121,7 @@ impl PyObjectInit for PyNativeTypeInitializer { /// } /// } /// Python::with_gil(|py| { -/// let typeobj = py.get_type_bound::(); +/// let typeobj = py.get_type::(); /// let sub_sub_class = typeobj.call((), None).unwrap(); /// py_run!( /// py, diff --git a/src/sync.rs b/src/sync.rs index 441afa08cc2..60c8c4747a2 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -210,7 +210,7 @@ impl GILOnceCell> { ) -> PyResult<&Bound<'py, PyType>> { self.get_or_try_init(py, || { let type_object = py - .import_bound(module_name)? + .import(module_name)? .getattr(attr_name)? .downcast_into()?; Ok(type_object.unbind()) diff --git a/src/tests/common.rs b/src/tests/common.rs index c56249c2796..8180c4cd1af 100644 --- a/src/tests/common.rs +++ b/src/tests/common.rs @@ -42,7 +42,7 @@ mod inner { ($py:expr, *$dict:expr, $code:expr, $err:ident) => {{ let res = $py.run_bound($code, None, Some(&$dict.as_borrowed())); let err = res.expect_err(&format!("Did not raise {}", stringify!($err))); - if !err.matches($py, $py.get_type_bound::()) { + if !err.matches($py, $py.get_type::()) { panic!("Expected {} but got {:?}", stringify!($err), err) } err @@ -83,7 +83,7 @@ mod inner { #[cfg(all(feature = "macros", Py_3_8))] impl UnraisableCapture { pub fn install(py: Python<'_>) -> Py { - let sys = py.import_bound("sys").unwrap(); + let sys = py.import("sys").unwrap(); let old_hook = sys.getattr("unraisablehook").unwrap().into(); let capture = Py::new( @@ -104,7 +104,7 @@ mod inner { pub fn uninstall(&mut self, py: Python<'_>) { let old_hook = self.old_hook.take().unwrap(); - let sys = py.import_bound("sys").unwrap(); + let sys = py.import("sys").unwrap(); sys.setattr("unraisablehook", old_hook).unwrap(); } } @@ -118,7 +118,7 @@ mod inner { py: Python<'py>, f: impl FnOnce(&Bound<'py, PyList>) -> PyResult, ) -> PyResult { - let warnings = py.import_bound("warnings")?; + let warnings = py.import("warnings")?; let kwargs = [("record", true)].into_py_dict(py); let catch_warnings = warnings .getattr("catch_warnings")? diff --git a/src/types/any.rs b/src/types/any.rs index 72f141eec02..d3322d1c33f 100644 --- a/src/types/any.rs +++ b/src/types/any.rs @@ -75,7 +75,7 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed { /// } /// # /// # Python::with_gil(|py| { - /// # let sys = py.import_bound("sys").unwrap(); + /// # let sys = py.import("sys").unwrap(); /// # has_version(&sys).unwrap(); /// # }); /// ``` @@ -101,7 +101,7 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed { /// } /// # /// # Python::with_gil(|py| { - /// # let sys = py.import_bound("sys").unwrap(); + /// # let sys = py.import("sys").unwrap(); /// # version(&sys).unwrap(); /// # }); /// ``` @@ -1738,7 +1738,7 @@ class SimpleClass: fn test_any_is_instance() { Python::with_gil(|py| { let l = vec![1u8, 2].to_object(py).into_bound(py); - assert!(l.is_instance(&py.get_type_bound::()).unwrap()); + assert!(l.is_instance(&py.get_type::()).unwrap()); }); } @@ -1762,9 +1762,9 @@ class SimpleClass: fn test_any_is_exact_instance() { Python::with_gil(|py| { let t = PyBool::new(py, true); - assert!(t.is_instance(&py.get_type_bound::()).unwrap()); - assert!(!t.is_exact_instance(&py.get_type_bound::())); - assert!(t.is_exact_instance(&py.get_type_bound::())); + assert!(t.is_instance(&py.get_type::()).unwrap()); + assert!(!t.is_exact_instance(&py.get_type::())); + assert!(t.is_exact_instance(&py.get_type::())); }); } diff --git a/src/types/dict.rs b/src/types/dict.rs index 20664541f0c..0fb6a711013 100644 --- a/src/types/dict.rs +++ b/src/types/dict.rs @@ -748,7 +748,7 @@ mod tests { } Python::with_gil(|py| { - let class = py.get_type_bound::(); + let class = py.get_type::(); let instance = class.call0().unwrap(); let d = PyDict::new(py); match d.get_item(instance) { @@ -1188,7 +1188,7 @@ mod tests { let dict = abc_dict(py); let keys = dict.call_method0("keys").unwrap(); assert!(keys - .is_instance(&py.get_type_bound::().as_borrowed()) + .is_instance(&py.get_type::().as_borrowed()) .unwrap()); }) } @@ -1200,7 +1200,7 @@ mod tests { let dict = abc_dict(py); let values = dict.call_method0("values").unwrap(); assert!(values - .is_instance(&py.get_type_bound::().as_borrowed()) + .is_instance(&py.get_type::().as_borrowed()) .unwrap()); }) } @@ -1212,7 +1212,7 @@ mod tests { let dict = abc_dict(py); let items = dict.call_method0("items").unwrap(); assert!(items - .is_instance(&py.get_type_bound::().as_borrowed()) + .is_instance(&py.get_type::().as_borrowed()) .unwrap()); }) } diff --git a/src/types/string.rs b/src/types/string.rs index 2701e331ed7..e455389e47d 100644 --- a/src/types/string.rs +++ b/src/types/string.rs @@ -695,7 +695,7 @@ mod tests { let err = data.to_string(py).unwrap_err(); assert!(err .get_type_bound(py) - .is(&py.get_type_bound::())); + .is(&py.get_type::())); assert!(err .to_string() .contains("'utf-8' codec can't decode byte 0xfe in position 1")); @@ -739,7 +739,7 @@ mod tests { let err = data.to_string(py).unwrap_err(); assert!(err .get_type_bound(py) - .is(&py.get_type_bound::())); + .is(&py.get_type::())); assert!(err .to_string() .contains("'utf-16' codec can't decode bytes in position 0-3")); @@ -780,7 +780,7 @@ mod tests { let err = data.to_string(py).unwrap_err(); assert!(err .get_type_bound(py) - .is(&py.get_type_bound::())); + .is(&py.get_type::())); assert!(err .to_string() .contains("'utf-32' codec can't decode bytes in position 0-7")); diff --git a/src/types/traceback.rs b/src/types/traceback.rs index ef0062c6af9..98e40632439 100644 --- a/src/types/traceback.rs +++ b/src/types/traceback.rs @@ -63,7 +63,7 @@ impl<'py> PyTracebackMethods<'py> for Bound<'py, PyTraceback> { fn format(&self) -> PyResult { let py = self.py(); let string_io = py - .import_bound(intern!(py, "io"))? + .import(intern!(py, "io"))? .getattr(intern!(py, "StringIO"))? .call0()?; let result = unsafe { ffi::PyTraceBack_Print(self.as_ptr(), string_io.as_ptr()) }; diff --git a/src/types/typeobject.rs b/src/types/typeobject.rs index bbb1e5ef4cb..0c3b0a6aaa5 100644 --- a/src/types/typeobject.rs +++ b/src/types/typeobject.rs @@ -257,8 +257,8 @@ mod tests { #[test] fn test_type_is_subclass() { Python::with_gil(|py| { - let bool_type = py.get_type_bound::(); - let long_type = py.get_type_bound::(); + let bool_type = py.get_type::(); + let long_type = py.get_type::(); assert!(bool_type.is_subclass(&long_type).unwrap()); }); } @@ -266,10 +266,7 @@ mod tests { #[test] fn test_type_is_subclass_of() { Python::with_gil(|py| { - assert!(py - .get_type_bound::() - .is_subclass_of::() - .unwrap()); + assert!(py.get_type::().is_subclass_of::().unwrap()); }); } @@ -277,14 +274,14 @@ mod tests { fn test_mro() { Python::with_gil(|py| { assert!(py - .get_type_bound::() + .get_type::() .mro() .eq(PyTuple::new( py, [ - py.get_type_bound::(), - py.get_type_bound::(), - py.get_type_bound::() + py.get_type::(), + py.get_type::(), + py.get_type::() ] )) .unwrap()); @@ -295,9 +292,9 @@ mod tests { fn test_bases_bool() { Python::with_gil(|py| { assert!(py - .get_type_bound::() + .get_type::() .bases() - .eq(PyTuple::new(py, [py.get_type_bound::()])) + .eq(PyTuple::new(py, [py.get_type::()])) .unwrap()); }); } @@ -306,7 +303,7 @@ mod tests { fn test_bases_object() { Python::with_gil(|py| { assert!(py - .get_type_bound::() + .get_type::() .bases() .eq(PyTuple::empty(py)) .unwrap()); @@ -342,7 +339,7 @@ class MyClass: #[test] fn test_type_names_builtin() { Python::with_gil(|py| { - let bool_type = py.get_type_bound::(); + let bool_type = py.get_type::(); assert_eq!(bool_type.name().unwrap(), "bool"); assert_eq!(bool_type.qualname().unwrap(), "bool"); assert_eq!(bool_type.module().unwrap(), "builtins"); diff --git a/tests/test_class_attributes.rs b/tests/test_class_attributes.rs index 53ed16320d0..3f8eb74df5f 100644 --- a/tests/test_class_attributes.rs +++ b/tests/test_class_attributes.rs @@ -56,7 +56,7 @@ impl Foo { #[test] fn class_attributes() { Python::with_gil(|py| { - let foo_obj = py.get_type_bound::(); + let foo_obj = py.get_type::(); py_assert!(py, foo_obj, "foo_obj.MY_CONST == 'foobar'"); py_assert!(py, foo_obj, "foo_obj.RENAMED_CONST == 'foobar_2'"); py_assert!(py, foo_obj, "foo_obj.a == 5"); @@ -72,7 +72,7 @@ fn class_attributes() { #[ignore] fn class_attributes_are_immutable() { Python::with_gil(|py| { - let foo_obj = py.get_type_bound::(); + let foo_obj = py.get_type::(); py_expect_exception!(py, foo_obj, "foo_obj.a = 6", PyTypeError); }); } @@ -88,8 +88,8 @@ impl Bar { #[test] fn recursive_class_attributes() { Python::with_gil(|py| { - let foo_obj = py.get_type_bound::(); - let bar_obj = py.get_type_bound::(); + let foo_obj = py.get_type::(); + let bar_obj = py.get_type::(); py_assert!(py, foo_obj, "foo_obj.a_foo.x == 1"); py_assert!(py, foo_obj, "foo_obj.bar.x == 2"); py_assert!(py, bar_obj, "bar_obj.a_foo.x == 3"); @@ -107,9 +107,9 @@ fn test_fallible_class_attribute() { impl<'py> CaptureStdErr<'py> { fn new(py: Python<'py>) -> PyResult { - let sys = py.import_bound("sys")?; + let sys = py.import("sys")?; let oldstderr = sys.getattr("stderr")?; - let string_io = py.import_bound("io")?.getattr("StringIO")?.call0()?; + let string_io = py.import("io")?.getattr("StringIO")?.call0()?; sys.setattr("stderr", &string_io)?; Ok(Self { oldstderr, @@ -126,7 +126,7 @@ fn test_fallible_class_attribute() { .downcast::()? .to_cow()? .into_owned(); - let sys = py.import_bound("sys")?; + let sys = py.import("sys")?; sys.setattr("stderr", self.oldstderr)?; Ok(payload) } @@ -145,7 +145,7 @@ fn test_fallible_class_attribute() { Python::with_gil(|py| { let stderr = CaptureStdErr::new(py).unwrap(); - assert!(std::panic::catch_unwind(|| py.get_type_bound::()).is_err()); + assert!(std::panic::catch_unwind(|| py.get_type::()).is_err()); assert_eq!( stderr.reset().unwrap().trim(), "\ @@ -187,7 +187,7 @@ fn test_renaming_all_struct_fields() { use pyo3::types::PyBool; Python::with_gil(|py| { - let struct_class = py.get_type_bound::(); + let struct_class = py.get_type::(); let struct_obj = struct_class.call0().unwrap(); assert!(struct_obj .setattr("firstField", PyBool::new(py, false)) @@ -220,7 +220,7 @@ macro_rules! test_case { //use pyo3::types::PyInt; Python::with_gil(|py| { - let struct_class = py.get_type_bound::<$struct_name>(); + let struct_class = py.get_type::<$struct_name>(); let struct_obj = struct_class.call0().unwrap(); assert!(struct_obj.setattr($renamed_field_name, 2).is_ok()); let attr = struct_obj.getattr($renamed_field_name).unwrap(); diff --git a/tests/test_class_basics.rs b/tests/test_class_basics.rs index cb42e532154..947ab66f894 100644 --- a/tests/test_class_basics.rs +++ b/tests/test_class_basics.rs @@ -13,7 +13,7 @@ struct EmptyClass {} #[test] fn empty_class() { Python::with_gil(|py| { - let typeobj = py.get_type_bound::(); + let typeobj = py.get_type::(); // By default, don't allow creating instances from python. assert!(typeobj.call((), None).is_err()); @@ -27,7 +27,7 @@ struct UnitClass; #[test] fn unit_class() { Python::with_gil(|py| { - let typeobj = py.get_type_bound::(); + let typeobj = py.get_type::(); // By default, don't allow creating instances from python. assert!(typeobj.call((), None).is_err()); @@ -58,7 +58,7 @@ struct ClassWithDocs { #[test] fn class_with_docstr() { Python::with_gil(|py| { - let typeobj = py.get_type_bound::(); + let typeobj = py.get_type::(); py_run!( py, typeobj, @@ -104,7 +104,7 @@ impl EmptyClass2 { #[test] fn custom_names() { Python::with_gil(|py| { - let typeobj = py.get_type_bound::(); + let typeobj = py.get_type::(); py_assert!(py, typeobj, "typeobj.__name__ == 'CustomName'"); py_assert!(py, typeobj, "typeobj.custom_fn.__name__ == 'custom_fn'"); py_assert!( @@ -142,7 +142,7 @@ impl ClassRustKeywords { #[test] fn keyword_names() { Python::with_gil(|py| { - let typeobj = py.get_type_bound::(); + let typeobj = py.get_type::(); py_assert!(py, typeobj, "typeobj.__name__ == 'loop'"); py_assert!(py, typeobj, "typeobj.struct.__name__ == 'struct'"); py_assert!(py, typeobj, "typeobj.type.__name__ == 'type'"); @@ -167,7 +167,7 @@ impl RawIdents { #[test] fn test_raw_idents() { Python::with_gil(|py| { - let typeobj = py.get_type_bound::(); + let typeobj = py.get_type::(); py_assert!(py, typeobj, "not hasattr(typeobj, 'r#fn')"); py_assert!(py, typeobj, "hasattr(typeobj, 'fn')"); py_assert!(py, typeobj, "hasattr(typeobj, 'type')"); @@ -221,7 +221,7 @@ impl ClassWithObjectField { #[test] fn class_with_object_field() { Python::with_gil(|py| { - let ty = py.get_type_bound::(); + let ty = py.get_type::(); py_assert!(py, ty, "ty(5).value == 5"); py_assert!(py, ty, "ty(None).value == None"); }); @@ -405,7 +405,7 @@ struct TupleClass(#[pyo3(get, set, name = "value")] i32); #[test] fn test_tuple_struct_class() { Python::with_gil(|py| { - let typeobj = py.get_type_bound::(); + let typeobj = py.get_type::(); assert!(typeobj.call((), None).is_err()); py_assert!(py, typeobj, "typeobj.__name__ == 'TupleClass'"); diff --git a/tests/test_class_new.rs b/tests/test_class_new.rs index b5e5a2b8c9c..f2de5df42e2 100644 --- a/tests/test_class_new.rs +++ b/tests/test_class_new.rs @@ -19,7 +19,7 @@ impl EmptyClassWithNew { #[test] fn empty_class_with_new() { Python::with_gil(|py| { - let typeobj = py.get_type_bound::(); + let typeobj = py.get_type::(); assert!(typeobj .call((), None) .unwrap() @@ -48,7 +48,7 @@ impl UnitClassWithNew { #[test] fn unit_class_with_new() { Python::with_gil(|py| { - let typeobj = py.get_type_bound::(); + let typeobj = py.get_type::(); assert!(typeobj .call((), None) .unwrap() @@ -71,7 +71,7 @@ impl TupleClassWithNew { #[test] fn tuple_class_with_new() { Python::with_gil(|py| { - let typeobj = py.get_type_bound::(); + let typeobj = py.get_type::(); let wrp = typeobj.call((42,), None).unwrap(); let obj = wrp.downcast::().unwrap(); let obj_ref = obj.borrow(); @@ -96,7 +96,7 @@ impl NewWithOneArg { #[test] fn new_with_one_arg() { Python::with_gil(|py| { - let typeobj = py.get_type_bound::(); + let typeobj = py.get_type::(); let wrp = typeobj.call((42,), None).unwrap(); let obj = wrp.downcast::().unwrap(); let obj_ref = obj.borrow(); @@ -124,7 +124,7 @@ impl NewWithTwoArgs { #[test] fn new_with_two_args() { Python::with_gil(|py| { - let typeobj = py.get_type_bound::(); + let typeobj = py.get_type::(); let wrp = typeobj .call((10, 20), None) .map_err(|e| e.display(py)) @@ -155,7 +155,7 @@ impl SuperClass { #[test] fn subclass_new() { Python::with_gil(|py| { - let super_cls = py.get_type_bound::(); + let super_cls = py.get_type::(); let source = pyo3::indoc::indoc!( r#" class Class(SuperClass): @@ -200,7 +200,7 @@ impl NewWithCustomError { #[test] fn new_with_custom_error() { Python::with_gil(|py| { - let typeobj = py.get_type_bound::(); + let typeobj = py.get_type::(); let err = typeobj.call0().unwrap_err(); assert_eq!(err.to_string(), "ValueError: custom error"); }); @@ -235,7 +235,7 @@ impl NewExisting { #[test] fn test_new_existing() { Python::with_gil(|py| { - let typeobj = py.get_type_bound::(); + let typeobj = py.get_type::(); let obj1 = typeobj.call1((0,)).unwrap(); let obj2 = typeobj.call1((0,)).unwrap(); @@ -273,7 +273,7 @@ impl NewReturnsPy { #[test] fn test_new_returns_py() { Python::with_gil(|py| { - let type_ = py.get_type_bound::(); + let type_ = py.get_type::(); let obj = type_.call0().unwrap(); assert!(obj.is_exact_instance_of::()); }) @@ -293,7 +293,7 @@ impl NewReturnsBound { #[test] fn test_new_returns_bound() { Python::with_gil(|py| { - let type_ = py.get_type_bound::(); + let type_ = py.get_type::(); let obj = type_.call0().unwrap(); assert!(obj.is_exact_instance_of::()); }) @@ -319,7 +319,7 @@ impl NewClassMethod { #[test] fn test_new_class_method() { pyo3::Python::with_gil(|py| { - let cls = py.get_type_bound::(); + let cls = py.get_type::(); pyo3::py_run!(py, cls, "assert cls().cls is cls"); }); } diff --git a/tests/test_coroutine.rs b/tests/test_coroutine.rs index d99e7f80448..fc382489911 100644 --- a/tests/test_coroutine.rs +++ b/tests/test_coroutine.rs @@ -73,7 +73,7 @@ fn test_coroutine_qualname() { "my_fn", wrap_pyfunction!(my_fn, gil).unwrap().as_borrowed().as_any(), ), - ("MyClass", gil.get_type_bound::().as_any()), + ("MyClass", gil.get_type::().as_any()), ] .into_py_dict(gil); py_run!(gil, *locals, &handle_windows(test)); @@ -148,7 +148,7 @@ fn cancelled_coroutine() { await task asyncio.run(main()) "#; - let globals = gil.import_bound("__main__").unwrap().dict(); + let globals = gil.import("__main__").unwrap().dict(); globals.set_item("sleep", sleep).unwrap(); let err = gil .run_bound( @@ -187,7 +187,7 @@ fn coroutine_cancel_handle() { return await task assert asyncio.run(main()) == 0 "#; - let globals = gil.import_bound("__main__").unwrap().dict(); + let globals = gil.import("__main__").unwrap().dict(); globals .set_item("cancellable_sleep", cancellable_sleep) .unwrap(); @@ -219,7 +219,7 @@ fn coroutine_is_cancelled() { await task asyncio.run(main()) "#; - let globals = gil.import_bound("__main__").unwrap().dict(); + let globals = gil.import("__main__").unwrap().dict(); globals.set_item("sleep_loop", sleep_loop).unwrap(); gil.run_bound( &pyo3::unindent::unindent(&handle_windows(test)), @@ -316,7 +316,7 @@ fn test_async_method_receiver() { assert False assert asyncio.run(coro3) == 1 "#; - let locals = [("Counter", gil.get_type_bound::())].into_py_dict(gil); + let locals = [("Counter", gil.get_type::())].into_py_dict(gil); py_run!(gil, *locals, test); }); @@ -351,7 +351,7 @@ fn test_async_method_receiver_with_other_args() { assert asyncio.run(v.set_value(10)) == 10 assert asyncio.run(v.get_value_plus_with(1, 1)) == 12 "#; - let locals = [("Value", gil.get_type_bound::())].into_py_dict(gil); + let locals = [("Value", gil.get_type::())].into_py_dict(gil); py_run!(gil, *locals, test); }); } diff --git a/tests/test_datetime.rs b/tests/test_datetime.rs index da820e3d264..93492fc33a3 100644 --- a/tests/test_datetime.rs +++ b/tests/test_datetime.rs @@ -10,7 +10,7 @@ fn _get_subclasses<'py>( args: &str, ) -> PyResult<(Bound<'py, PyAny>, Bound<'py, PyAny>, Bound<'py, PyAny>)> { // Import the class from Python and create some subclasses - let datetime = py.import_bound("datetime")?; + let datetime = py.import("datetime")?; let locals = [(py_type, datetime.getattr(py_type)?)].into_py_dict(py); diff --git a/tests/test_datetime_import.rs b/tests/test_datetime_import.rs index 619df891944..68ef776a37a 100644 --- a/tests/test_datetime_import.rs +++ b/tests/test_datetime_import.rs @@ -14,7 +14,7 @@ fn test_bad_datetime_module_panic() { std::fs::File::create(tmpdir.join("datetime.py")).unwrap(); Python::with_gil(|py: Python<'_>| { - let sys = py.import_bound("sys").unwrap(); + let sys = py.import("sys").unwrap(); sys.getattr("path") .unwrap() .call_method1("insert", (0, tmpdir)) diff --git a/tests/test_enum.rs b/tests/test_enum.rs index cedb7ebaadb..abe743ee9ca 100644 --- a/tests/test_enum.rs +++ b/tests/test_enum.rs @@ -16,7 +16,7 @@ pub enum MyEnum { #[test] fn test_enum_class_attr() { Python::with_gil(|py| { - let my_enum = py.get_type_bound::(); + let my_enum = py.get_type::(); let var = Py::new(py, MyEnum::Variant).unwrap(); py_assert!(py, my_enum var, "my_enum.Variant == var"); }) @@ -31,7 +31,7 @@ fn return_enum() -> MyEnum { fn test_return_enum() { Python::with_gil(|py| { let f = wrap_pyfunction!(return_enum)(py).unwrap(); - let mynum = py.get_type_bound::(); + let mynum = py.get_type::(); py_run!(py, f mynum, "assert f() == mynum.Variant") }); @@ -46,7 +46,7 @@ fn enum_arg(e: MyEnum) { fn test_enum_arg() { Python::with_gil(|py| { let f = wrap_pyfunction!(enum_arg)(py).unwrap(); - let mynum = py.get_type_bound::(); + let mynum = py.get_type::(); py_run!(py, f mynum, "f(mynum.OtherVariant)") }) @@ -63,7 +63,7 @@ enum CustomDiscriminant { fn test_custom_discriminant() { Python::with_gil(|py| { #[allow(non_snake_case)] - let CustomDiscriminant = py.get_type_bound::(); + let CustomDiscriminant = py.get_type::(); let one = Py::new(py, CustomDiscriminant::One).unwrap(); let two = Py::new(py, CustomDiscriminant::Two).unwrap(); py_run!(py, CustomDiscriminant one two, r#" @@ -189,7 +189,7 @@ enum RenameAllVariantsEnum { #[test] fn test_renaming_all_enum_variants() { Python::with_gil(|py| { - let enum_obj = py.get_type_bound::(); + let enum_obj = py.get_type::(); py_assert!(py, enum_obj, "enum_obj.VARIANT_ONE == enum_obj.VARIANT_ONE"); py_assert!(py, enum_obj, "enum_obj.VARIANT_TWO == enum_obj.VARIANT_TWO"); py_assert!( @@ -209,7 +209,7 @@ enum CustomModuleComplexEnum { #[test] fn test_custom_module() { Python::with_gil(|py| { - let enum_obj = py.get_type_bound::(); + let enum_obj = py.get_type::(); py_assert!( py, enum_obj, @@ -340,7 +340,7 @@ mod deprecated { fn test_custom_discriminant() { Python::with_gil(|py| { #[allow(non_snake_case)] - let CustomDiscriminant = py.get_type_bound::(); + let CustomDiscriminant = py.get_type::(); let one = Py::new(py, CustomDiscriminant::One).unwrap(); let two = Py::new(py, CustomDiscriminant::Two).unwrap(); py_run!(py, CustomDiscriminant one two, r#" diff --git a/tests/test_gc.rs b/tests/test_gc.rs index b95abd4adea..0d54065c9c9 100644 --- a/tests/test_gc.rs +++ b/tests/test_gc.rs @@ -211,8 +211,8 @@ fn inheritance_with_new_methods_with_drop() { let drop_called2 = Arc::new(AtomicBool::new(false)); Python::with_gil(|py| { - let _typebase = py.get_type_bound::(); - let typeobj = py.get_type_bound::(); + let _typebase = py.get_type::(); + let typeobj = py.get_type::(); let inst = typeobj.call((), None).unwrap(); let obj = inst.downcast::().unwrap(); @@ -255,7 +255,7 @@ fn gc_during_borrow() { Python::with_gil(|py| { unsafe { // get the traverse function - let ty = py.get_type_bound::(); + let ty = py.get_type::(); let traverse = get_type_traverse(ty.as_type_ptr()).unwrap(); // create an object and check that traversing it works normally @@ -303,7 +303,7 @@ impl PartialTraverse { fn traverse_partial() { Python::with_gil(|py| unsafe { // get the traverse function - let ty = py.get_type_bound::(); + let ty = py.get_type::(); let traverse = get_type_traverse(ty.as_type_ptr()).unwrap(); // confirm that traversing errors @@ -338,7 +338,7 @@ impl PanickyTraverse { fn traverse_panic() { Python::with_gil(|py| unsafe { // get the traverse function - let ty = py.get_type_bound::(); + let ty = py.get_type::(); let traverse = get_type_traverse(ty.as_type_ptr()).unwrap(); // confirm that traversing errors @@ -361,7 +361,7 @@ impl TriesGILInTraverse { fn tries_gil_in_traverse() { Python::with_gil(|py| unsafe { // get the traverse function - let ty = py.get_type_bound::(); + let ty = py.get_type::(); let traverse = get_type_traverse(ty.as_type_ptr()).unwrap(); // confirm that traversing panicks @@ -414,7 +414,7 @@ impl<'a> Traversable for PyRef<'a, HijackedTraverse> { fn traverse_cannot_be_hijacked() { Python::with_gil(|py| unsafe { // get the traverse function - let ty = py.get_type_bound::(); + let ty = py.get_type::(); let traverse = get_type_traverse(ty.as_type_ptr()).unwrap(); let cell = Bound::new(py, HijackedTraverse::new()).unwrap(); @@ -536,7 +536,7 @@ fn unsendable_are_not_traversed_on_foreign_thread() { unsafe impl Send for SendablePtr {} Python::with_gil(|py| unsafe { - let ty = py.get_type_bound::(); + let ty = py.get_type::(); let traverse = get_type_traverse(ty.as_type_ptr()).unwrap(); let obj = Py::new( diff --git a/tests/test_getter_setter.rs b/tests/test_getter_setter.rs index a28030c4de0..dc67c040fc5 100644 --- a/tests/test_getter_setter.rs +++ b/tests/test_getter_setter.rs @@ -85,7 +85,7 @@ fn class_with_properties() { py_run!(py, inst, "inst.from_any = 15"); py_run!(py, inst, "assert inst.get_num() == 15"); - let d = [("C", py.get_type_bound::())].into_py_dict(py); + let d = [("C", py.get_type::())].into_py_dict(py); py_assert!(py, *d, "C.DATA.__doc__ == 'a getter for data'"); }); } diff --git a/tests/test_inheritance.rs b/tests/test_inheritance.rs index 6037ae1a57b..1f02edacbc8 100644 --- a/tests/test_inheritance.rs +++ b/tests/test_inheritance.rs @@ -20,7 +20,7 @@ struct SubclassAble {} #[test] fn subclass() { Python::with_gil(|py| { - let d = [("SubclassAble", py.get_type_bound::())].into_py_dict(py); + let d = [("SubclassAble", py.get_type::())].into_py_dict(py); py.run_bound( "class A(SubclassAble): pass\nassert issubclass(A, SubclassAble)", @@ -72,7 +72,7 @@ impl SubClass { #[test] fn inheritance_with_new_methods() { Python::with_gil(|py| { - let typeobj = py.get_type_bound::(); + let typeobj = py.get_type::(); let inst = typeobj.call((), None).unwrap(); py_run!(py, inst, "assert inst.val1 == 10; assert inst.val2 == 5"); }); @@ -112,8 +112,8 @@ fn mutation_fails() { #[test] fn is_subclass_and_is_instance() { Python::with_gil(|py| { - let sub_ty = py.get_type_bound::(); - let base_ty = py.get_type_bound::(); + let sub_ty = py.get_type::(); + let base_ty = py.get_type::(); assert!(sub_ty.is_subclass_of::().unwrap()); assert!(sub_ty.is_subclass(&base_ty).unwrap()); @@ -155,7 +155,7 @@ impl SubClass2 { #[test] fn handle_result_in_new() { Python::with_gil(|py| { - let subclass = py.get_type_bound::(); + let subclass = py.get_type::(); py_run!( py, subclass, @@ -274,7 +274,7 @@ mod inheriting_native_type { #[test] fn custom_exception() { Python::with_gil(|py| { - let cls = py.get_type_bound::(); + let cls = py.get_type::(); let dict = [("cls", &cls)].into_py_dict(py); let res = py.run_bound( "e = cls('hello'); assert str(e) == 'hello'; assert e.context == 'Hello :)'; raise e", @@ -315,7 +315,7 @@ fn test_subclass_ref_counts() { // regression test for issue #1363 Python::with_gil(|py| { #[allow(non_snake_case)] - let SimpleClass = py.get_type_bound::(); + let SimpleClass = py.get_type::(); py_run!( py, SimpleClass, diff --git a/tests/test_macro_docs.rs b/tests/test_macro_docs.rs index 2eb21c52a00..964e762886d 100644 --- a/tests/test_macro_docs.rs +++ b/tests/test_macro_docs.rs @@ -23,7 +23,7 @@ impl MacroDocs { #[test] fn meth_doc() { Python::with_gil(|py| { - let d = [("C", py.get_type_bound::())].into_py_dict(py); + let d = [("C", py.get_type::())].into_py_dict(py); py_assert!( py, *d, diff --git a/tests/test_macros.rs b/tests/test_macros.rs index 4bf2807f93a..40fd4847679 100644 --- a/tests/test_macros.rs +++ b/tests/test_macros.rs @@ -74,7 +74,7 @@ property_rename_via_macro!(my_new_property_name); #[test] fn test_macro_rules_interactions() { Python::with_gil(|py| { - let my_base = py.get_type_bound::(); + let my_base = py.get_type::(); py_assert!(py, my_base, "my_base.__name__ == 'MyClass'"); let my_func = wrap_pyfunction!(my_function_in_macro, py).unwrap(); @@ -84,7 +84,7 @@ fn test_macro_rules_interactions() { "my_func.__text_signature__ == '(a, b=None, *, c=42)'" ); - let renamed_prop = py.get_type_bound::(); + let renamed_prop = py.get_type::(); py_assert!( py, renamed_prop, diff --git a/tests/test_mapping.rs b/tests/test_mapping.rs index 65d07dd2611..1938c8837ff 100644 --- a/tests/test_mapping.rs +++ b/tests/test_mapping.rs @@ -71,7 +71,7 @@ impl Mapping { /// Return a dict with `m = Mapping(['1', '2', '3'])`. fn map_dict(py: Python<'_>) -> Bound<'_, pyo3::types::PyDict> { - let d = [("Mapping", py.get_type_bound::())].into_py_dict(py); + let d = [("Mapping", py.get_type::())].into_py_dict(py); py_run!(py, *d, "m = Mapping(['1', '2', '3'])"); d } diff --git a/tests/test_methods.rs b/tests/test_methods.rs index 159cc210133..ecf7f64e94c 100644 --- a/tests/test_methods.rs +++ b/tests/test_methods.rs @@ -86,7 +86,7 @@ impl ClassMethod { #[test] fn class_method() { Python::with_gil(|py| { - let d = [("C", py.get_type_bound::())].into_py_dict(py); + 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!( @@ -113,7 +113,7 @@ impl ClassMethodWithArgs { #[test] fn class_method_with_args() { Python::with_gil(|py| { - let d = [("C", py.get_type_bound::())].into_py_dict(py); + let d = [("C", py.get_type::())].into_py_dict(py); py_assert!( py, *d, @@ -144,7 +144,7 @@ fn static_method() { Python::with_gil(|py| { assert_eq!(StaticMethod::method(py), "StaticMethod.method()!"); - let d = [("C", py.get_type_bound::())].into_py_dict(py); + let d = [("C", py.get_type::())].into_py_dict(py); py_assert!(py, *d, "C.method() == 'StaticMethod.method()!'"); py_assert!(py, *d, "C().method() == 'StaticMethod.method()!'"); py_assert!(py, *d, "C.method.__doc__ == 'Test static method.'"); @@ -168,7 +168,7 @@ fn static_method_with_args() { Python::with_gil(|py| { assert_eq!(StaticMethodWithArgs::method(py, 1234), "0x4d2"); - let d = [("C", py.get_type_bound::())].into_py_dict(py); + let d = [("C", py.get_type::())].into_py_dict(py); py_assert!(py, *d, "C.method(1337) == '0x539'"); }); } @@ -677,7 +677,7 @@ impl MethDocs { #[test] fn meth_doc() { Python::with_gil(|py| { - let d = [("C", py.get_type_bound::())].into_py_dict(py); + let d = [("C", py.get_type::())].into_py_dict(py); py_assert!(py, *d, "C.__doc__ == 'A class with \"documentation\".'"); py_assert!( py, @@ -868,7 +868,7 @@ impl FromSequence { #[test] fn test_from_sequence() { Python::with_gil(|py| { - let typeobj = py.get_type_bound::(); + let typeobj = py.get_type::(); py_assert!(py, typeobj, "typeobj(range(0, 4)).numbers == [0, 1, 2, 3]"); }); } @@ -948,7 +948,7 @@ impl r#RawIdents { #[test] fn test_raw_idents() { Python::with_gil(|py| { - let raw_idents_type = py.get_type_bound::(); + let raw_idents_type = py.get_type::(); assert_eq!(raw_idents_type.qualname().unwrap(), "RawIdents"); py_run!( py, diff --git a/tests/test_multiple_pymethods.rs b/tests/test_multiple_pymethods.rs index 308220e78b2..13baeed3815 100644 --- a/tests/test_multiple_pymethods.rs +++ b/tests/test_multiple_pymethods.rs @@ -65,7 +65,7 @@ impl PyClassWithMultiplePyMethods { #[test] fn test_class_with_multiple_pymethods() { Python::with_gil(|py| { - let cls = py.get_type_bound::(); + let cls = py.get_type::(); py_assert!(py, cls, "cls()() == 'call'"); py_assert!(py, cls, "cls().method() == 'method'"); py_assert!(py, cls, "cls.classmethod() == 'classmethod'"); diff --git a/tests/test_proto_methods.rs b/tests/test_proto_methods.rs index 597eed9b7cf..6373640a2f9 100644 --- a/tests/test_proto_methods.rs +++ b/tests/test_proto_methods.rs @@ -668,7 +668,7 @@ impl OnceFuture { #[cfg(not(target_arch = "wasm32"))] // Won't work without wasm32 event loop (e.g., Pyodide has WebLoop) fn test_await() { Python::with_gil(|py| { - let once = py.get_type_bound::(); + let once = py.get_type::(); let source = r#" import asyncio import sys @@ -718,7 +718,7 @@ impl AsyncIterator { #[cfg(not(target_arch = "wasm32"))] // Won't work without wasm32 event loop (e.g., Pyodide has WebLoop) fn test_anext_aiter() { Python::with_gil(|py| { - let once = py.get_type_bound::(); + let once = py.get_type::(); let source = r#" import asyncio import sys @@ -740,7 +740,7 @@ asyncio.run(main()) let globals = PyModule::import_bound(py, "__main__").unwrap().dict(); globals.set_item("Once", once).unwrap(); globals - .set_item("AsyncIterator", py.get_type_bound::()) + .set_item("AsyncIterator", py.get_type::()) .unwrap(); py.run_bound(source, Some(&globals), None) .map_err(|e| e.display(py)) @@ -783,7 +783,7 @@ impl DescrCounter { #[test] fn descr_getset() { Python::with_gil(|py| { - let counter = py.get_type_bound::(); + let counter = py.get_type::(); let source = pyo3::indoc::indoc!( r#" class Class: diff --git a/tests/test_sequence.rs b/tests/test_sequence.rs index 22ae864c8cf..9ac8d6dbe89 100644 --- a/tests/test_sequence.rs +++ b/tests/test_sequence.rs @@ -107,7 +107,7 @@ impl ByteSequence { /// Return a dict with `s = ByteSequence([1, 2, 3])`. fn seq_dict(py: Python<'_>) -> Bound<'_, pyo3::types::PyDict> { - let d = [("ByteSequence", py.get_type_bound::())].into_py_dict(py); + let d = [("ByteSequence", py.get_type::())].into_py_dict(py); // Though we can construct `s` in Rust, let's test `__new__` works. py_run!(py, *d, "s = ByteSequence([1, 2, 3])"); d @@ -139,7 +139,7 @@ fn test_setitem() { #[test] fn test_delitem() { Python::with_gil(|py| { - let d = [("ByteSequence", py.get_type_bound::())].into_py_dict(py); + let d = [("ByteSequence", py.get_type::())].into_py_dict(py); py_run!( py, @@ -235,7 +235,7 @@ fn test_repeat() { #[test] fn test_inplace_repeat() { Python::with_gil(|py| { - let d = [("ByteSequence", py.get_type_bound::())].into_py_dict(py); + let d = [("ByteSequence", py.get_type::())].into_py_dict(py); py_run!( py, diff --git a/tests/test_static_slots.rs b/tests/test_static_slots.rs index 581459b7d6e..bd1cb0cdc8a 100644 --- a/tests/test_static_slots.rs +++ b/tests/test_static_slots.rs @@ -38,7 +38,7 @@ impl Count5 { /// Return a dict with `s = Count5()`. fn test_dict(py: Python<'_>) -> Bound<'_, pyo3::types::PyDict> { - let d = [("Count5", py.get_type_bound::())].into_py_dict(py); + let d = [("Count5", py.get_type::())].into_py_dict(py); // Though we can construct `s` in Rust, let's test `__new__` works. py_run!(py, *d, "s = Count5()"); d diff --git a/tests/test_super.rs b/tests/test_super.rs index b64fd57e687..4ff049824b0 100644 --- a/tests/test_super.rs +++ b/tests/test_super.rs @@ -43,7 +43,7 @@ impl SubClass { #[test] fn test_call_super_method() { Python::with_gil(|py| { - let cls = py.get_type_bound::(); + let cls = py.get_type::(); pyo3::py_run!( py, cls, diff --git a/tests/test_text_signature.rs b/tests/test_text_signature.rs index d9fcb83d3bd..7aceedd44c0 100644 --- a/tests/test_text_signature.rs +++ b/tests/test_text_signature.rs @@ -13,7 +13,7 @@ fn class_without_docs_or_signature() { struct MyClass {} Python::with_gil(|py| { - let typeobj = py.get_type_bound::(); + let typeobj = py.get_type::(); py_assert!(py, typeobj, "typeobj.__doc__ is None"); py_assert!(py, typeobj, "typeobj.__text_signature__ is None"); @@ -28,7 +28,7 @@ fn class_with_docs() { struct MyClass {} Python::with_gil(|py| { - let typeobj = py.get_type_bound::(); + let typeobj = py.get_type::(); py_assert!(py, typeobj, "typeobj.__doc__ == 'docs line1\\ndocs line2'"); py_assert!(py, typeobj, "typeobj.__text_signature__ is None"); @@ -52,7 +52,7 @@ fn class_with_signature_no_doc() { } Python::with_gil(|py| { - let typeobj = py.get_type_bound::(); + let typeobj = py.get_type::(); py_assert!(py, typeobj, "typeobj.__doc__ == ''"); py_assert!( py, @@ -81,7 +81,7 @@ fn class_with_docs_and_signature() { } Python::with_gil(|py| { - let typeobj = py.get_type_bound::(); + let typeobj = py.get_type::(); py_assert!(py, typeobj, "typeobj.__doc__ == 'docs line1\\ndocs line2'"); py_assert!( @@ -239,7 +239,7 @@ fn test_auto_test_signature_method() { } Python::with_gil(|py| { - let cls = py.get_type_bound::(); + let cls = py.get_type::(); #[cfg(any(not(Py_LIMITED_API), Py_3_10))] py_assert!(py, cls, "cls.__text_signature__ == '(a, b, c)'"); py_assert!( @@ -324,7 +324,7 @@ fn test_auto_test_signature_opt_out() { let f = wrap_pyfunction!(my_function_2)(py).unwrap(); py_assert!(py, f, "f.__text_signature__ == None"); - let cls = py.get_type_bound::(); + let cls = py.get_type::(); py_assert!(py, cls, "cls.__text_signature__ == None"); py_assert!(py, cls, "cls.method.__text_signature__ == None"); py_assert!(py, cls, "cls.method_2.__text_signature__ == None"); @@ -384,7 +384,7 @@ fn test_methods() { } Python::with_gil(|py| { - let typeobj = py.get_type_bound::(); + let typeobj = py.get_type::(); py_assert!( py, @@ -425,7 +425,7 @@ fn test_raw_identifiers() { } Python::with_gil(|py| { - let typeobj = py.get_type_bound::(); + let typeobj = py.get_type::(); py_assert!(py, typeobj, "typeobj.__text_signature__ == '()'"); diff --git a/tests/test_variable_arguments.rs b/tests/test_variable_arguments.rs index 3724689d836..d8d9c79e31a 100644 --- a/tests/test_variable_arguments.rs +++ b/tests/test_variable_arguments.rs @@ -27,7 +27,7 @@ impl MyClass { #[test] fn variable_args() { Python::with_gil(|py| { - let my_obj = py.get_type_bound::(); + let my_obj = py.get_type::(); py_assert!(py, my_obj, "my_obj.test_args() == ()"); py_assert!(py, my_obj, "my_obj.test_args(1) == (1,)"); py_assert!(py, my_obj, "my_obj.test_args(1, 2) == (1, 2)"); @@ -37,7 +37,7 @@ fn variable_args() { #[test] fn variable_kwargs() { Python::with_gil(|py| { - let my_obj = py.get_type_bound::(); + let my_obj = py.get_type::(); py_assert!(py, my_obj, "my_obj.test_kwargs() == None"); py_assert!(py, my_obj, "my_obj.test_kwargs(test=1) == {'test': 1}"); py_assert!( diff --git a/tests/ui/invalid_intern_arg.rs b/tests/ui/invalid_intern_arg.rs index eb479431b90..ac82fed97d8 100644 --- a/tests/ui/invalid_intern_arg.rs +++ b/tests/ui/invalid_intern_arg.rs @@ -2,5 +2,5 @@ use pyo3::Python; fn main() { let _foo = if true { "foo" } else { "bar" }; - Python::with_gil(|py| py.import_bound(pyo3::intern!(py, _foo)).unwrap()); + Python::with_gil(|py| py.import(pyo3::intern!(py, _foo)).unwrap()); } diff --git a/tests/ui/invalid_intern_arg.stderr b/tests/ui/invalid_intern_arg.stderr index 7d1aad1ae28..7b02b72a214 100644 --- a/tests/ui/invalid_intern_arg.stderr +++ b/tests/ui/invalid_intern_arg.stderr @@ -1,17 +1,17 @@ error[E0435]: attempt to use a non-constant value in a constant - --> tests/ui/invalid_intern_arg.rs:5:61 + --> tests/ui/invalid_intern_arg.rs:5:55 | -5 | Python::with_gil(|py| py.import_bound(pyo3::intern!(py, _foo)).unwrap()); - | ------------------^^^^- - | | | - | | non-constant value - | help: consider using `let` instead of `static`: `let INTERNED` +5 | Python::with_gil(|py| py.import(pyo3::intern!(py, _foo)).unwrap()); + | ------------------^^^^- + | | | + | | non-constant value + | help: consider using `let` instead of `static`: `let INTERNED` error: lifetime may not live long enough --> tests/ui/invalid_intern_arg.rs:5:27 | -5 | Python::with_gil(|py| py.import_bound(pyo3::intern!(py, _foo)).unwrap()); - | --- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ returning this value requires that `'1` must outlive `'2` +5 | Python::with_gil(|py| py.import(pyo3::intern!(py, _foo)).unwrap()); + | --- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ returning this value requires that `'1` must outlive `'2` | | | | | return type of closure is pyo3::Bound<'2, PyModule> | has type `Python<'1>` From 0958568be07ccd6ffef8d4b9c3100314a0aa69be Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Mon, 12 Aug 2024 16:07:32 -0600 Subject: [PATCH 207/495] Remove Copy and Clone derives from PyObject (#4434) * Remove Copy and Clone derives from PyObject * add changelog entry --- newsfragments/4434.changed.md | 5 +++++ pyo3-ffi/src/bytearrayobject.rs | 1 - pyo3-ffi/src/complexobject.rs | 1 - pyo3-ffi/src/cpython/bytesobject.rs | 1 - pyo3-ffi/src/cpython/code.rs | 4 ---- pyo3-ffi/src/cpython/frameobject.rs | 1 - pyo3-ffi/src/cpython/genobject.rs | 1 - pyo3-ffi/src/cpython/listobject.rs | 1 - pyo3-ffi/src/cpython/object.rs | 2 -- pyo3-ffi/src/datetime.rs | 18 ++++++------------ pyo3-ffi/src/moduleobject.rs | 2 -- pyo3-ffi/src/object.rs | 4 ++-- 12 files changed, 13 insertions(+), 28 deletions(-) create mode 100644 newsfragments/4434.changed.md diff --git a/newsfragments/4434.changed.md b/newsfragments/4434.changed.md new file mode 100644 index 00000000000..f16513add1b --- /dev/null +++ b/newsfragments/4434.changed.md @@ -0,0 +1,5 @@ +* The `PyO3::ffi` bindings for the C `PyObject` struct no longer derive from + `Copy` and `Clone`. If you use the ffi directly you will need to remove `Copy` + and `Clone` from any derived types. Any cases where a PyObject struct was + copied or cloned directly likely indicates a bug, it is not safe to allocate + PyObject structs outside of the Python runtime. diff --git a/pyo3-ffi/src/bytearrayobject.rs b/pyo3-ffi/src/bytearrayobject.rs index c09eac5b22c..24a97bcc31b 100644 --- a/pyo3-ffi/src/bytearrayobject.rs +++ b/pyo3-ffi/src/bytearrayobject.rs @@ -5,7 +5,6 @@ use std::ptr::addr_of_mut; #[cfg(not(any(PyPy, GraalPy, Py_LIMITED_API)))] #[repr(C)] -#[derive(Copy, Clone)] pub struct PyByteArrayObject { pub ob_base: PyVarObject, pub ob_alloc: Py_ssize_t, diff --git a/pyo3-ffi/src/complexobject.rs b/pyo3-ffi/src/complexobject.rs index a03d9b00932..78bb9ccaaaf 100644 --- a/pyo3-ffi/src/complexobject.rs +++ b/pyo3-ffi/src/complexobject.rs @@ -26,7 +26,6 @@ extern "C" { } #[repr(C)] -#[derive(Copy, Clone)] // non-limited pub struct PyComplexObject { pub ob_base: PyObject, diff --git a/pyo3-ffi/src/cpython/bytesobject.rs b/pyo3-ffi/src/cpython/bytesobject.rs index fb0b38cf1d8..d0ac5b9c30e 100644 --- a/pyo3-ffi/src/cpython/bytesobject.rs +++ b/pyo3-ffi/src/cpython/bytesobject.rs @@ -6,7 +6,6 @@ use std::os::raw::c_int; #[cfg(not(any(PyPy, GraalPy, Py_LIMITED_API)))] #[repr(C)] -#[derive(Copy, Clone)] pub struct PyBytesObject { pub ob_base: PyVarObject, pub ob_shash: crate::Py_hash_t, diff --git a/pyo3-ffi/src/cpython/code.rs b/pyo3-ffi/src/cpython/code.rs index 86e862f21ab..230096ca378 100644 --- a/pyo3-ffi/src/cpython/code.rs +++ b/pyo3-ffi/src/cpython/code.rs @@ -82,7 +82,6 @@ opaque_struct!(PyCodeObject); #[cfg(all(not(any(PyPy, GraalPy)), Py_3_7, not(Py_3_8)))] #[repr(C)] -#[derive(Copy, Clone)] pub struct PyCodeObject { pub ob_base: PyObject, pub co_argcount: c_int, @@ -111,7 +110,6 @@ opaque_struct!(_PyExecutorArray); #[cfg(all(not(any(PyPy, GraalPy)), Py_3_8, not(Py_3_11)))] #[repr(C)] -#[derive(Copy, Clone)] pub struct PyCodeObject { pub ob_base: PyObject, pub co_argcount: c_int, @@ -145,7 +143,6 @@ pub struct PyCodeObject { #[cfg(all(not(any(PyPy, GraalPy)), Py_3_11))] #[repr(C)] -#[derive(Copy, Clone)] pub struct PyCodeObject { pub ob_base: PyVarObject, pub co_consts: *mut PyObject, @@ -198,7 +195,6 @@ pub struct PyCodeObject { #[cfg(PyPy)] #[repr(C)] -#[derive(Copy, Clone)] pub struct PyCodeObject { pub ob_base: PyObject, pub co_name: *mut PyObject, diff --git a/pyo3-ffi/src/cpython/frameobject.rs b/pyo3-ffi/src/cpython/frameobject.rs index a85818ace0a..6d2346f9288 100644 --- a/pyo3-ffi/src/cpython/frameobject.rs +++ b/pyo3-ffi/src/cpython/frameobject.rs @@ -21,7 +21,6 @@ pub struct PyTryBlock { } #[repr(C)] -#[derive(Copy, Clone)] #[cfg(not(any(PyPy, GraalPy, Py_3_11)))] pub struct PyFrameObject { pub ob_base: PyVarObject, diff --git a/pyo3-ffi/src/cpython/genobject.rs b/pyo3-ffi/src/cpython/genobject.rs index 73ebdb491ff..17348b2f7bd 100644 --- a/pyo3-ffi/src/cpython/genobject.rs +++ b/pyo3-ffi/src/cpython/genobject.rs @@ -9,7 +9,6 @@ use std::ptr::addr_of_mut; #[cfg(not(any(PyPy, GraalPy)))] #[repr(C)] -#[derive(Copy, Clone)] pub struct PyGenObject { pub ob_base: PyObject, #[cfg(not(Py_3_11))] diff --git a/pyo3-ffi/src/cpython/listobject.rs b/pyo3-ffi/src/cpython/listobject.rs index ea15cfc1ff5..963ddfbea87 100644 --- a/pyo3-ffi/src/cpython/listobject.rs +++ b/pyo3-ffi/src/cpython/listobject.rs @@ -4,7 +4,6 @@ use crate::pyport::Py_ssize_t; #[cfg(not(any(PyPy, GraalPy)))] #[repr(C)] -#[derive(Copy, Clone)] pub struct PyListObject { pub ob_base: PyVarObject, pub ob_item: *mut *mut PyObject, diff --git a/pyo3-ffi/src/cpython/object.rs b/pyo3-ffi/src/cpython/object.rs index b4f6ce5a878..04811cbda9e 100644 --- a/pyo3-ffi/src/cpython/object.rs +++ b/pyo3-ffi/src/cpython/object.rs @@ -205,7 +205,6 @@ pub type printfunc = unsafe extern "C" fn(arg1: *mut PyObject, arg2: *mut ::libc::FILE, arg3: c_int) -> c_int; #[repr(C)] -#[derive(Debug, Copy, Clone)] pub struct PyTypeObject { #[cfg(all(PyPy, not(Py_3_9)))] pub ob_refcnt: Py_ssize_t, @@ -301,7 +300,6 @@ pub struct _specialization_cache { } #[repr(C)] -#[derive(Clone)] pub struct PyHeapTypeObject { pub ht_type: PyTypeObject, pub as_async: PyAsyncMethods, diff --git a/pyo3-ffi/src/datetime.rs b/pyo3-ffi/src/datetime.rs index 5da2956c5e9..2ab76c3830f 100644 --- a/pyo3-ffi/src/datetime.rs +++ b/pyo3-ffi/src/datetime.rs @@ -27,7 +27,6 @@ const _PyDateTime_TIME_DATASIZE: usize = 6; const _PyDateTime_DATETIME_DATASIZE: usize = 10; #[repr(C)] -#[derive(Debug, Copy, Clone)] /// Structure representing a `datetime.timedelta`. pub struct PyDateTime_Delta { pub ob_base: PyObject, @@ -46,7 +45,6 @@ pub struct PyDateTime_Delta { #[cfg(not(any(PyPy, GraalPy)))] #[repr(C)] -#[derive(Debug, Copy, Clone)] /// Structure representing a `datetime.time` without a `tzinfo` member. pub struct _PyDateTime_BaseTime { pub ob_base: PyObject, @@ -56,7 +54,6 @@ pub struct _PyDateTime_BaseTime { } #[repr(C)] -#[derive(Debug, Copy, Clone)] /// Structure representing a `datetime.time`. pub struct PyDateTime_Time { pub ob_base: PyObject, @@ -77,7 +74,6 @@ pub struct PyDateTime_Time { } #[repr(C)] -#[derive(Debug, Copy, Clone)] /// Structure representing a `datetime.date` pub struct PyDateTime_Date { pub ob_base: PyObject, @@ -91,7 +87,6 @@ pub struct PyDateTime_Date { #[cfg(not(any(PyPy, GraalPy)))] #[repr(C)] -#[derive(Debug, Copy, Clone)] /// Structure representing a `datetime.datetime` without a `tzinfo` member. pub struct _PyDateTime_BaseDateTime { pub ob_base: PyObject, @@ -101,7 +96,6 @@ pub struct _PyDateTime_BaseDateTime { } #[repr(C)] -#[derive(Debug, Copy, Clone)] /// Structure representing a `datetime.datetime`. pub struct PyDateTime_DateTime { pub ob_base: PyObject, @@ -130,8 +124,8 @@ pub struct PyDateTime_DateTime { /// Returns a signed integer greater than 0. pub unsafe fn PyDateTime_GET_YEAR(o: *mut PyObject) -> c_int { // This should work for Date or DateTime - let d = *(o as *mut PyDateTime_Date); - c_int::from(d.data[0]) << 8 | c_int::from(d.data[1]) + let data = (*(o as *mut PyDateTime_Date)).data; + c_int::from(data[0]) << 8 | c_int::from(data[1]) } #[inline] @@ -139,8 +133,8 @@ pub unsafe fn PyDateTime_GET_YEAR(o: *mut PyObject) -> c_int { /// Retrieve the month component of a `PyDateTime_Date` or `PyDateTime_DateTime`. /// Returns a signed integer in the range `[1, 12]`. pub unsafe fn PyDateTime_GET_MONTH(o: *mut PyObject) -> c_int { - let d = *(o as *mut PyDateTime_Date); - c_int::from(d.data[2]) + let data = (*(o as *mut PyDateTime_Date)).data; + c_int::from(data[2]) } #[inline] @@ -148,8 +142,8 @@ pub unsafe fn PyDateTime_GET_MONTH(o: *mut PyObject) -> c_int { /// Retrieve the day component of a `PyDateTime_Date` or `PyDateTime_DateTime`. /// Returns a signed integer in the interval `[1, 31]`. pub unsafe fn PyDateTime_GET_DAY(o: *mut PyObject) -> c_int { - let d = *(o as *mut PyDateTime_Date); - c_int::from(d.data[3]) + let data = (*(o as *mut PyDateTime_Date)).data; + c_int::from(data[3]) } // Accessor macros for times diff --git a/pyo3-ffi/src/moduleobject.rs b/pyo3-ffi/src/moduleobject.rs index f4306b18639..b9026997d2e 100644 --- a/pyo3-ffi/src/moduleobject.rs +++ b/pyo3-ffi/src/moduleobject.rs @@ -52,7 +52,6 @@ extern "C" { } #[repr(C)] -#[derive(Copy, Clone)] pub struct PyModuleDef_Base { pub ob_base: PyObject, pub m_init: Option *mut PyObject>, @@ -98,7 +97,6 @@ pub const Py_MOD_PER_INTERPRETER_GIL_SUPPORTED: *mut c_void = 2 as *mut c_void; // skipped non-limited _Py_mod_LAST_SLOT #[repr(C)] -#[derive(Copy, Clone)] pub struct PyModuleDef { pub m_base: PyModuleDef_Base, pub m_name: *const c_char, diff --git a/pyo3-ffi/src/object.rs b/pyo3-ffi/src/object.rs index 21161a34d3a..9181cb17dd2 100644 --- a/pyo3-ffi/src/object.rs +++ b/pyo3-ffi/src/object.rs @@ -61,7 +61,7 @@ impl std::fmt::Debug for PyObjectObRefcnt { pub type PyObjectObRefcnt = Py_ssize_t; #[repr(C)] -#[derive(Copy, Clone, Debug)] +#[derive(Debug)] pub struct PyObject { #[cfg(py_sys_config = "Py_TRACE_REFS")] pub _ob_next: *mut PyObject, @@ -76,7 +76,7 @@ pub struct PyObject { // skipped _PyObject_CAST #[repr(C)] -#[derive(Debug, Copy, Clone)] +#[derive(Debug)] pub struct PyVarObject { pub ob_base: PyObject, #[cfg(not(GraalPy))] From 7760d6b11144c457bc3fb9c8d24c53d824aaa367 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Tue, 13 Aug 2024 00:34:01 +0100 Subject: [PATCH 208/495] ci: fix nightly warning about unreachable pattern (#4437) * ci: fix nightly warning about unreachable pattern * add comment --- src/sync.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/sync.rs b/src/sync.rs index 60c8c4747a2..c781755c067 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -126,10 +126,9 @@ impl GILOnceCell { return value; } - match self.init(py, || Ok::(f())) { - Ok(value) => value, - Err(void) => match void {}, - } + // .unwrap() will never panic because the result is always Ok + self.init(py, || Ok::(f())) + .unwrap() } /// Like `get_or_init`, but accepts a fallible initialization function. If it fails, the cell From f869d167d66dacecb035453bcbcb2bda961bf388 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers <7943856+bschoenmaeckers@users.noreply.github.com> Date: Tue, 13 Aug 2024 22:18:21 +0200 Subject: [PATCH 209/495] reintroduce `PyModule` constructors (#4404) * reintroduce `PyModule` constructors * Update links * Use PyModule_NewObject * module::from_code takes CStr instead str * Add changelog * fix PyPy link_name for `PyModule_NewObject` --------- Co-authored-by: Icxolu <10486322+Icxolu@users.noreply.github.com> --- examples/maturin-starter/src/lib.rs | 2 +- examples/plugin/src/main.rs | 2 +- examples/setuptools-rust-starter/src/lib.rs | 2 +- guide/src/class.md | 4 +- guide/src/class/numeric.md | 2 +- guide/src/conversions/traits.md | 43 ++++++----- guide/src/function/signature.md | 8 +- guide/src/module.md | 2 +- .../python-from-rust/calling-existing-code.md | 57 +++++++------- guide/src/python-from-rust/function-calls.md | 22 +++--- newsfragments/4404.changed.md | 1 + pyo3-benches/benches/bench_call.rs | 3 +- pyo3-ffi/src/moduleobject.rs | 1 + pytests/src/lib.rs | 2 +- src/conversions/anyhow.rs | 2 +- src/conversions/eyre.rs | 2 +- src/conversions/num_bigint.rs | 7 +- src/conversions/num_complex.rs | 51 +++++++------ src/instance.rs | 24 +++--- src/marker.rs | 4 +- src/pyclass_init.rs | 2 +- src/types/any.rs | 71 +++++++++++------- src/types/capsule.rs | 4 +- src/types/module.rs | 75 +++++++++++++------ src/types/typeobject.rs | 25 ++++--- tests/test_class_basics.rs | 2 +- tests/test_class_new.rs | 2 +- tests/test_inheritance.rs | 2 +- tests/test_module.rs | 13 ++-- tests/test_proto_methods.rs | 6 +- tests/test_various.rs | 4 +- 31 files changed, 264 insertions(+), 183 deletions(-) create mode 100644 newsfragments/4404.changed.md diff --git a/examples/maturin-starter/src/lib.rs b/examples/maturin-starter/src/lib.rs index faa147b2a10..4c2a30d3a5d 100644 --- a/examples/maturin-starter/src/lib.rs +++ b/examples/maturin-starter/src/lib.rs @@ -27,7 +27,7 @@ fn maturin_starter(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { // Inserting to sys.modules allows importing submodules nicely from Python // e.g. from maturin_starter.submodule import SubmoduleClass - let sys = PyModule::import_bound(py, "sys")?; + let sys = PyModule::import(py, "sys")?; let sys_modules: Bound<'_, PyDict> = sys.getattr("modules")?.downcast_into()?; sys_modules.set_item("maturin_starter.submodule", m.getattr("submodule")?)?; diff --git a/examples/plugin/src/main.rs b/examples/plugin/src/main.rs index 5a54a1837cb..b50b54548e5 100644 --- a/examples/plugin/src/main.rs +++ b/examples/plugin/src/main.rs @@ -19,7 +19,7 @@ fn main() -> Result<(), Box> { // Now we can load our python_plugin/gadget_init_plugin.py file. // It can in turn import other stuff as it deems appropriate - let plugin = PyModule::import_bound(py, "gadget_init_plugin")?; + let plugin = PyModule::import(py, "gadget_init_plugin")?; // and call start function there, which will return a python reference to Gadget. // Gadget here is a "pyclass" object reference let gadget = plugin.getattr("start")?.call0()?; diff --git a/examples/setuptools-rust-starter/src/lib.rs b/examples/setuptools-rust-starter/src/lib.rs index d31284be7a3..a26623bc044 100644 --- a/examples/setuptools-rust-starter/src/lib.rs +++ b/examples/setuptools-rust-starter/src/lib.rs @@ -27,7 +27,7 @@ fn _setuptools_rust_starter(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult // Inserting to sys.modules allows importing submodules nicely from Python // e.g. from setuptools_rust_starter.submodule import SubmoduleClass - let sys = PyModule::import_bound(py, "sys")?; + let sys = PyModule::import(py, "sys")?; let sys_modules: Bound<'_, PyDict> = sys.getattr("modules")?.downcast_into()?; sys_modules.set_item("setuptools_rust_starter.submodule", m.getattr("submodule")?)?; diff --git a/guide/src/class.md b/guide/src/class.md index 9f87f2828b0..02f8a2a194c 100644 --- a/guide/src/class.md +++ b/guide/src/class.md @@ -968,8 +968,8 @@ impl MyClass { # # fn main() -> PyResult<()> { # Python::with_gil(|py| { -# let inspect = PyModule::import_bound(py, "inspect")?.getattr("signature")?; -# let module = PyModule::new_bound(py, "my_module")?; +# let inspect = PyModule::import(py, "inspect")?.getattr("signature")?; +# let module = PyModule::new(py, "my_module")?; # module.add_class::()?; # let class = module.getattr("MyClass")?; # diff --git a/guide/src/class/numeric.md b/guide/src/class/numeric.md index 541f8fb5893..adb2e46dbe3 100644 --- a/guide/src/class/numeric.md +++ b/guide/src/class/numeric.md @@ -386,7 +386,7 @@ fn my_module(m: &Bound<'_, PyModule>) -> PyResult<()> { # # fn main() -> PyResult<()> { # Python::with_gil(|py| -> PyResult<()> { -# let globals = PyModule::import_bound(py, "__main__")?.dict(); +# let globals = PyModule::import(py, "__main__")?.dict(); # globals.set_item("Number", Number::type_object_bound(py))?; # # py.run_bound(SCRIPT, Some(&globals), None)?; diff --git a/guide/src/conversions/traits.md b/guide/src/conversions/traits.md index 5e8c3fe4202..6836e0bd9fb 100644 --- a/guide/src/conversions/traits.md +++ b/guide/src/conversions/traits.md @@ -46,6 +46,7 @@ the Python object, i.e. `obj.getattr("my_string")`, and call `extract()` on the ```rust use pyo3::prelude::*; +use pyo3_ffi::c_str; #[derive(FromPyObject)] struct RustyStruct { @@ -54,13 +55,13 @@ struct RustyStruct { # # fn main() -> PyResult<()> { # Python::with_gil(|py| -> PyResult<()> { -# let module = PyModule::from_code_bound( +# let module = PyModule::from_code( # py, -# "class Foo: +# c_str!("class Foo: # def __init__(self): -# self.my_string = 'test'", -# "", -# "", +# self.my_string = 'test'"), +# c_str!(""), +# c_str!(""), # )?; # # let class = module.getattr("Foo")?; @@ -100,6 +101,7 @@ The argument passed to `getattr` and `get_item` can also be configured: ```rust use pyo3::prelude::*; +use pyo3_ffi::c_str; #[derive(FromPyObject)] struct RustyStruct { @@ -111,14 +113,14 @@ struct RustyStruct { # # fn main() -> PyResult<()> { # Python::with_gil(|py| -> PyResult<()> { -# let module = PyModule::from_code_bound( +# let module = PyModule::from_code( # py, -# "class Foo(dict): +# c_str!("class Foo(dict): # def __init__(self): # self.name = 'test' -# self['key'] = 'test2'", -# "", -# "", +# self['key'] = 'test2'"), +# c_str!(""), +# c_str!(""), # )?; # # let class = module.getattr("Foo")?; @@ -262,6 +264,7 @@ attribute can be applied to single-field-variants. ```rust use pyo3::prelude::*; +use pyo3_ffi::c_str; #[derive(FromPyObject)] # #[derive(Debug)] @@ -339,15 +342,15 @@ enum RustyEnum<'py> { # ); # } # { -# let module = PyModule::from_code_bound( +# let module = PyModule::from_code( # py, -# "class Foo(dict): +# c_str!("class Foo(dict): # def __init__(self): # self.x = 0 # self.y = 1 -# self.z = 2", -# "", -# "", +# self.z = 2"), +# c_str!(""), +# c_str!(""), # )?; # # let class = module.getattr("Foo")?; @@ -364,14 +367,14 @@ enum RustyEnum<'py> { # } # # { -# let module = PyModule::from_code_bound( +# let module = PyModule::from_code( # py, -# "class Foo(dict): +# c_str!("class Foo(dict): # def __init__(self): # self.x = 3 -# self.y = 4", -# "", -# "", +# self.y = 4"), +# c_str!(""), +# c_str!(""), # )?; # # let class = module.getattr("Foo")?; diff --git a/guide/src/function/signature.md b/guide/src/function/signature.md index 0b3d7a9a507..8ebe74456a1 100644 --- a/guide/src/function/signature.md +++ b/guide/src/function/signature.md @@ -150,7 +150,7 @@ fn increment(x: u64, amount: Option) -> u64 { # Python::with_gil(|py| { # let fun = pyo3::wrap_pyfunction!(increment, py)?; # -# let inspect = PyModule::import_bound(py, "inspect")?.getattr("signature")?; +# let inspect = PyModule::import(py, "inspect")?.getattr("signature")?; # let sig: String = inspect # .call1((fun,))? # .call_method0("__str__")? @@ -178,7 +178,7 @@ fn increment(x: u64, amount: Option) -> u64 { # Python::with_gil(|py| { # let fun = pyo3::wrap_pyfunction!(increment, py)?; # -# let inspect = PyModule::import_bound(py, "inspect")?.getattr("signature")?; +# let inspect = PyModule::import(py, "inspect")?.getattr("signature")?; # let sig: String = inspect # .call1((fun,))? # .call_method0("__str__")? @@ -221,7 +221,7 @@ fn add(a: u64, b: u64) -> u64 { # let doc: String = fun.getattr("__doc__")?.extract()?; # assert_eq!(doc, "This function adds two unsigned 64-bit integers."); # -# let inspect = PyModule::import_bound(py, "inspect")?.getattr("signature")?; +# let inspect = PyModule::import(py, "inspect")?.getattr("signature")?; # let sig: String = inspect # .call1((fun,))? # .call_method0("__str__")? @@ -269,7 +269,7 @@ fn add(a: u64, b: u64) -> u64 { # let doc: String = fun.getattr("__doc__")?.extract()?; # assert_eq!(doc, "This function adds two unsigned 64-bit integers."); # -# let inspect = PyModule::import_bound(py, "inspect")?.getattr("signature")?; +# let inspect = PyModule::import(py, "inspect")?.getattr("signature")?; # let sig: String = inspect # .call1((fun,))? # .call_method0("__str__")? diff --git a/guide/src/module.md b/guide/src/module.md index c6a7b3c9c64..4aac2937016 100644 --- a/guide/src/module.md +++ b/guide/src/module.md @@ -75,7 +75,7 @@ fn parent_module(m: &Bound<'_, PyModule>) -> PyResult<()> { } fn register_child_module(parent_module: &Bound<'_, PyModule>) -> PyResult<()> { - let child_module = PyModule::new_bound(parent_module.py(), "child_module")?; + let child_module = PyModule::new(parent_module.py(), "child_module")?; child_module.add_function(wrap_pyfunction!(func, &child_module)?)?; parent_module.add_submodule(&child_module) } diff --git a/guide/src/python-from-rust/calling-existing-code.md b/guide/src/python-from-rust/calling-existing-code.md index 997009310b4..0378bbf4611 100644 --- a/guide/src/python-from-rust/calling-existing-code.md +++ b/guide/src/python-from-rust/calling-existing-code.md @@ -2,9 +2,9 @@ If you already have some existing Python code that you need to execute from Rust, the following FAQs can help you select the right PyO3 functionality for your situation: -## Want to access Python APIs? Then use `PyModule::import_bound`. +## Want to access Python APIs? Then use `PyModule::import`. -[`PyModule::import_bound`]({{#PYO3_DOCS_URL}}/pyo3/types/struct.PyModule.html#method.import_bound) can +[`PyModule::import`]({{#PYO3_DOCS_URL}}/pyo3/types/struct.PyModule.html#method.import) can be used to get handle to a Python module from Rust. You can use this to import and use any Python module available in your environment. @@ -13,7 +13,7 @@ use pyo3::prelude::*; fn main() -> PyResult<()> { Python::with_gil(|py| { - let builtins = PyModule::import_bound(py, "builtins")?; + let builtins = PyModule::import(py, "builtins")?; let total: i32 = builtins .getattr("sum")? .call1((vec![1, 2, 3],))? @@ -95,9 +95,9 @@ assert userdata.as_tuple() == userdata_as_tuple # } ``` -## You have a Python file or code snippet? Then use `PyModule::from_code_bound`. +## You have a Python file or code snippet? Then use `PyModule::from_code`. -[`PyModule::from_code_bound`]({{#PYO3_DOCS_URL}}/pyo3/types/struct.PyModule.html#method.from_code_bound) +[`PyModule::from_code`]({{#PYO3_DOCS_URL}}/pyo3/types/struct.PyModule.html#method.from_code) can be used to generate a Python module which can then be used just as if it was imported with `PyModule::import`. @@ -106,21 +106,22 @@ to this function! ```rust use pyo3::{prelude::*, types::IntoPyDict}; +use pyo3_ffi::c_str; # fn main() -> PyResult<()> { Python::with_gil(|py| { - let activators = PyModule::from_code_bound( + let activators = PyModule::from_code( py, - r#" + c_str!(r#" def relu(x): """see https://en.wikipedia.org/wiki/Rectifier_(neural_networks)""" return max(0.0, x) def leaky_relu(x, slope=0.01): return x if x >= 0 else x * slope - "#, - "activators.py", - "activators", + "#), + c_str!("activators.py"), + c_str!("activators"), )?; let relu_result: f64 = activators.getattr("relu")?.call1((-1.0,))?.extract()?; @@ -171,7 +172,7 @@ fn main() -> PyResult<()> { ``` If `append_to_inittab` cannot be used due to constraints in the program, -an alternative is to create a module using [`PyModule::new_bound`] +an alternative is to create a module using [`PyModule::new`] and insert it manually into `sys.modules`: ```rust @@ -186,11 +187,11 @@ pub fn add_one(x: i64) -> i64 { fn main() -> PyResult<()> { Python::with_gil(|py| { // Create new module - let foo_module = PyModule::new_bound(py, "foo")?; + let foo_module = PyModule::new(py, "foo")?; foo_module.add_function(wrap_pyfunction!(add_one, &foo_module)?)?; // Import and get sys.modules - let sys = PyModule::import_bound(py, "sys")?; + let sys = PyModule::import(py, "sys")?; let py_modules: Bound<'_, PyDict> = sys.getattr("modules")?.downcast_into()?; // Insert foo into sys.modules @@ -249,16 +250,17 @@ The example below shows: `src/main.rs`: ```rust,ignore use pyo3::prelude::*; +use pyo3_ffi::c_str; fn main() -> PyResult<()> { - let py_foo = include_str!(concat!( + let py_foo = c_str!(include_str!(concat!( env!("CARGO_MANIFEST_DIR"), "/python_app/utils/foo.py" - )); - let py_app = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/python_app/app.py")); + ))); + let py_app = c_str!(include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/python_app/app.py"))); let from_python = Python::with_gil(|py| -> PyResult> { - PyModule::from_code_bound(py, py_foo, "utils.foo", "utils.foo")?; - let app: Py = PyModule::from_code_bound(py, py_app, "", "")? + PyModule::from_code(py, py_foo, c_str!("utils.foo"), c_str!("utils.foo"))?; + let app: Py = PyModule::from_code(py, py_app, c_str!(""), c_str!(""))? .getattr("run")? .into(); app.call0(py) @@ -283,19 +285,21 @@ that directory is `/usr/share/python_app`). ```rust,no_run use pyo3::prelude::*; use pyo3::types::PyList; +use pyo3_ffi::c_str; use std::fs; use std::path::Path; +use std::ffi::CString; fn main() -> PyResult<()> { let path = Path::new("/usr/share/python_app"); - let py_app = fs::read_to_string(path.join("app.py"))?; + let py_app = CString::new(fs::read_to_string(path.join("app.py"))?)?; let from_python = Python::with_gil(|py| -> PyResult> { let syspath = py .import("sys")? .getattr("path")? .downcast_into::()?; syspath.insert(0, &path)?; - let app: Py = PyModule::from_code_bound(py, &py_app, "", "")? + let app: Py = PyModule::from_code(py, py_app.as_c_str(), c_str!(""), c_str!(""))? .getattr("run")? .into(); app.call0(py) @@ -316,12 +320,13 @@ Use context managers by directly invoking `__enter__` and `__exit__`. ```rust use pyo3::prelude::*; +use pyo3_ffi::c_str; fn main() { Python::with_gil(|py| { - let custom_manager = PyModule::from_code_bound( + let custom_manager = PyModule::from_code( py, - r#" + c_str!(r#" class House(object): def __init__(self, address): self.address = address @@ -333,9 +338,9 @@ class House(object): else: print(f"Thank you for visiting {self.address}, come again soon!") - "#, - "house.py", - "house", + "#), + c_str!("house.py"), + c_str!("house"), ) .unwrap(); @@ -394,4 +399,4 @@ Python::with_gil(|py| -> PyResult<()> { ``` -[`PyModule::new_bound`]: {{#PYO3_DOCS_URL}}/pyo3/types/struct.PyModule.html#method.new_bound +[`PyModule::new`]: {{#PYO3_DOCS_URL}}/pyo3/types/struct.PyModule.html#method.new diff --git a/guide/src/python-from-rust/function-calls.md b/guide/src/python-from-rust/function-calls.md index 3f27b7d2da5..22fcd8cacf0 100644 --- a/guide/src/python-from-rust/function-calls.md +++ b/guide/src/python-from-rust/function-calls.md @@ -19,6 +19,7 @@ The example below calls a Python function behind a `PyObject` (aka `Py`) ```rust use pyo3::prelude::*; use pyo3::types::PyTuple; +use pyo3_ffi::c_str; fn main() -> PyResult<()> { let arg1 = "arg1"; @@ -26,17 +27,17 @@ fn main() -> PyResult<()> { let arg3 = "arg3"; Python::with_gil(|py| { - let fun: Py = PyModule::from_code_bound( + let fun: Py = PyModule::from_code( py, - "def example(*args, **kwargs): + c_str!("def example(*args, **kwargs): if args != (): print('called with args', args) if kwargs != {}: print('called with kwargs', kwargs) if args == () and kwargs == {}: - print('called with no arguments')", - "", - "", + print('called with no arguments')"), + c_str!(""), + c_str!(""), )? .getattr("example")? .into(); @@ -64,6 +65,7 @@ For the `call` and `call_method` APIs, `kwargs` are `Option<&Bound<'py, PyDict>> use pyo3::prelude::*; use pyo3::types::IntoPyDict; use std::collections::HashMap; +use pyo3_ffi::c_str; fn main() -> PyResult<()> { let key1 = "key1"; @@ -72,17 +74,17 @@ fn main() -> PyResult<()> { let val2 = 2; Python::with_gil(|py| { - let fun: Py = PyModule::from_code_bound( + let fun: Py = PyModule::from_code( py, - "def example(*args, **kwargs): + c_str!("def example(*args, **kwargs): if args != (): print('called with args', args) if kwargs != {}: print('called with kwargs', kwargs) if args == () and kwargs == {}: - print('called with no arguments')", - "", - "", + print('called with no arguments')"), + c_str!(""), + c_str!(""), )? .getattr("example")? .into(); diff --git a/newsfragments/4404.changed.md b/newsfragments/4404.changed.md new file mode 100644 index 00000000000..728c45ae694 --- /dev/null +++ b/newsfragments/4404.changed.md @@ -0,0 +1 @@ +`PyModule::from_code` now expects &CStr as arguments instead of `&str`. \ No newline at end of file diff --git a/pyo3-benches/benches/bench_call.rs b/pyo3-benches/benches/bench_call.rs index 8470c8768d3..ca18bbd5e60 100644 --- a/pyo3-benches/benches/bench_call.rs +++ b/pyo3-benches/benches/bench_call.rs @@ -3,10 +3,11 @@ use std::hint::black_box; use codspeed_criterion_compat::{criterion_group, criterion_main, Bencher, Criterion}; use pyo3::prelude::*; +use pyo3::ffi::c_str; macro_rules! test_module { ($py:ident, $code:literal) => { - PyModule::from_code_bound($py, $code, file!(), "test_module") + PyModule::from_code($py, c_str!($code), c_str!(file!()), c_str!("test_module")) .expect("module creation failed") }; } diff --git a/pyo3-ffi/src/moduleobject.rs b/pyo3-ffi/src/moduleobject.rs index b9026997d2e..04b0f4ac25f 100644 --- a/pyo3-ffi/src/moduleobject.rs +++ b/pyo3-ffi/src/moduleobject.rs @@ -21,6 +21,7 @@ pub unsafe fn PyModule_CheckExact(op: *mut PyObject) -> c_int { } extern "C" { + #[cfg_attr(PyPy, link_name = "PyPyModule_NewObject")] pub fn PyModule_NewObject(name: *mut PyObject) -> *mut PyObject; #[cfg_attr(PyPy, link_name = "PyPyModule_New")] pub fn PyModule_New(name: *const c_char) -> *mut PyObject; diff --git a/pytests/src/lib.rs b/pytests/src/lib.rs index cbd65c8012c..72f5feaa0f4 100644 --- a/pytests/src/lib.rs +++ b/pytests/src/lib.rs @@ -39,7 +39,7 @@ fn pyo3_pytests(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { // Inserting to sys.modules allows importing submodules nicely from Python // e.g. import pyo3_pytests.buf_and_str as bas - let sys = PyModule::import_bound(py, "sys")?; + let sys = PyModule::import(py, "sys")?; let sys_modules = sys.getattr("modules")?.downcast_into::()?; sys_modules.set_item("pyo3_pytests.awaitable", m.getattr("awaitable")?)?; sys_modules.set_item("pyo3_pytests.buf_and_str", m.getattr("buf_and_str")?)?; diff --git a/src/conversions/anyhow.rs b/src/conversions/anyhow.rs index 4167c5379e1..c5614b7159c 100644 --- a/src/conversions/anyhow.rs +++ b/src/conversions/anyhow.rs @@ -74,7 +74,7 @@ //! // could call inside an application... //! // This might return a `PyErr`. //! let res = Python::with_gil(|py| { -//! let zlib = PyModule::import_bound(py, "zlib")?; +//! let zlib = PyModule::import(py, "zlib")?; //! let decompress = zlib.getattr("decompress")?; //! let bytes = PyBytes::new(py, bytes); //! let value = decompress.call1((bytes,))?; diff --git a/src/conversions/eyre.rs b/src/conversions/eyre.rs index 87894783d27..23cb43a5a5b 100644 --- a/src/conversions/eyre.rs +++ b/src/conversions/eyre.rs @@ -73,7 +73,7 @@ //! // could call inside an application... //! // This might return a `PyErr`. //! let res = Python::with_gil(|py| { -//! let zlib = PyModule::import_bound(py, "zlib")?; +//! let zlib = PyModule::import(py, "zlib")?; //! let decompress = zlib.getattr("decompress")?; //! let bytes = PyBytes::new(py, bytes); //! let value = decompress.call1((bytes,))?; diff --git a/src/conversions/num_bigint.rs b/src/conversions/num_bigint.rs index d709824d702..a0c986f047b 100644 --- a/src/conversions/num_bigint.rs +++ b/src/conversions/num_bigint.rs @@ -381,6 +381,7 @@ mod tests { use super::*; use crate::types::{PyDict, PyModule}; use indoc::indoc; + use pyo3_ffi::c_str; fn rust_fib() -> impl Iterator where @@ -441,7 +442,7 @@ mod tests { } fn python_index_class(py: Python<'_>) -> Bound<'_, PyModule> { - let index_code = indoc!( + let index_code = c_str!(indoc!( r#" class C: def __init__(self, x): @@ -449,8 +450,8 @@ mod tests { def __index__(self): return self.x "# - ); - PyModule::from_code_bound(py, index_code, "index.py", "index").unwrap() + )); + PyModule::from_code(py, index_code, c_str!("index.py"), c_str!("index")).unwrap() } #[test] diff --git a/src/conversions/num_complex.rs b/src/conversions/num_complex.rs index 0b2e714b995..645d704c672 100644 --- a/src/conversions/num_complex.rs +++ b/src/conversions/num_complex.rs @@ -55,7 +55,7 @@ //! # //! # fn main() -> PyResult<()> { //! # Python::with_gil(|py| -> PyResult<()> { -//! # let module = PyModule::new_bound(py, "my_module")?; +//! # let module = PyModule::new(py, "my_module")?; //! # //! # module.add_function(&wrap_pyfunction!(get_eigenvalues, module)?)?; //! # @@ -205,6 +205,7 @@ complex_conversion!(f64); mod tests { use super::*; use crate::types::{complex::PyComplexMethods, PyModule}; + use pyo3_ffi::c_str; #[test] fn from_complex() { @@ -233,18 +234,20 @@ mod tests { #[test] fn from_python_magic() { Python::with_gil(|py| { - let module = PyModule::from_code_bound( + let module = PyModule::from_code( py, - r#" + c_str!( + r#" class A: def __complex__(self): return 3.0+1.2j class B: def __float__(self): return 3.0 class C: def __index__(self): return 3 - "#, - "test.py", - "test", + "# + ), + c_str!("test.py"), + c_str!("test"), ) .unwrap(); let from_complex = module.getattr("A").unwrap().call0().unwrap(); @@ -271,9 +274,10 @@ class C: #[test] fn from_python_inherited_magic() { Python::with_gil(|py| { - let module = PyModule::from_code_bound( + let module = PyModule::from_code( py, - r#" + c_str!( + r#" class First: pass class ComplexMixin: def __complex__(self): return 3.0+1.2j @@ -284,9 +288,10 @@ class IndexMixin: class A(First, ComplexMixin): pass class B(First, FloatMixin): pass class C(First, IndexMixin): pass - "#, - "test.py", - "test", + "# + ), + c_str!("test.py"), + c_str!("test"), ) .unwrap(); let from_complex = module.getattr("A").unwrap().call0().unwrap(); @@ -315,16 +320,18 @@ class C(First, IndexMixin): pass // `type(inst).attr(inst)` equivalent to `inst.attr()` for methods, but this isn't the only // way the descriptor protocol might be implemented. Python::with_gil(|py| { - let module = PyModule::from_code_bound( + let module = PyModule::from_code( py, - r#" + c_str!( + r#" class A: @property def __complex__(self): return lambda: 3.0+1.2j - "#, - "test.py", - "test", + "# + ), + c_str!("test.py"), + c_str!("test"), ) .unwrap(); let obj = module.getattr("A").unwrap().call0().unwrap(); @@ -338,16 +345,18 @@ class A: fn from_python_nondescriptor_magic() { // Magic methods don't need to implement the descriptor protocol, if they're callable. Python::with_gil(|py| { - let module = PyModule::from_code_bound( + let module = PyModule::from_code( py, - r#" + c_str!( + r#" class MyComplex: def __call__(self): return 3.0+1.2j class A: __complex__ = MyComplex() - "#, - "test.py", - "test", + "# + ), + c_str!("test.py"), + c_str!("test"), ) .unwrap(); let obj = module.getattr("A").unwrap().call0().unwrap(); diff --git a/src/instance.rs b/src/instance.rs index 773431859c9..d49a1f4d6f4 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -923,7 +923,7 @@ impl<'a, 'py, T> BoundObject<'py, T> for Borrowed<'a, 'py, T> { /// # /// # fn main() -> PyResult<()> { /// # Python::with_gil(|py| { -/// # let m = pyo3::types::PyModule::new_bound(py, "test")?; +/// # let m = pyo3::types::PyModule::new(py, "test")?; /// # m.add_class::()?; /// # /// # let foo: Bound<'_, Foo> = m.getattr("Foo")?.call0()?.downcast_into()?; @@ -960,7 +960,7 @@ impl<'a, 'py, T> BoundObject<'py, T> for Borrowed<'a, 'py, T> { /// # /// # fn main() -> PyResult<()> { /// # Python::with_gil(|py| { -/// # let m = pyo3::types::PyModule::new_bound(py, "test")?; +/// # let m = pyo3::types::PyModule::new(py, "test")?; /// # m.add_class::()?; /// # /// # let foo: Bound<'_, Foo> = m.getattr("Foo")?.call0()?.downcast_into()?; @@ -1451,7 +1451,7 @@ impl Py { /// } /// # /// # Python::with_gil(|py| { - /// # let ob = PyModule::new_bound(py, "empty").unwrap().into_py(py); + /// # let ob = PyModule::new(py, "empty").unwrap().into_py(py); /// # set_answer(ob, py).unwrap(); /// # }); /// ``` @@ -1902,6 +1902,8 @@ mod tests { use super::{Bound, Py, PyObject}; use crate::types::{dict::IntoPyDict, PyAnyMethods, PyCapsule, PyDict, PyString}; use crate::{ffi, Borrowed, PyAny, PyResult, Python, ToPyObject}; + use pyo3_ffi::c_str; + use std::ffi::CStr; #[test] fn test_call() { @@ -1966,12 +1968,14 @@ mod tests { use crate::types::PyModule; Python::with_gil(|py| { - const CODE: &str = r#" + const CODE: &CStr = c_str!( + r#" class A: pass a = A() - "#; - let module = PyModule::from_code_bound(py, CODE, "", "")?; + "# + ); + let module = PyModule::from_code(py, CODE, c_str!(""), c_str!(""))?; let instance: Py = module.getattr("a")?.into(); instance.getattr(py, "foo").unwrap_err(); @@ -1993,12 +1997,14 @@ a = A() use crate::types::PyModule; Python::with_gil(|py| { - const CODE: &str = r#" + const CODE: &CStr = c_str!( + r#" class A: pass a = A() - "#; - let module = PyModule::from_code_bound(py, CODE, "", "")?; + "# + ); + let module = PyModule::from_code(py, CODE, c_str!(""), c_str!(""))?; let instance: Py = module.getattr("a")?.into(); let foo = crate::intern!(py, "foo"); diff --git a/src/marker.rs b/src/marker.rs index 778ccf61c7d..37a48b2ec23 100644 --- a/src/marker.rs +++ b/src/marker.rs @@ -350,7 +350,7 @@ pub use nightly::Ungil; /// # Releasing and freeing memory /// /// The [`Python<'py>`] type can be used to create references to variables owned by the Python -/// interpreter, using functions such as [`Python::eval_bound`] and [`PyModule::import_bound`]. +/// interpreter, using functions such as [`Python::eval_bound`] and [`PyModule::import`]. #[derive(Copy, Clone)] pub struct Python<'py>(PhantomData<(&'py GILGuard, NotSend)>); @@ -674,7 +674,7 @@ impl<'py> Python<'py> { where N: IntoPy>, { - PyModule::import_bound(self, name) + PyModule::import(self, name) } /// Deprecated name for [`Python::import`]. diff --git a/src/pyclass_init.rs b/src/pyclass_init.rs index 28b4a1cd719..01983c79b13 100644 --- a/src/pyclass_init.rs +++ b/src/pyclass_init.rs @@ -183,7 +183,7 @@ impl PyClassInitializer { /// /// fn main() -> PyResult<()> { /// Python::with_gil(|py| { - /// let m = PyModule::new_bound(py, "example")?; + /// let m = PyModule::new(py, "example")?; /// m.add_class::()?; /// m.add_class::()?; /// diff --git a/src/types/any.rs b/src/types/any.rs index d3322d1c33f..06493e437c3 100644 --- a/src/types/any.rs +++ b/src/types/any.rs @@ -127,7 +127,7 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed { /// } /// # /// # Python::with_gil(|py| { - /// # let ob = PyModule::new_bound(py, "empty").unwrap(); + /// # let ob = PyModule::new(py, "empty").unwrap(); /// # set_answer(&ob).unwrap(); /// # }); /// ``` @@ -377,7 +377,7 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed { /// /// # fn main() -> PyResult<()> { /// Python::with_gil(|py| -> PyResult<()> { - /// let builtins = PyModule::import_bound(py, "builtins")?; + /// let builtins = PyModule::import(py, "builtins")?; /// let print = builtins.getattr("print")?; /// assert!(print.is_callable()); /// Ok(()) @@ -404,17 +404,19 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed { /// ```rust /// use pyo3::prelude::*; /// use pyo3::types::PyDict; + /// use pyo3_ffi::c_str; + /// use std::ffi::CStr; /// - /// const CODE: &str = r#" + /// const CODE: &CStr = c_str!(r#" /// def function(*args, **kwargs): /// assert args == ("hello",) /// assert kwargs == {"cruel": "world"} /// return "called with args and kwargs" - /// "#; + /// "#); /// /// # fn main() -> PyResult<()> { /// Python::with_gil(|py| { - /// let module = PyModule::from_code_bound(py, CODE, "", "")?; + /// let module = PyModule::from_code(py, CODE, c_str!(""), c_str!(""))?; /// let fun = module.getattr("function")?; /// let args = ("hello",); /// let kwargs = PyDict::new(py); @@ -442,7 +444,7 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed { /// /// # fn main() -> PyResult<()> { /// Python::with_gil(|py| -> PyResult<()> { - /// let module = PyModule::import_bound(py, "builtins")?; + /// let module = PyModule::import(py, "builtins")?; /// let help = module.getattr("help")?; /// help.call0()?; /// Ok(()) @@ -461,17 +463,19 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed { /// /// ```rust /// use pyo3::prelude::*; + /// use pyo3_ffi::c_str; + /// use std::ffi::CStr; /// - /// const CODE: &str = r#" + /// const CODE: &CStr = c_str!(r#" /// def function(*args, **kwargs): /// assert args == ("hello",) /// assert kwargs == {} /// return "called with args" - /// "#; + /// "#); /// /// # fn main() -> PyResult<()> { /// Python::with_gil(|py| { - /// let module = PyModule::from_code_bound(py, CODE, "", "")?; + /// let module = PyModule::from_code(py, CODE, c_str!(""), c_str!(""))?; /// let fun = module.getattr("function")?; /// let args = ("hello",); /// let result = fun.call1(args)?; @@ -494,19 +498,21 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed { /// ```rust /// use pyo3::prelude::*; /// use pyo3::types::PyDict; + /// use pyo3_ffi::c_str; + /// use std::ffi::CStr; /// - /// const CODE: &str = r#" + /// const CODE: &CStr = c_str!(r#" /// class A: /// def method(self, *args, **kwargs): /// assert args == ("hello",) /// assert kwargs == {"cruel": "world"} /// return "called with args and kwargs" /// a = A() - /// "#; + /// "#); /// /// # fn main() -> PyResult<()> { /// Python::with_gil(|py| { - /// let module = PyModule::from_code_bound(py, CODE, "", "")?; + /// let module = PyModule::from_code(py, CODE, c_str!(""), c_str!(""))?; /// let instance = module.getattr("a")?; /// let args = ("hello",); /// let kwargs = PyDict::new(py); @@ -538,19 +544,21 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed { /// /// ```rust /// use pyo3::prelude::*; + /// use pyo3_ffi::c_str; + /// use std::ffi::CStr; /// - /// const CODE: &str = r#" + /// const CODE: &CStr = c_str!(r#" /// class A: /// def method(self, *args, **kwargs): /// assert args == () /// assert kwargs == {} /// return "called with no arguments" /// a = A() - /// "#; + /// "#); /// /// # fn main() -> PyResult<()> { /// Python::with_gil(|py| { - /// let module = PyModule::from_code_bound(py, CODE, "", "")?; + /// let module = PyModule::from_code(py, CODE, c_str!(""), c_str!(""))?; /// let instance = module.getattr("a")?; /// let result = instance.call_method0("method")?; /// assert_eq!(result.extract::()?, "called with no arguments"); @@ -573,19 +581,21 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed { /// /// ```rust /// use pyo3::prelude::*; + /// use pyo3_ffi::c_str; + /// use std::ffi::CStr; /// - /// const CODE: &str = r#" + /// const CODE: &CStr = c_str!(r#" /// class A: /// def method(self, *args, **kwargs): /// assert args == ("hello",) /// assert kwargs == {} /// return "called with args" /// a = A() - /// "#; + /// "#); /// /// # fn main() -> PyResult<()> { /// Python::with_gil(|py| { - /// let module = PyModule::from_code_bound(py, CODE, "", "")?; + /// let module = PyModule::from_code(py, CODE, c_str!(""), c_str!(""))?; /// let instance = module.getattr("a")?; /// let args = ("hello",); /// let result = instance.call_method1("method", args)?; @@ -1528,13 +1538,15 @@ mod tests { types::{IntoPyDict, PyAny, PyAnyMethods, PyBool, PyInt, PyList, PyModule, PyTypeMethods}, Bound, PyTypeInfo, Python, ToPyObject, }; + use pyo3_ffi::c_str; #[test] fn test_lookup_special() { Python::with_gil(|py| { - let module = PyModule::from_code_bound( + let module = PyModule::from_code( py, - r#" + c_str!( + r#" class CustomCallable: def __call__(self): return 1 @@ -1564,9 +1576,10 @@ class ErrorInDescriptorInt: class NonHeapNonDescriptorInt: # A static-typed callable that doesn't implement `__get__`. These are pretty hard to come by. __int__ = int - "#, - "test.py", - "test", + "# + ), + c_str!("test.py"), + c_str!("test"), ) .unwrap(); @@ -1625,15 +1638,17 @@ class NonHeapNonDescriptorInt: #[test] fn test_call_method0() { Python::with_gil(|py| { - let module = PyModule::from_code_bound( + let module = PyModule::from_code( py, - r#" + c_str!( + r#" class SimpleClass: def foo(self): return 42 -"#, - file!(), - "test_module", +"# + ), + c_str!(file!()), + c_str!("test_module"), ) .expect("module creation failed"); diff --git a/src/types/capsule.rs b/src/types/capsule.rs index 83ce85faf47..a6a2ba30c7b 100644 --- a/src/types/capsule.rs +++ b/src/types/capsule.rs @@ -35,7 +35,7 @@ use std::os::raw::{c_char, c_int, c_void}; /// /// let capsule = PyCapsule::new(py, foo, Some(name.clone()))?; /// -/// let module = PyModule::import_bound(py, "builtins")?; +/// let module = PyModule::import(py, "builtins")?; /// module.add("capsule", capsule)?; /// /// let cap: &Foo = unsafe { PyCapsule::import(py, name.as_ref())? }; @@ -444,7 +444,7 @@ mod tests { let capsule = PyCapsule::new(py, foo, Some(name.clone()))?; - let module = PyModule::import_bound(py, "builtins")?; + let module = PyModule::import(py, "builtins")?; module.add("capsule", capsule)?; // check error when wrong named passed for capsule. diff --git a/src/types/module.rs b/src/types/module.rs index 873d3347fc0..7307dfd4c2d 100644 --- a/src/types/module.rs +++ b/src/types/module.rs @@ -7,7 +7,7 @@ use crate::types::{ any::PyAnyMethods, list::PyListMethods, PyAny, PyCFunction, PyDict, PyList, PyString, }; use crate::{exceptions, ffi, Bound, IntoPy, Py, PyObject, Python}; -use std::ffi::CString; +use std::ffi::{CStr, CString}; use std::str; /// Represents a Python [`module`][1] object. @@ -38,23 +38,29 @@ impl PyModule { /// /// # fn main() -> PyResult<()> { /// Python::with_gil(|py| -> PyResult<()> { - /// let module = PyModule::new_bound(py, "my_module")?; + /// let module = PyModule::new(py, "my_module")?; /// /// assert_eq!(module.name()?, "my_module"); /// Ok(()) /// })?; /// # Ok(())} /// ``` - pub fn new_bound<'py>(py: Python<'py>, name: &str) -> PyResult> { - // Could use PyModule_NewObject, but it doesn't exist on PyPy. - let name = CString::new(name)?; + pub fn new<'py>(py: Python<'py>, name: &str) -> PyResult> { + let name = PyString::new(py, name); unsafe { - ffi::PyModule_New(name.as_ptr()) + ffi::PyModule_NewObject(name.as_ptr()) .assume_owned_or_err(py) .downcast_into_unchecked() } } + /// Deprecated name for [`PyModule::new`]. + #[deprecated(since = "0.23.0", note = "renamed to `PyModule::new`")] + #[inline] + pub fn new_bound<'py>(py: Python<'py>, name: &str) -> PyResult> { + Self::new(py, name) + } + /// Imports the Python module with the specified name. /// /// # Examples @@ -64,7 +70,7 @@ impl PyModule { /// use pyo3::prelude::*; /// /// Python::with_gil(|py| { - /// let module = PyModule::import_bound(py, "antigravity").expect("No flying for you."); + /// let module = PyModule::import(py, "antigravity").expect("No flying for you."); /// }); /// # } /// ``` @@ -73,7 +79,7 @@ impl PyModule { /// ```python /// import antigravity /// ``` - pub fn import_bound(py: Python<'_>, name: N) -> PyResult> + pub fn import(py: Python<'_>, name: N) -> PyResult> where N: IntoPy>, { @@ -85,6 +91,16 @@ impl PyModule { } } + /// Deprecated name for [`PyModule::import`]. + #[deprecated(since = "0.23.0", note = "renamed to `PyModule::import`")] + #[inline] + pub fn import_bound(py: Python<'_>, name: N) -> PyResult> + where + N: IntoPy>, + { + Self::import(py, name) + } + /// Creates and loads a module named `module_name`, /// containing the Python code passed to `code` /// and pretending to live at `file_name`. @@ -108,13 +124,14 @@ impl PyModule { /// /// ```rust /// use pyo3::prelude::*; + /// use pyo3::ffi::c_str; /// /// # fn main() -> PyResult<()> { /// // This path is resolved relative to this file. - /// let code = include_str!("../../assets/script.py"); + /// let code = c_str!(include_str!("../../assets/script.py")); /// /// Python::with_gil(|py| -> PyResult<()> { - /// PyModule::from_code_bound(py, code, "example.py", "example")?; + /// PyModule::from_code(py, code, c_str!("example.py"), c_str!("example"))?; /// Ok(()) /// })?; /// # Ok(()) @@ -125,6 +142,8 @@ impl PyModule { /// /// ```rust /// use pyo3::prelude::*; + /// use pyo3::ffi::c_str; + /// use std::ffi::CString; /// /// # fn main() -> PyResult<()> { /// // This path is resolved by however the platform resolves paths, @@ -133,12 +152,31 @@ impl PyModule { /// let code = std::fs::read_to_string("assets/script.py")?; /// /// Python::with_gil(|py| -> PyResult<()> { - /// PyModule::from_code_bound(py, &code, "example.py", "example")?; + /// PyModule::from_code(py, CString::new(code)?.as_c_str(), c_str!("example.py"), c_str!("example"))?; /// Ok(()) /// })?; /// Ok(()) /// # } /// ``` + pub fn from_code<'py>( + py: Python<'py>, + code: &CStr, + file_name: &CStr, + module_name: &CStr, + ) -> PyResult> { + unsafe { + let code = ffi::Py_CompileString(code.as_ptr(), file_name.as_ptr(), ffi::Py_file_input) + .assume_owned_or_err(py)?; + + ffi::PyImport_ExecCodeModuleEx(module_name.as_ptr(), code.as_ptr(), file_name.as_ptr()) + .assume_owned_or_err(py) + .downcast_into() + } + } + + /// Deprecated name for [`PyModule::from_code`]. + #[deprecated(since = "0.23.0", note = "renamed to `PyModule::from_code`")] + #[inline] pub fn from_code_bound<'py>( py: Python<'py>, code: &str, @@ -149,14 +187,7 @@ impl PyModule { let filename = CString::new(file_name)?; let module = CString::new(module_name)?; - unsafe { - let code = ffi::Py_CompileString(data.as_ptr(), filename.as_ptr(), ffi::Py_file_input) - .assume_owned_or_err(py)?; - - ffi::PyImport_ExecCodeModuleEx(module.as_ptr(), code.as_ptr(), filename.as_ptr()) - .assume_owned_or_err(py) - .downcast_into() - } + Self::from_code(py, data.as_c_str(), filename.as_c_str(), module.as_c_str()) } } @@ -288,7 +319,7 @@ pub trait PyModuleMethods<'py>: crate::sealed::Sealed { /// /// #[pymodule] /// fn my_module(py: Python<'_>, module: &Bound<'_, PyModule>) -> PyResult<()> { - /// let submodule = PyModule::new_bound(py, "submodule")?; + /// let submodule = PyModule::new(py, "submodule")?; /// submodule.add("super_useful_constant", "important")?; /// /// module.add_submodule(&submodule)?; @@ -491,7 +522,7 @@ mod tests { #[test] fn module_import_and_name() { Python::with_gil(|py| { - let builtins = PyModule::import_bound(py, "builtins").unwrap(); + let builtins = PyModule::import(py, "builtins").unwrap(); assert_eq!(builtins.name().unwrap(), "builtins"); }) } @@ -500,7 +531,7 @@ mod tests { fn module_filename() { use crate::types::string::PyStringMethods; Python::with_gil(|py| { - let site = PyModule::import_bound(py, "site").unwrap(); + let site = PyModule::import(py, "site").unwrap(); assert!(site .filename() .unwrap() diff --git a/src/types/typeobject.rs b/src/types/typeobject.rs index 0c3b0a6aaa5..1d05015b591 100644 --- a/src/types/typeobject.rs +++ b/src/types/typeobject.rs @@ -253,6 +253,7 @@ mod tests { use crate::types::{PyAnyMethods, PyBool, PyInt, PyModule, PyTuple, PyType, PyTypeMethods}; use crate::PyAny; use crate::Python; + use pyo3_ffi::c_str; #[test] fn test_type_is_subclass() { @@ -313,14 +314,16 @@ mod tests { #[test] fn test_type_names_standard() { Python::with_gil(|py| { - let module = PyModule::from_code_bound( + let module = PyModule::from_code( py, - r#" + c_str!( + r#" class MyClass: pass -"#, - file!(), - "test_module", +"# + ), + c_str!(file!()), + c_str!("test_module"), ) .expect("module create failed"); @@ -350,15 +353,17 @@ class MyClass: #[test] fn test_type_names_nested() { Python::with_gil(|py| { - let module = PyModule::from_code_bound( + let module = PyModule::from_code( py, - r#" + c_str!( + r#" class OuterClass: class InnerClass: pass -"#, - file!(), - "test_module", +"# + ), + c_str!(file!()), + c_str!("test_module"), ) .expect("module create failed"); diff --git a/tests/test_class_basics.rs b/tests/test_class_basics.rs index 947ab66f894..5b91ca9e695 100644 --- a/tests/test_class_basics.rs +++ b/tests/test_class_basics.rs @@ -184,7 +184,7 @@ struct EmptyClassInModule {} #[ignore] fn empty_class_in_module() { Python::with_gil(|py| { - let module = PyModule::new_bound(py, "test_module.nested").unwrap(); + let module = PyModule::new(py, "test_module.nested").unwrap(); module.add_class::().unwrap(); let ty = module.getattr("EmptyClassInModule").unwrap(); diff --git a/tests/test_class_new.rs b/tests/test_class_new.rs index f2de5df42e2..f5f980e32b9 100644 --- a/tests/test_class_new.rs +++ b/tests/test_class_new.rs @@ -169,7 +169,7 @@ c = Class() assert c.from_rust is False "# ); - let globals = PyModule::import_bound(py, "__main__").unwrap().dict(); + let globals = PyModule::import(py, "__main__").unwrap().dict(); globals.set_item("SuperClass", super_cls).unwrap(); py.run_bound(source, Some(&globals), None) .map_err(|e| e.display(py)) diff --git a/tests/test_inheritance.rs b/tests/test_inheritance.rs index 1f02edacbc8..5abdd28185c 100644 --- a/tests/test_inheritance.rs +++ b/tests/test_inheritance.rs @@ -354,7 +354,7 @@ fn module_add_class_inherit_bool_fails() { struct ExtendsBool; Python::with_gil(|py| { - let m = PyModule::new_bound(py, "test_module").unwrap(); + let m = PyModule::new(py, "test_module").unwrap(); let err = m.add_class::().unwrap_err(); assert_eq!( diff --git a/tests/test_module.rs b/tests/test_module.rs index a300ff052d3..115bbf5b6be 100644 --- a/tests/test_module.rs +++ b/tests/test_module.rs @@ -5,6 +5,7 @@ use pyo3::prelude::*; use pyo3::py_run; use pyo3::types::PyString; use pyo3::types::{IntoPyDict, PyDict, PyTuple}; +use pyo3_ffi::c_str; #[path = "../src/tests/common.rs"] mod common; @@ -158,11 +159,11 @@ fn test_module_renaming() { #[test] fn test_module_from_code_bound() { Python::with_gil(|py| { - let adder_mod = PyModule::from_code_bound( + let adder_mod = PyModule::from_code( py, - "def add(a,b):\n\treturn a+b", - "adder_mod.py", - "adder_mod", + c_str!("def add(a,b):\n\treturn a+b"), + c_str!("adder_mod.py"), + c_str!("adder_mod"), ) .expect("Module code should be loaded"); @@ -279,10 +280,10 @@ fn superfunction() -> String { #[pymodule] fn supermodule(module: &Bound<'_, PyModule>) -> PyResult<()> { module.add_function(wrap_pyfunction!(superfunction, module)?)?; - let module_to_add = PyModule::new_bound(module.py(), "submodule")?; + let module_to_add = PyModule::new(module.py(), "submodule")?; submodule(&module_to_add)?; module.add_submodule(&module_to_add)?; - let module_to_add = PyModule::new_bound(module.py(), "submodule_with_init_fn")?; + let module_to_add = PyModule::new(module.py(), "submodule_with_init_fn")?; submodule_with_init_fn(&module_to_add)?; module.add_submodule(&module_to_add)?; Ok(()) diff --git a/tests/test_proto_methods.rs b/tests/test_proto_methods.rs index 6373640a2f9..40b66c934da 100644 --- a/tests/test_proto_methods.rs +++ b/tests/test_proto_methods.rs @@ -683,7 +683,7 @@ if sys.platform == "win32" and sys.version_info >= (3, 8, 0): asyncio.run(main()) "#; - let globals = PyModule::import_bound(py, "__main__").unwrap().dict(); + let globals = PyModule::import(py, "__main__").unwrap().dict(); globals.set_item("Once", once).unwrap(); py.run_bound(source, Some(&globals), None) .map_err(|e| e.display(py)) @@ -737,7 +737,7 @@ if sys.platform == "win32" and sys.version_info >= (3, 8, 0): asyncio.run(main()) "#; - let globals = PyModule::import_bound(py, "__main__").unwrap().dict(); + let globals = PyModule::import(py, "__main__").unwrap().dict(); globals.set_item("Once", once).unwrap(); globals .set_item("AsyncIterator", py.get_type::()) @@ -809,7 +809,7 @@ del c.counter assert c.counter.count == 1 "# ); - let globals = PyModule::import_bound(py, "__main__").unwrap().dict(); + let globals = PyModule::import(py, "__main__").unwrap().dict(); globals.set_item("Counter", counter).unwrap(); py.run_bound(source, Some(&globals), None) .map_err(|e| e.display(py)) diff --git a/tests/test_various.rs b/tests/test_various.rs index b7c1ea70cfa..dc6bbc76dba 100644 --- a/tests/test_various.rs +++ b/tests/test_various.rs @@ -139,7 +139,7 @@ fn test_pickle() { } fn add_module(module: Bound<'_, PyModule>) -> PyResult<()> { - PyModule::import_bound(module.py(), "sys")? + PyModule::import(module.py(), "sys")? .dict() .get_item("modules") .unwrap() @@ -149,7 +149,7 @@ fn test_pickle() { } Python::with_gil(|py| { - let module = PyModule::new_bound(py, "test_module").unwrap(); + let module = PyModule::new(py, "test_module").unwrap(); module.add_class::().unwrap(); add_module(module).unwrap(); let inst = Py::new(py, PickleSupport {}).unwrap(); From ffeb901db402189f16a8145c94cf024bed773cf6 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Wed, 14 Aug 2024 12:38:05 -0600 Subject: [PATCH 210/495] restore derive(Debug) for ffi types where it was removed (#4438) --- pyo3-ffi/src/cpython/object.rs | 1 + pyo3-ffi/src/datetime.rs | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/pyo3-ffi/src/cpython/object.rs b/pyo3-ffi/src/cpython/object.rs index 04811cbda9e..871f3b883d9 100644 --- a/pyo3-ffi/src/cpython/object.rs +++ b/pyo3-ffi/src/cpython/object.rs @@ -205,6 +205,7 @@ pub type printfunc = unsafe extern "C" fn(arg1: *mut PyObject, arg2: *mut ::libc::FILE, arg3: c_int) -> c_int; #[repr(C)] +#[derive(Debug)] pub struct PyTypeObject { #[cfg(all(PyPy, not(Py_3_9)))] pub ob_refcnt: Py_ssize_t, diff --git a/pyo3-ffi/src/datetime.rs b/pyo3-ffi/src/datetime.rs index 2ab76c3830f..bbee057d561 100644 --- a/pyo3-ffi/src/datetime.rs +++ b/pyo3-ffi/src/datetime.rs @@ -27,6 +27,7 @@ const _PyDateTime_TIME_DATASIZE: usize = 6; const _PyDateTime_DATETIME_DATASIZE: usize = 10; #[repr(C)] +#[derive(Debug)] /// Structure representing a `datetime.timedelta`. pub struct PyDateTime_Delta { pub ob_base: PyObject, @@ -45,6 +46,7 @@ pub struct PyDateTime_Delta { #[cfg(not(any(PyPy, GraalPy)))] #[repr(C)] +#[derive(Debug)] /// Structure representing a `datetime.time` without a `tzinfo` member. pub struct _PyDateTime_BaseTime { pub ob_base: PyObject, @@ -54,6 +56,7 @@ pub struct _PyDateTime_BaseTime { } #[repr(C)] +#[derive(Debug)] /// Structure representing a `datetime.time`. pub struct PyDateTime_Time { pub ob_base: PyObject, @@ -74,6 +77,7 @@ pub struct PyDateTime_Time { } #[repr(C)] +#[derive(Debug)] /// Structure representing a `datetime.date` pub struct PyDateTime_Date { pub ob_base: PyObject, @@ -87,6 +91,7 @@ pub struct PyDateTime_Date { #[cfg(not(any(PyPy, GraalPy)))] #[repr(C)] +#[derive(Debug)] /// Structure representing a `datetime.datetime` without a `tzinfo` member. pub struct _PyDateTime_BaseDateTime { pub ob_base: PyObject, @@ -96,6 +101,7 @@ pub struct _PyDateTime_BaseDateTime { } #[repr(C)] +#[derive(Debug)] /// Structure representing a `datetime.datetime`. pub struct PyDateTime_DateTime { pub ob_base: PyObject, From e14aab520c69d529e0ba231a5059bab98b00a42b Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Thu, 15 Aug 2024 15:59:37 +0100 Subject: [PATCH 211/495] remove some `methods` impl details from public API (#4441) --- newsfragments/4441.changed.md | 1 + pyo3-macros-backend/src/pyclass.rs | 4 ++-- pyo3-macros-backend/src/pyimpl.rs | 4 ++-- pyo3-macros-backend/src/pymethod.rs | 14 +++++++------- src/impl_/pyclass.rs | 16 +++++++++------- src/impl_/pyclass/lazy_type_object.rs | 3 ++- src/impl_/pymodule.rs | 3 ++- src/impl_/trampoline.rs | 2 +- src/lib.rs | 25 +++++++++++++++---------- src/pyclass/create_type_object.rs | 4 ++-- 10 files changed, 43 insertions(+), 33 deletions(-) create mode 100644 newsfragments/4441.changed.md diff --git a/newsfragments/4441.changed.md b/newsfragments/4441.changed.md new file mode 100644 index 00000000000..996b368807e --- /dev/null +++ b/newsfragments/4441.changed.md @@ -0,0 +1 @@ +Deprecate `IPowModulo`, `PyClassAttributeDef`, `PyGetterDef`, `PyMethodDef`, `PyMethodDefType`, and `PySetterDef` from PyO3's public API. diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index 164b5f7e921..5ad5e61da26 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -1541,8 +1541,8 @@ pub fn gen_complex_enum_variant_attr( let method_def = quote! { #pyo3_path::impl_::pyclass::MaybeRuntimePyMethodDef::Static( - #pyo3_path::class::PyMethodDefType::ClassAttribute({ - #pyo3_path::class::PyClassAttributeDef::new( + #pyo3_path::impl_::pymethods::PyMethodDefType::ClassAttribute({ + #pyo3_path::impl_::pymethods::PyClassAttributeDef::new( #python_name, #cls_type::#wrapper_ident ) diff --git a/pyo3-macros-backend/src/pyimpl.rs b/pyo3-macros-backend/src/pyimpl.rs index 2f054be165f..69b7bb04f48 100644 --- a/pyo3-macros-backend/src/pyimpl.rs +++ b/pyo3-macros-backend/src/pyimpl.rs @@ -219,8 +219,8 @@ pub fn gen_py_const(cls: &syn::Type, spec: &ConstSpec, ctx: &Ctx) -> MethodAndMe let method_def = quote! { #pyo3_path::impl_::pyclass::MaybeRuntimePyMethodDef::Static( - #pyo3_path::class::PyMethodDefType::ClassAttribute({ - #pyo3_path::class::PyClassAttributeDef::new( + #pyo3_path::impl_::pymethods::PyMethodDefType::ClassAttribute({ + #pyo3_path::impl_::pymethods::PyClassAttributeDef::new( #python_name, #cls::#wrapper_ident ) diff --git a/pyo3-macros-backend/src/pymethod.rs b/pyo3-macros-backend/src/pymethod.rs index 78d7dac2330..3fb13e17373 100644 --- a/pyo3-macros-backend/src/pymethod.rs +++ b/pyo3-macros-backend/src/pymethod.rs @@ -330,7 +330,7 @@ pub fn impl_py_method_def( let methoddef = spec.get_methoddef(quote! { #cls::#wrapper_ident }, doc, ctx); let method_def = quote! { #pyo3_path::impl_::pyclass::MaybeRuntimePyMethodDef::Static( - #pyo3_path::class::PyMethodDefType::#methoddef_type(#methoddef #add_flags) + #pyo3_path::impl_::pymethods::PyMethodDefType::#methoddef_type(#methoddef #add_flags) ) }; Ok(MethodAndMethodDef { @@ -510,8 +510,8 @@ fn impl_py_class_attribute( let method_def = quote! { #pyo3_path::impl_::pyclass::MaybeRuntimePyMethodDef::Static( - #pyo3_path::class::PyMethodDefType::ClassAttribute({ - #pyo3_path::class::PyClassAttributeDef::new( + #pyo3_path::impl_::pymethods::PyMethodDefType::ClassAttribute({ + #pyo3_path::impl_::pymethods::PyClassAttributeDef::new( #python_name, #cls::#wrapper_ident ) @@ -691,8 +691,8 @@ pub fn impl_py_setter_def( let method_def = quote! { #cfg_attrs #pyo3_path::impl_::pyclass::MaybeRuntimePyMethodDef::Static( - #pyo3_path::class::PyMethodDefType::Setter( - #pyo3_path::class::PySetterDef::new( + #pyo3_path::impl_::pymethods::PyMethodDefType::Setter( + #pyo3_path::impl_::pymethods::PySetterDef::new( #python_name, #cls::#wrapper_ident, #doc @@ -826,8 +826,8 @@ pub fn impl_py_getter_def( let method_def = quote! { #cfg_attrs #pyo3_path::impl_::pyclass::MaybeRuntimePyMethodDef::Static( - #pyo3_path::class::PyMethodDefType::Getter( - #pyo3_path::class::PyGetterDef::new( + #pyo3_path::impl_::pymethods::PyMethodDefType::Getter( + #pyo3_path::impl_::pymethods::PyGetterDef::new( #python_name, #cls::#wrapper_ident, #doc diff --git a/src/impl_/pyclass.rs b/src/impl_/pyclass.rs index fdc7c2edd25..1680eca34ac 100644 --- a/src/impl_/pyclass.rs +++ b/src/impl_/pyclass.rs @@ -1,12 +1,14 @@ use crate::{ exceptions::{PyAttributeError, PyNotImplementedError, PyRuntimeError, PyValueError}, ffi, - impl_::freelist::FreeList, - impl_::pycell::{GetBorrowChecker, PyClassMutability, PyClassObjectLayout}, + impl_::{ + freelist::FreeList, + pycell::{GetBorrowChecker, PyClassMutability, PyClassObjectLayout}, + pymethods::{PyGetterDef, PyMethodDefType}, + }, pyclass_init::PyObjectInit, types::{any::PyAnyMethods, PyBool}, - Borrowed, IntoPy, Py, PyAny, PyClass, PyErr, PyMethodDefType, PyResult, PyTypeInfo, Python, - ToPyObject, + Borrowed, IntoPy, Py, PyAny, PyClass, PyErr, PyResult, PyTypeInfo, Python, ToPyObject, }; use std::{ borrow::Cow, @@ -1249,7 +1251,7 @@ impl< doc: doc.as_ptr(), }) } else { - PyMethodDefType::Getter(crate::PyGetterDef { + PyMethodDefType::Getter(PyGetterDef { name, meth: pyo3_get_value_topyobject::, Offset>, doc, @@ -1263,7 +1265,7 @@ impl { pub const fn generate(&self, name: &'static CStr, doc: &'static CStr) -> PyMethodDefType { - PyMethodDefType::Getter(crate::PyGetterDef { + PyMethodDefType::Getter(PyGetterDef { name, meth: pyo3_get_value_topyobject::, doc, @@ -1292,7 +1294,7 @@ impl> where FieldT: PyO3GetField, { - PyMethodDefType::Getter(crate::PyGetterDef { + PyMethodDefType::Getter(PyGetterDef { name, meth: pyo3_get_value::, doc, diff --git a/src/impl_/pyclass/lazy_type_object.rs b/src/impl_/pyclass/lazy_type_object.rs index 08a5f17d4bd..be383a272f3 100644 --- a/src/impl_/pyclass/lazy_type_object.rs +++ b/src/impl_/pyclass/lazy_type_object.rs @@ -9,10 +9,11 @@ use crate::{ exceptions::PyRuntimeError, ffi, impl_::pyclass::MaybeRuntimePyMethodDef, + impl_::pymethods::PyMethodDefType, pyclass::{create_type_object, PyClassTypeObject}, sync::{GILOnceCell, GILProtected}, types::PyType, - Bound, PyClass, PyErr, PyMethodDefType, PyObject, PyResult, Python, + Bound, PyClass, PyErr, PyObject, PyResult, Python, }; use super::PyClassItemsIter; diff --git a/src/impl_/pymodule.rs b/src/impl_/pymodule.rs index 08d55bfa5e8..d199bf95aac 100644 --- a/src/impl_/pymodule.rs +++ b/src/impl_/pymodule.rs @@ -21,9 +21,10 @@ use std::sync::atomic::{AtomicI64, Ordering}; use crate::exceptions::PyImportError; use crate::{ ffi, + impl_::pymethods::PyMethodDef, sync::GILOnceCell, types::{PyCFunction, PyModule, PyModuleMethods}, - Bound, Py, PyClass, PyMethodDef, PyResult, PyTypeInfo, Python, + Bound, Py, PyClass, PyResult, PyTypeInfo, Python, }; /// `Sync` wrapper of `ffi::PyModuleDef`. diff --git a/src/impl_/trampoline.rs b/src/impl_/trampoline.rs index f485258e5e5..86dc0067c8b 100644 --- a/src/impl_/trampoline.rs +++ b/src/impl_/trampoline.rs @@ -12,7 +12,7 @@ use std::{ use crate::gil::GILGuard; use crate::{ callback::PyCallbackOutput, ffi, ffi_ptr_ext::FfiPtrExt, impl_::panic::PanicTrap, - methods::IPowModulo, panic::PanicException, types::PyModule, Py, PyResult, Python, + impl_::pymethods::IPowModulo, panic::PanicException, types::PyModule, Py, PyResult, Python, }; #[inline] diff --git a/src/lib.rs b/src/lib.rs index 267639a271d..8af911e0fbe 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -343,19 +343,24 @@ pub(crate) mod sealed; pub mod class { pub use self::gc::{PyTraverseError, PyVisit}; - #[doc(hidden)] - pub use self::methods::{ - PyClassAttributeDef, PyGetterDef, PyMethodDef, PyMethodDefType, PyMethodType, PySetterDef, - }; + pub use self::methods::*; #[doc(hidden)] pub mod methods { - // frozen with the contents of the `impl_::pymethods` module in 0.20, - // this should probably all be replaced with deprecated type aliases and removed. - pub use crate::impl_::pymethods::{ - IPowModulo, PyClassAttributeDef, PyGetterDef, PyMethodDef, PyMethodDefType, - PyMethodType, PySetterDef, - }; + #[deprecated(since = "0.23.0", note = "PyO3 implementation detail")] + pub type IPowModulo = crate::impl_::pymethods::IPowModulo; + #[deprecated(since = "0.23.0", note = "PyO3 implementation detail")] + pub type PyClassAttributeDef = crate::impl_::pymethods::PyClassAttributeDef; + #[deprecated(since = "0.23.0", note = "PyO3 implementation detail")] + pub type PyGetterDef = crate::impl_::pymethods::PyGetterDef; + #[deprecated(since = "0.23.0", note = "PyO3 implementation detail")] + pub type PyMethodDef = crate::impl_::pymethods::PyMethodDef; + #[deprecated(since = "0.23.0", note = "PyO3 implementation detail")] + pub type PyMethodDefType = crate::impl_::pymethods::PyMethodDefType; + #[deprecated(since = "0.23.0", note = "PyO3 implementation detail")] + pub type PyMethodType = crate::impl_::pymethods::PyMethodType; + #[deprecated(since = "0.23.0", note = "PyO3 implementation detail")] + pub type PySetterDef = crate::impl_::pymethods::PySetterDef; } /// Old module which contained some implementation details of the `#[pyproto]` module. diff --git a/src/pyclass/create_type_object.rs b/src/pyclass/create_type_object.rs index 00dc7ee36f8..ac5b6287e86 100644 --- a/src/pyclass/create_type_object.rs +++ b/src/pyclass/create_type_object.rs @@ -7,12 +7,12 @@ use crate::{ assign_sequence_item_from_mapping, get_sequence_item_from_mapping, tp_dealloc, tp_dealloc_with_gc, MaybeRuntimePyMethodDef, PyClassItemsIter, }, - pymethods::{Getter, Setter}, + pymethods::{Getter, PyGetterDef, PyMethodDefType, PySetterDef, Setter}, trampoline::trampoline, }, internal_tricks::ptr_from_ref, types::{typeobject::PyTypeMethods, PyType}, - Py, PyClass, PyGetterDef, PyMethodDefType, PyResult, PySetterDef, PyTypeInfo, Python, + Py, PyClass, PyResult, PyTypeInfo, Python, }; use std::{ collections::HashMap, From 92673308fcb6e35f2f053b23c7b0c30cb5153320 Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Thu, 15 Aug 2024 21:18:02 +0200 Subject: [PATCH 212/495] Reintroduce `Python::run` and `Python::eval` (#4435) * reintroduce `Python::eval` * reintroduce `Python::run` * switch to `&CStr` --- README.md | 5 +- guide/src/class/numeric.md | 6 +- guide/src/conversions/traits.md | 2 +- guide/src/module.md | 3 +- .../python-from-rust/calling-existing-code.md | 13 ++-- newsfragments/4435.changed.md | 1 + src/buffer.rs | 4 +- src/conversions/anyhow.rs | 10 ++- src/conversions/chrono.rs | 8 ++- src/conversions/eyre.rs | 10 ++- src/conversions/num_bigint.rs | 4 +- src/conversions/num_rational.rs | 26 ++++---- src/conversions/rust_decimal.rs | 27 ++++---- src/conversions/std/array.rs | 12 ++-- src/conversions/std/num.rs | 23 ++++--- src/conversions/std/slice.rs | 9 +-- src/err/mod.rs | 12 ++-- src/exceptions.rs | 63 ++++++++++-------- src/ffi/tests.rs | 16 ++--- src/gil.rs | 12 ++-- src/instance.rs | 6 +- src/lib.rs | 5 +- src/macros.rs | 8 +-- src/marker.rs | 66 ++++++++++++++----- src/tests/common.rs | 2 +- src/types/any.rs | 13 ++-- src/types/bytearray.rs | 4 +- src/types/dict.rs | 2 +- src/types/iterator.rs | 33 ++++++---- src/types/list.rs | 8 +-- src/types/mod.rs | 3 +- src/types/sequence.rs | 10 +-- src/types/set.rs | 7 +- src/types/string.rs | 15 +++-- src/types/traceback.rs | 23 ++++--- src/types/weakref/anyref.rs | 6 +- src/types/weakref/proxy.rs | 25 ++++--- src/types/weakref/reference.rs | 17 +++-- tests/test_anyhow.rs | 10 +-- tests/test_append_to_inittab.rs | 18 +++-- tests/test_arithmetics.rs | 2 +- tests/test_class_new.rs | 6 +- tests/test_coroutine.rs | 14 ++-- tests/test_datetime.rs | 35 +++++++--- tests/test_frompyobject.rs | 14 ++-- tests/test_gc.rs | 13 ++-- tests/test_inheritance.rs | 15 +++-- tests/test_proto_methods.rs | 22 ++++--- 48 files changed, 404 insertions(+), 264 deletions(-) create mode 100644 newsfragments/4435.changed.md diff --git a/README.md b/README.md index d0c53f0d86c..63c15846d88 100644 --- a/README.md +++ b/README.md @@ -146,6 +146,7 @@ Example program displaying the value of `sys.version` and the current user name: ```rust use pyo3::prelude::*; use pyo3::types::IntoPyDict; +use pyo3::ffi::c_str; fn main() -> PyResult<()> { Python::with_gil(|py| { @@ -153,8 +154,8 @@ fn main() -> PyResult<()> { let version: String = sys.getattr("version")?.extract()?; let locals = [("os", py.import("os")?)].into_py_dict(py); - let code = "os.getenv('USER') or os.getenv('USERNAME') or 'Unknown'"; - let user: String = py.eval_bound(code, None, Some(&locals))?.extract()?; + let code = c_str!("os.getenv('USER') or os.getenv('USERNAME') or 'Unknown'"); + let user: String = py.eval(code, None, Some(&locals))?.extract()?; println!("Hello {}, I'm Python {}", user, version); Ok(()) diff --git a/guide/src/class/numeric.md b/guide/src/class/numeric.md index adb2e46dbe3..4144f760def 100644 --- a/guide/src/class/numeric.md +++ b/guide/src/class/numeric.md @@ -330,7 +330,7 @@ fn my_module(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; Ok(()) } -# const SCRIPT: &'static str = r#" +# const SCRIPT: &'static std::ffi::CStr = pyo3::ffi::c_str!(r#" # def hash_djb2(s: str): # n = Number(0) # five = Number(5) @@ -379,7 +379,7 @@ fn my_module(m: &Bound<'_, PyModule>) -> PyResult<()> { # pass # assert Number(1337).__str__() == '1337' # assert Number(1337).__repr__() == 'Number(1337)' -"#; +"#); # # use pyo3::PyTypeInfo; @@ -389,7 +389,7 @@ fn my_module(m: &Bound<'_, PyModule>) -> PyResult<()> { # let globals = PyModule::import(py, "__main__")?.dict(); # globals.set_item("Number", Number::type_object_bound(py))?; # -# py.run_bound(SCRIPT, Some(&globals), None)?; +# py.run(SCRIPT, Some(&globals), None)?; # Ok(()) # }) # } diff --git a/guide/src/conversions/traits.md b/guide/src/conversions/traits.md index 6836e0bd9fb..7f61616eefe 100644 --- a/guide/src/conversions/traits.md +++ b/guide/src/conversions/traits.md @@ -157,7 +157,7 @@ struct RustyStruct { # # fn main() -> PyResult<()> { # Python::with_gil(|py| -> PyResult<()> { -# let py_dict = py.eval_bound("{'foo': 'foo', 'bar': 'bar', 'foobar': 'foobar'}", None, None)?; +# let py_dict = py.eval(pyo3::ffi::c_str!("{'foo': 'foo', 'bar': 'bar', 'foobar': 'foobar'}"), None, None)?; # let rustystruct: RustyStruct = py_dict.extract()?; # assert_eq!(rustystruct.foo, "foo"); # assert_eq!(rustystruct.bar, "bar"); diff --git a/guide/src/module.md b/guide/src/module.md index 4aac2937016..1b2aa49d8c9 100644 --- a/guide/src/module.md +++ b/guide/src/module.md @@ -88,10 +88,11 @@ fn func() -> String { # Python::with_gil(|py| { # use pyo3::wrap_pymodule; # use pyo3::types::IntoPyDict; +# use pyo3::ffi::c_str; # let parent_module = wrap_pymodule!(parent_module)(py); # let ctx = [("parent_module", parent_module)].into_py_dict(py); # -# py.run_bound("assert parent_module.child_module.func() == 'func'", None, Some(&ctx)).unwrap(); +# py.run(c_str!("assert parent_module.child_module.func() == 'func'"), None, Some(&ctx)).unwrap(); # }) ``` diff --git a/guide/src/python-from-rust/calling-existing-code.md b/guide/src/python-from-rust/calling-existing-code.md index 0378bbf4611..9ffe8d77c70 100644 --- a/guide/src/python-from-rust/calling-existing-code.md +++ b/guide/src/python-from-rust/calling-existing-code.md @@ -32,11 +32,12 @@ and return the evaluated value as a `Bound<'py, PyAny>` object. ```rust use pyo3::prelude::*; +use pyo3::ffi::c_str; # fn main() -> Result<(), ()> { Python::with_gil(|py| { let result = py - .eval_bound("[i * 10 for i in range(5)]", None, None) + .eval(c_str!("[i * 10 for i in range(5)]"), None, None) .map_err(|e| { e.print_and_set_sys_last_vars(py); })?; @@ -153,6 +154,7 @@ As an example, the below adds the module `foo` to the embedded interpreter: ```rust use pyo3::prelude::*; +use pyo3::ffi::c_str; #[pyfunction] fn add_one(x: i64) -> i64 { @@ -167,7 +169,7 @@ fn foo(foo_module: &Bound<'_, PyModule>) -> PyResult<()> { fn main() -> PyResult<()> { pyo3::append_to_inittab!(foo); - Python::with_gil(|py| Python::run_bound(py, "import foo; foo.add_one(6)", None, None)) + Python::with_gil(|py| Python::run(py, c_str!("import foo; foo.add_one(6)"), None, None)) } ``` @@ -178,6 +180,7 @@ and insert it manually into `sys.modules`: ```rust use pyo3::prelude::*; use pyo3::types::PyDict; +use pyo3::ffi::c_str; #[pyfunction] pub fn add_one(x: i64) -> i64 { @@ -198,7 +201,7 @@ fn main() -> PyResult<()> { py_modules.set_item("foo", foo_module)?; // Now we can import + run our python code - Python::run_bound(py, "import foo; foo.add_one(6)", None, None) + Python::run(py, c_str!("import foo; foo.add_one(6)"), None, None) }) } ``` @@ -320,7 +323,7 @@ Use context managers by directly invoking `__enter__` and `__exit__`. ```rust use pyo3::prelude::*; -use pyo3_ffi::c_str; +use pyo3::ffi::c_str; fn main() { Python::with_gil(|py| { @@ -349,7 +352,7 @@ class House(object): house.call_method0("__enter__").unwrap(); - let result = py.eval_bound("undefined_variable + 1", None, None); + let result = py.eval(c_str!("undefined_variable + 1"), None, None); // If the eval threw an exception we'll pass it through to the context manager. // Otherwise, __exit__ is called with empty arguments (Python "None"). diff --git a/newsfragments/4435.changed.md b/newsfragments/4435.changed.md new file mode 100644 index 00000000000..9de2b84df5e --- /dev/null +++ b/newsfragments/4435.changed.md @@ -0,0 +1 @@ +Reintroduced `Python::eval` and `Python::run` now take a `&CStr` instead of `&str`. \ No newline at end of file diff --git a/src/buffer.rs b/src/buffer.rs index f2e402aecd4..2a2e7659435 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -685,7 +685,7 @@ mod tests { #[test] fn test_debug() { Python::with_gil(|py| { - let bytes = py.eval_bound("b'abcde'", None, None).unwrap(); + let bytes = py.eval(ffi::c_str!("b'abcde'"), None, None).unwrap(); let buffer: PyBuffer = PyBuffer::get_bound(&bytes).unwrap(); let expected = format!( concat!( @@ -847,7 +847,7 @@ mod tests { #[test] fn test_bytes_buffer() { Python::with_gil(|py| { - let bytes = py.eval_bound("b'abcde'", None, None).unwrap(); + let bytes = py.eval(ffi::c_str!("b'abcde'"), None, None).unwrap(); let buffer = PyBuffer::get_bound(&bytes).unwrap(); assert_eq!(buffer.dimensions(), 1); assert_eq!(buffer.item_count(), 5); diff --git a/src/conversions/anyhow.rs b/src/conversions/anyhow.rs index c5614b7159c..00a966d4f21 100644 --- a/src/conversions/anyhow.rs +++ b/src/conversions/anyhow.rs @@ -121,8 +121,8 @@ impl From for PyErr { #[cfg(test)] mod test_anyhow { use crate::exceptions::{PyRuntimeError, PyValueError}; - use crate::prelude::*; use crate::types::IntoPyDict; + use crate::{ffi, prelude::*}; use anyhow::{anyhow, bail, Context, Result}; @@ -147,7 +147,9 @@ mod test_anyhow { Python::with_gil(|py| { let locals = [("err", pyerr)].into_py_dict(py); - let pyerr = py.run_bound("raise err", None, Some(&locals)).unwrap_err(); + let pyerr = py + .run(ffi::c_str!("raise err"), None, Some(&locals)) + .unwrap_err(); assert_eq!(pyerr.value_bound(py).to_string(), expected_contents); }) } @@ -164,7 +166,9 @@ mod test_anyhow { Python::with_gil(|py| { let locals = [("err", pyerr)].into_py_dict(py); - let pyerr = py.run_bound("raise err", None, Some(&locals)).unwrap_err(); + let pyerr = py + .run(ffi::c_str!("raise err"), None, Some(&locals)) + .unwrap_err(); assert_eq!(pyerr.value_bound(py).to_string(), expected_contents); }) } diff --git a/src/conversions/chrono.rs b/src/conversions/chrono.rs index 47c22f0c18f..25c5acd9963 100644 --- a/src/conversions/chrono.rs +++ b/src/conversions/chrono.rs @@ -794,13 +794,14 @@ mod tests { // tzdata there to make this work. #[cfg(all(Py_3_9, not(target_os = "windows")))] fn test_zoneinfo_is_not_fixed_offset() { + use crate::ffi; use crate::types::any::PyAnyMethods; use crate::types::dict::PyDictMethods; Python::with_gil(|py| { let locals = crate::types::PyDict::new(py); - py.run_bound( - "import zoneinfo; zi = zoneinfo.ZoneInfo('Europe/London')", + py.run( + ffi::c_str!("import zoneinfo; zi = zoneinfo.ZoneInfo('Europe/London')"), None, Some(&locals), ) @@ -1320,6 +1321,7 @@ mod tests { use crate::tests::common::CatchWarnings; use crate::types::IntoPyDict; use proptest::prelude::*; + use std::ffi::CString; proptest! { @@ -1330,7 +1332,7 @@ mod tests { let globals = [("datetime", py.import("datetime").unwrap())].into_py_dict(py); let code = format!("datetime.datetime.fromtimestamp({}).replace(tzinfo=datetime.timezone(datetime.timedelta(seconds={})))", timestamp, timedelta); - let t = py.eval_bound(&code, Some(&globals), None).unwrap(); + let t = py.eval(&CString::new(code).unwrap(), Some(&globals), None).unwrap(); // Get ISO 8601 string from python let py_iso_str = t.call_method0("isoformat").unwrap(); diff --git a/src/conversions/eyre.rs b/src/conversions/eyre.rs index 23cb43a5a5b..dadf6039f9a 100644 --- a/src/conversions/eyre.rs +++ b/src/conversions/eyre.rs @@ -126,8 +126,8 @@ impl From for PyErr { #[cfg(test)] mod tests { use crate::exceptions::{PyRuntimeError, PyValueError}; - use crate::prelude::*; use crate::types::IntoPyDict; + use crate::{ffi, prelude::*}; use eyre::{bail, eyre, Report, Result, WrapErr}; @@ -152,7 +152,9 @@ mod tests { Python::with_gil(|py| { let locals = [("err", pyerr)].into_py_dict(py); - let pyerr = py.run_bound("raise err", None, Some(&locals)).unwrap_err(); + let pyerr = py + .run(ffi::c_str!("raise err"), None, Some(&locals)) + .unwrap_err(); assert_eq!(pyerr.value_bound(py).to_string(), expected_contents); }) } @@ -169,7 +171,9 @@ mod tests { Python::with_gil(|py| { let locals = [("err", pyerr)].into_py_dict(py); - let pyerr = py.run_bound("raise err", None, Some(&locals)).unwrap_err(); + let pyerr = py + .run(ffi::c_str!("raise err"), None, Some(&locals)) + .unwrap_err(); assert_eq!(pyerr.value_bound(py).to_string(), expected_contents); }) } diff --git a/src/conversions/num_bigint.rs b/src/conversions/num_bigint.rs index a0c986f047b..5ac3bf9ef61 100644 --- a/src/conversions/num_bigint.rs +++ b/src/conversions/num_bigint.rs @@ -460,7 +460,9 @@ mod tests { let index = python_index_class(py); let locals = PyDict::new(py); locals.set_item("index", index).unwrap(); - let ob = py.eval_bound("index.C(10)", None, Some(&locals)).unwrap(); + let ob = py + .eval(ffi::c_str!("index.C(10)"), None, Some(&locals)) + .unwrap(); let _: BigInt = ob.extract().unwrap(); }); } diff --git a/src/conversions/num_rational.rs b/src/conversions/num_rational.rs index 65d0b6f3714..3843af95344 100644 --- a/src/conversions/num_rational.rs +++ b/src/conversions/num_rational.rs @@ -116,8 +116,8 @@ mod tests { fn test_negative_fraction() { Python::with_gil(|py| { let locals = PyDict::new(py); - py.run_bound( - "import fractions\npy_frac = fractions.Fraction(-0.125)", + py.run( + ffi::c_str!("import fractions\npy_frac = fractions.Fraction(-0.125)"), None, Some(&locals), ) @@ -132,8 +132,8 @@ mod tests { fn test_obj_with_incorrect_atts() { Python::with_gil(|py| { let locals = PyDict::new(py); - py.run_bound( - "not_fraction = \"contains_incorrect_atts\"", + py.run( + ffi::c_str!("not_fraction = \"contains_incorrect_atts\""), None, Some(&locals), ) @@ -147,8 +147,10 @@ mod tests { fn test_fraction_with_fraction_type() { Python::with_gil(|py| { let locals = PyDict::new(py); - py.run_bound( - "import fractions\npy_frac = fractions.Fraction(fractions.Fraction(10))", + py.run( + ffi::c_str!( + "import fractions\npy_frac = fractions.Fraction(fractions.Fraction(10))" + ), None, Some(&locals), ) @@ -164,8 +166,8 @@ mod tests { fn test_fraction_with_decimal() { Python::with_gil(|py| { let locals = PyDict::new(py); - py.run_bound( - "import fractions\n\nfrom decimal import Decimal\npy_frac = fractions.Fraction(Decimal(\"1.1\"))", + py.run( + ffi::c_str!("import fractions\n\nfrom decimal import Decimal\npy_frac = fractions.Fraction(Decimal(\"1.1\"))"), None, Some(&locals), ) @@ -181,8 +183,8 @@ mod tests { fn test_fraction_with_num_den() { Python::with_gil(|py| { let locals = PyDict::new(py); - py.run_bound( - "import fractions\npy_frac = fractions.Fraction(10,5)", + py.run( + ffi::c_str!("import fractions\npy_frac = fractions.Fraction(10,5)"), None, Some(&locals), ) @@ -246,8 +248,8 @@ mod tests { fn test_infinity() { Python::with_gil(|py| { let locals = PyDict::new(py); - let py_bound = py.run_bound( - "import fractions\npy_frac = fractions.Fraction(\"Infinity\")", + let py_bound = py.run( + ffi::c_str!("import fractions\npy_frac = fractions.Fraction(\"Infinity\")"), None, Some(&locals), ); diff --git a/src/conversions/rust_decimal.rs b/src/conversions/rust_decimal.rs index 4b8a2083ab6..c7056dd8a0e 100644 --- a/src/conversions/rust_decimal.rs +++ b/src/conversions/rust_decimal.rs @@ -120,7 +120,9 @@ mod test_rust_decimal { use super::*; use crate::types::dict::PyDictMethods; use crate::types::PyDict; + use std::ffi::CString; + use crate::ffi; #[cfg(not(target_arch = "wasm32"))] use proptest::prelude::*; @@ -134,11 +136,12 @@ mod test_rust_decimal { let locals = PyDict::new(py); locals.set_item("rs_dec", &rs_dec).unwrap(); // Checks if Rust Decimal -> Python Decimal conversion is correct - py.run_bound( - &format!( + py.run( + &CString::new(format!( "import decimal\npy_dec = decimal.Decimal({})\nassert py_dec == rs_dec", $py - ), + )) + .unwrap(), None, Some(&locals), ) @@ -175,10 +178,10 @@ mod test_rust_decimal { let rs_dec = num.into_py(py); let locals = PyDict::new(py); locals.set_item("rs_dec", &rs_dec).unwrap(); - py.run_bound( - &format!( + py.run( + &CString::new(format!( "import decimal\npy_dec = decimal.Decimal(\"{}\")\nassert py_dec == rs_dec", - num), + num)).unwrap(), None, Some(&locals)).unwrap(); let roundtripped: Decimal = rs_dec.extract(py).unwrap(); assert_eq!(num, roundtripped); @@ -200,8 +203,8 @@ mod test_rust_decimal { fn test_nan() { Python::with_gil(|py| { let locals = PyDict::new(py); - py.run_bound( - "import decimal\npy_dec = decimal.Decimal(\"NaN\")", + py.run( + ffi::c_str!("import decimal\npy_dec = decimal.Decimal(\"NaN\")"), None, Some(&locals), ) @@ -216,8 +219,8 @@ mod test_rust_decimal { fn test_scientific_notation() { Python::with_gil(|py| { let locals = PyDict::new(py); - py.run_bound( - "import decimal\npy_dec = decimal.Decimal(\"1e3\")", + py.run( + ffi::c_str!("import decimal\npy_dec = decimal.Decimal(\"1e3\")"), None, Some(&locals), ) @@ -233,8 +236,8 @@ mod test_rust_decimal { fn test_infinity() { Python::with_gil(|py| { let locals = PyDict::new(py); - py.run_bound( - "import decimal\npy_dec = decimal.Decimal(\"Infinity\")", + py.run( + ffi::c_str!("import decimal\npy_dec = decimal.Decimal(\"Infinity\")"), None, Some(&locals), ) diff --git a/src/conversions/std/array.rs b/src/conversions/std/array.rs index 3af13f20532..bae5c6eb2fa 100644 --- a/src/conversions/std/array.rs +++ b/src/conversions/std/array.rs @@ -166,7 +166,7 @@ mod tests { sync::atomic::{AtomicUsize, Ordering}, }; - use crate::types::any::PyAnyMethods; + use crate::{ffi, types::any::PyAnyMethods}; use crate::{types::PyList, IntoPy, PyResult, Python, ToPyObject}; #[test] @@ -194,8 +194,8 @@ mod tests { fn test_extract_bytearray_to_array() { Python::with_gil(|py| { let v: [u8; 33] = py - .eval_bound( - "bytearray(b'abcabcabcabcabcabcabcabcabcabcabc')", + .eval( + ffi::c_str!("bytearray(b'abcabcabcabcabcabcabcabcabcabcabc')"), None, None, ) @@ -210,7 +210,7 @@ mod tests { fn test_extract_small_bytearray_to_array() { Python::with_gil(|py| { let v: [u8; 3] = py - .eval_bound("bytearray(b'abc')", None, None) + .eval(ffi::c_str!("bytearray(b'abc')"), None, None) .unwrap() .extract() .unwrap(); @@ -234,7 +234,7 @@ mod tests { fn test_extract_invalid_sequence_length() { Python::with_gil(|py| { let v: PyResult<[u8; 3]> = py - .eval_bound("bytearray(b'abcdefg')", None, None) + .eval(ffi::c_str!("bytearray(b'abcdefg')"), None, None) .unwrap() .extract(); assert_eq!( @@ -260,7 +260,7 @@ mod tests { #[test] fn test_extract_non_iterable_to_array() { Python::with_gil(|py| { - let v = py.eval_bound("42", None, None).unwrap(); + let v = py.eval(ffi::c_str!("42"), None, None).unwrap(); v.extract::().unwrap(); v.extract::<[i32; 1]>().unwrap_err(); }); diff --git a/src/conversions/std/num.rs b/src/conversions/std/num.rs index aaaac8734b2..618ca8f142b 100644 --- a/src/conversions/std/num.rs +++ b/src/conversions/std/num.rs @@ -470,6 +470,9 @@ mod test_128bit_integers { #[cfg(not(target_arch = "wasm32"))] use proptest::prelude::*; + #[cfg(not(target_arch = "wasm32"))] + use std::ffi::CString; + #[cfg(not(target_arch = "wasm32"))] proptest! { #[test] @@ -478,7 +481,7 @@ mod test_128bit_integers { let x_py = x.into_py(py); let locals = PyDict::new(py); locals.set_item("x_py", x_py.clone_ref(py)).unwrap(); - py.run_bound(&format!("assert x_py == {}", x), None, Some(&locals)).unwrap(); + py.run(&CString::new(format!("assert x_py == {}", x)).unwrap(), None, Some(&locals)).unwrap(); let roundtripped: i128 = x_py.extract(py).unwrap(); assert_eq!(x, roundtripped); }) @@ -494,7 +497,7 @@ mod test_128bit_integers { let x_py = x.into_py(py); let locals = PyDict::new(py); locals.set_item("x_py", x_py.clone_ref(py)).unwrap(); - py.run_bound(&format!("assert x_py == {}", x), None, Some(&locals)).unwrap(); + py.run(&CString::new(format!("assert x_py == {}", x)).unwrap(), None, Some(&locals)).unwrap(); let roundtripped: NonZeroI128 = x_py.extract(py).unwrap(); assert_eq!(x, roundtripped); }) @@ -509,7 +512,7 @@ mod test_128bit_integers { let x_py = x.into_py(py); let locals = PyDict::new(py); locals.set_item("x_py", x_py.clone_ref(py)).unwrap(); - py.run_bound(&format!("assert x_py == {}", x), None, Some(&locals)).unwrap(); + py.run(&CString::new(format!("assert x_py == {}", x)).unwrap(), None, Some(&locals)).unwrap(); let roundtripped: u128 = x_py.extract(py).unwrap(); assert_eq!(x, roundtripped); }) @@ -525,7 +528,7 @@ mod test_128bit_integers { let x_py = x.into_py(py); let locals = PyDict::new(py); locals.set_item("x_py", x_py.clone_ref(py)).unwrap(); - py.run_bound(&format!("assert x_py == {}", x), None, Some(&locals)).unwrap(); + py.run(&CString::new(format!("assert x_py == {}", x)).unwrap(), None, Some(&locals)).unwrap(); let roundtripped: NonZeroU128 = x_py.extract(py).unwrap(); assert_eq!(x, roundtripped); }) @@ -567,7 +570,7 @@ mod test_128bit_integers { #[test] fn test_i128_overflow() { Python::with_gil(|py| { - let obj = py.eval_bound("(1 << 130) * -1", None, None).unwrap(); + let obj = py.eval(ffi::c_str!("(1 << 130) * -1"), None, None).unwrap(); let err = obj.extract::().unwrap_err(); assert!(err.is_instance_of::(py)); }) @@ -576,7 +579,7 @@ mod test_128bit_integers { #[test] fn test_u128_overflow() { Python::with_gil(|py| { - let obj = py.eval_bound("1 << 130", None, None).unwrap(); + let obj = py.eval(ffi::c_str!("1 << 130"), None, None).unwrap(); let err = obj.extract::().unwrap_err(); assert!(err.is_instance_of::(py)); }) @@ -620,7 +623,7 @@ mod test_128bit_integers { #[test] fn test_nonzero_i128_overflow() { Python::with_gil(|py| { - let obj = py.eval_bound("(1 << 130) * -1", None, None).unwrap(); + let obj = py.eval(ffi::c_str!("(1 << 130) * -1"), None, None).unwrap(); let err = obj.extract::().unwrap_err(); assert!(err.is_instance_of::(py)); }) @@ -629,7 +632,7 @@ mod test_128bit_integers { #[test] fn test_nonzero_u128_overflow() { Python::with_gil(|py| { - let obj = py.eval_bound("1 << 130", None, None).unwrap(); + let obj = py.eval(ffi::c_str!("1 << 130"), None, None).unwrap(); let err = obj.extract::().unwrap_err(); assert!(err.is_instance_of::(py)); }) @@ -638,7 +641,7 @@ mod test_128bit_integers { #[test] fn test_nonzero_i128_zero_value() { Python::with_gil(|py| { - let obj = py.eval_bound("0", None, None).unwrap(); + let obj = py.eval(ffi::c_str!("0"), None, None).unwrap(); let err = obj.extract::().unwrap_err(); assert!(err.is_instance_of::(py)); }) @@ -647,7 +650,7 @@ mod test_128bit_integers { #[test] fn test_nonzero_u128_zero_value() { Python::with_gil(|py| { - let obj = py.eval_bound("0", None, None).unwrap(); + let obj = py.eval(ffi::c_str!("0"), None, None).unwrap(); let err = obj.extract::().unwrap_err(); assert!(err.is_instance_of::(py)); }) diff --git a/src/conversions/std/slice.rs b/src/conversions/std/slice.rs index 107b0ad5ce3..4c25305f777 100644 --- a/src/conversions/std/slice.rs +++ b/src/conversions/std/slice.rs @@ -88,6 +88,7 @@ mod tests { use std::borrow::Cow; use crate::{ + ffi, types::{any::PyAnyMethods, PyBytes}, Python, ToPyObject, }; @@ -95,7 +96,7 @@ mod tests { #[test] fn test_extract_bytes() { Python::with_gil(|py| { - let py_bytes = py.eval_bound("b'Hello Python'", None, None).unwrap(); + let py_bytes = py.eval(ffi::c_str!("b'Hello Python'"), None, None).unwrap(); let bytes: &[u8] = py_bytes.extract().unwrap(); assert_eq!(bytes, b"Hello Python"); }); @@ -104,17 +105,17 @@ mod tests { #[test] fn test_cow_impl() { Python::with_gil(|py| { - let bytes = py.eval_bound(r#"b"foobar""#, None, None).unwrap(); + let bytes = py.eval(ffi::c_str!(r#"b"foobar""#), None, None).unwrap(); let cow = bytes.extract::>().unwrap(); assert_eq!(cow, Cow::<[u8]>::Borrowed(b"foobar")); let byte_array = py - .eval_bound(r#"bytearray(b"foobar")"#, None, None) + .eval(ffi::c_str!(r#"bytearray(b"foobar")"#), None, None) .unwrap(); let cow = byte_array.extract::>().unwrap(); assert_eq!(cow, Cow::<[u8]>::Owned(b"foobar".to_vec())); - let something_else_entirely = py.eval_bound("42", None, None).unwrap(); + let something_else_entirely = py.eval(ffi::c_str!("42"), None, None).unwrap(); something_else_entirely .extract::>() .unwrap_err(); diff --git a/src/err/mod.rs b/src/err/mod.rs index 6e1e00844d8..bb5c80f2ca0 100644 --- a/src/err/mod.rs +++ b/src/err/mod.rs @@ -929,7 +929,7 @@ impl_signed_integer!(isize); mod tests { use super::PyErrState; use crate::exceptions::{self, PyTypeError, PyValueError}; - use crate::{PyErr, PyTypeInfo, Python}; + use crate::{ffi, PyErr, PyTypeInfo, Python}; #[test] fn no_error() { @@ -1020,7 +1020,7 @@ mod tests { Python::with_gil(|py| { let err = py - .run_bound("raise Exception('banana')", None, None) + .run(ffi::c_str!("raise Exception('banana')"), None, None) .expect_err("raising should have given us an error"); let debug_str = format!("{:?}", err); @@ -1045,7 +1045,7 @@ mod tests { fn err_display() { Python::with_gil(|py| { let err = py - .run_bound("raise Exception('banana')", None, None) + .run(ffi::c_str!("raise Exception('banana')"), None, None) .expect_err("raising should have given us an error"); assert_eq!(err.to_string(), "Exception: banana"); }); @@ -1090,13 +1090,13 @@ mod tests { fn test_pyerr_cause() { Python::with_gil(|py| { let err = py - .run_bound("raise Exception('banana')", None, None) + .run(ffi::c_str!("raise Exception('banana')"), None, None) .expect_err("raising should have given us an error"); assert!(err.cause(py).is_none()); let err = py - .run_bound( - "raise Exception('banana') from Exception('apple')", + .run( + ffi::c_str!("raise Exception('banana') from Exception('apple')"), None, None, ) diff --git a/src/exceptions.rs b/src/exceptions.rs index ee35e4752e1..fbd2eb077b5 100644 --- a/src/exceptions.rs +++ b/src/exceptions.rs @@ -179,12 +179,12 @@ macro_rules! import_exception_bound { /// # locals.set_item("MyError", py.get_type::())?; /// # locals.set_item("raise_myerror", fun)?; /// # -/// # py.run_bound( +/// # py.run(pyo3::ffi::c_str!( /// # "try: /// # raise_myerror() /// # except MyError as e: /// # assert e.__doc__ == 'Some description.' -/// # assert str(e) == 'Some error happened.'", +/// # assert str(e) == 'Some error happened.'"), /// # None, /// # Some(&locals), /// # )?; @@ -346,9 +346,10 @@ except ", $name, " as e: ``` use pyo3::prelude::*; use pyo3::exceptions::Py", $name, "; +use pyo3::ffi::c_str; Python::with_gil(|py| { - let result: PyResult<()> = py.run_bound(\"raise ", $name, "\", None, None); + let result: PyResult<()> = py.run(c_str!(\"raise ", $name, "\"), None, None); let error_type = match result { Ok(_) => \"Not an error\", @@ -835,9 +836,13 @@ mod tests { .map_err(|e| e.display(py)) .expect("could not setitem"); - py.run_bound("assert isinstance(exc, socket.gaierror)", None, Some(&d)) - .map_err(|e| e.display(py)) - .expect("assertion failed"); + py.run( + ffi::c_str!("assert isinstance(exc, socket.gaierror)"), + None, + Some(&d), + ) + .map_err(|e| e.display(py)) + .expect("assertion failed"); }); } @@ -858,8 +863,8 @@ mod tests { .map_err(|e| e.display(py)) .expect("could not setitem"); - py.run_bound( - "assert isinstance(exc, email.errors.MessageError)", + py.run( + ffi::c_str!("assert isinstance(exc, email.errors.MessageError)"), None, Some(&d), ) @@ -876,19 +881,23 @@ mod tests { let error_type = py.get_type::(); let ctx = [("CustomError", error_type)].into_py_dict(py); let type_description: String = py - .eval_bound("str(CustomError)", None, Some(&ctx)) + .eval(ffi::c_str!("str(CustomError)"), None, Some(&ctx)) .unwrap() .extract() .unwrap(); assert_eq!(type_description, ""); - py.run_bound( - "assert CustomError('oops').args == ('oops',)", + py.run( + ffi::c_str!("assert CustomError('oops').args == ('oops',)"), + None, + Some(&ctx), + ) + .unwrap(); + py.run( + ffi::c_str!("assert CustomError.__doc__ is None"), None, Some(&ctx), ) .unwrap(); - py.run_bound("assert CustomError.__doc__ is None", None, Some(&ctx)) - .unwrap(); }); } @@ -899,7 +908,7 @@ mod tests { let error_type = py.get_type::(); let ctx = [("CustomError", error_type)].into_py_dict(py); let type_description: String = py - .eval_bound("str(CustomError)", None, Some(&ctx)) + .eval(ffi::c_str!("str(CustomError)"), None, Some(&ctx)) .unwrap() .extract() .unwrap(); @@ -918,19 +927,19 @@ mod tests { let error_type = py.get_type::(); let ctx = [("CustomError", error_type)].into_py_dict(py); let type_description: String = py - .eval_bound("str(CustomError)", None, Some(&ctx)) + .eval(ffi::c_str!("str(CustomError)"), None, Some(&ctx)) .unwrap() .extract() .unwrap(); assert_eq!(type_description, ""); - py.run_bound( - "assert CustomError('oops').args == ('oops',)", + py.run( + ffi::c_str!("assert CustomError('oops').args == ('oops',)"), None, Some(&ctx), ) .unwrap(); - py.run_bound( - "assert CustomError.__doc__ == 'Some docs'", + py.run( + ffi::c_str!("assert CustomError.__doc__ == 'Some docs'"), None, Some(&ctx), ) @@ -951,19 +960,19 @@ mod tests { let error_type = py.get_type::(); let ctx = [("CustomError", error_type)].into_py_dict(py); let type_description: String = py - .eval_bound("str(CustomError)", None, Some(&ctx)) + .eval(ffi::c_str!("str(CustomError)"), None, Some(&ctx)) .unwrap() .extract() .unwrap(); assert_eq!(type_description, ""); - py.run_bound( - "assert CustomError('oops').args == ('oops',)", + py.run( + ffi::c_str!("assert CustomError('oops').args == ('oops',)"), None, Some(&ctx), ) .unwrap(); - py.run_bound( - "assert CustomError.__doc__ == 'Some more docs'", + py.run( + ffi::c_str!("assert CustomError.__doc__ == 'Some more docs'"), None, Some(&ctx), ) @@ -975,7 +984,7 @@ mod tests { fn native_exception_debug() { Python::with_gil(|py| { let exc = py - .run_bound("raise Exception('banana')", None, None) + .run(ffi::c_str!("raise Exception('banana')"), None, None) .expect_err("raising should have given us an error") .into_value(py) .into_bound(py); @@ -990,7 +999,7 @@ mod tests { fn native_exception_display() { Python::with_gil(|py| { let exc = py - .run_bound("raise Exception('banana')", None, None) + .run(ffi::c_str!("raise Exception('banana')"), None, None) .expect_err("raising should have given us an error") .into_value(py) .into_bound(py); @@ -1070,7 +1079,7 @@ mod tests { ) }); test_exception!(PyUnicodeEncodeError, |py| py - .eval_bound("chr(40960).encode('ascii')", None, None) + .eval(ffi::c_str!("chr(40960).encode('ascii')"), None, None) .unwrap_err()); test_exception!(PyUnicodeTranslateError, |_| { PyUnicodeTranslateError::new_err(("\u{3042}", 0, 1, "ouch")) diff --git a/src/ffi/tests.rs b/src/ffi/tests.rs index 63a7a4ef352..ba2bd409cab 100644 --- a/src/ffi/tests.rs +++ b/src/ffi/tests.rs @@ -1,4 +1,4 @@ -use crate::ffi::*; +use crate::ffi::{self, *}; use crate::types::any::PyAnyMethods; use crate::Python; @@ -22,8 +22,8 @@ fn test_datetime_fromtimestamp() { }; let locals = PyDict::new(py); locals.set_item("dt", dt).unwrap(); - py.run_bound( - "import datetime; assert dt == datetime.datetime.fromtimestamp(100)", + py.run( + ffi::c_str!("import datetime; assert dt == datetime.datetime.fromtimestamp(100)"), None, Some(&locals), ) @@ -43,8 +43,8 @@ fn test_date_fromtimestamp() { }; let locals = PyDict::new(py); locals.set_item("dt", dt).unwrap(); - py.run_bound( - "import datetime; assert dt == datetime.date.fromtimestamp(100)", + py.run( + ffi::c_str!("import datetime; assert dt == datetime.date.fromtimestamp(100)"), None, Some(&locals), ) @@ -63,8 +63,8 @@ fn test_utc_timezone() { }; let locals = PyDict::new(py); locals.set_item("utc_timezone", utc_timezone).unwrap(); - py.run_bound( - "import datetime; assert utc_timezone is datetime.timezone.utc", + py.run( + ffi::c_str!("import datetime; assert utc_timezone is datetime.timezone.utc"), None, Some(&locals), ) @@ -287,7 +287,7 @@ fn test_get_tzinfo() { #[test] fn test_inc_dec_ref() { Python::with_gil(|py| { - let obj = py.eval_bound("object()", None, None).unwrap(); + let obj = py.eval(ffi::c_str!("object()"), None, None).unwrap(); let ref_count = obj.get_refcnt(); let ptr = obj.as_ptr(); diff --git a/src/gil.rs b/src/gil.rs index 71b95e55b18..ed3b80eac32 100644 --- a/src/gil.rs +++ b/src/gil.rs @@ -55,7 +55,7 @@ fn gil_is_acquired() -> bool { /// /// # fn main() -> PyResult<()> { /// pyo3::prepare_freethreaded_python(); -/// Python::with_gil(|py| py.run_bound("print('Hello World')", None, None)) +/// Python::with_gil(|py| py.run(pyo3::ffi::c_str!("print('Hello World')"), None, None)) /// # } /// ``` #[cfg(not(any(PyPy, GraalPy)))] @@ -98,7 +98,7 @@ pub fn prepare_freethreaded_python() { /// ```rust /// unsafe { /// pyo3::with_embedded_python_interpreter(|py| { -/// if let Err(e) = py.run_bound("print('Hello World')", None, None) { +/// if let Err(e) = py.run(pyo3::ffi::c_str!("print('Hello World')"), None, None) { /// // We must make sure to not return a `PyErr`! /// e.print(py); /// } @@ -428,12 +428,14 @@ mod tests { use super::GIL_COUNT; #[cfg(not(pyo3_disable_reference_pool))] use super::{gil_is_acquired, POOL}; + use crate::{ffi, PyObject, Python}; use crate::{gil::GILGuard, types::any::PyAnyMethods}; - use crate::{PyObject, Python}; use std::ptr::NonNull; fn get_object(py: Python<'_>) -> PyObject { - py.eval_bound("object()", None, None).unwrap().unbind() + py.eval(ffi::c_str!("object()"), None, None) + .unwrap() + .unbind() } #[cfg(not(pyo3_disable_reference_pool))] @@ -571,7 +573,7 @@ mod tests { fn dropping_gil_does_not_invalidate_references() { // Acquiring GIL for the second time should be safe - see #864 Python::with_gil(|py| { - let obj = Python::with_gil(|_| py.eval_bound("object()", None, None).unwrap()); + let obj = Python::with_gil(|_| py.eval(ffi::c_str!("object()"), None, None).unwrap()); // After gil2 drops, obj should still have a reference count of one assert_eq!(obj.get_refcnt(), 1); diff --git a/src/instance.rs b/src/instance.rs index d49a1f4d6f4..6e4e9ab23e7 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -2020,7 +2020,7 @@ a = A() #[test] fn invalid_attr() -> PyResult<()> { Python::with_gil(|py| { - let instance: Py = py.eval_bound("object()", None, None)?.into(); + let instance: Py = py.eval(ffi::c_str!("object()"), None, None)?.into(); instance.getattr(py, "foo").unwrap_err(); @@ -2033,7 +2033,7 @@ a = A() #[test] fn test_py2_from_py_object() { Python::with_gil(|py| { - let instance = py.eval_bound("object()", None, None).unwrap(); + let instance = py.eval(ffi::c_str!("object()"), None, None).unwrap(); let ptr = instance.as_ptr(); let instance: Bound<'_, PyAny> = instance.extract().unwrap(); assert_eq!(instance.as_ptr(), ptr); @@ -2044,7 +2044,7 @@ a = A() fn test_py2_into_py_object() { Python::with_gil(|py| { let instance = py - .eval_bound("object()", None, None) + .eval(ffi::c_str!("object()"), None, None) .unwrap() .as_borrowed() .to_owned(); diff --git a/src/lib.rs b/src/lib.rs index 8af911e0fbe..7f1329c9ea5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -232,6 +232,7 @@ //! ```rust //! use pyo3::prelude::*; //! use pyo3::types::IntoPyDict; +//! use pyo3::ffi::c_str; //! //! fn main() -> PyResult<()> { //! Python::with_gil(|py| { @@ -239,8 +240,8 @@ //! let version: String = sys.getattr("version")?.extract()?; //! //! let locals = [("os", py.import("os")?)].into_py_dict(py); -//! let code = "os.getenv('USER') or os.getenv('USERNAME') or 'Unknown'"; -//! let user: String = py.eval_bound(code, None, Some(&locals))?.extract()?; +//! let code = c_str!("os.getenv('USER') or os.getenv('USERNAME') or 'Unknown'"); +//! let user: String = py.eval(code, None, Some(&locals))?.extract()?; //! //! println!("Hello {}, I'm Python {}", user, version); //! Ok(()) diff --git a/src/macros.rs b/src/macros.rs index d121efb367e..1a021998bcf 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -83,13 +83,13 @@ macro_rules! py_run { $crate::py_run_impl!($py, $($val)+, $crate::indoc::indoc!($code)) }}; ($py:expr, $($val:ident)+, $code:expr) => {{ - $crate::py_run_impl!($py, $($val)+, &$crate::unindent::unindent($code)) + $crate::py_run_impl!($py, $($val)+, $crate::unindent::unindent($code)) }}; ($py:expr, *$dict:expr, $code:literal) => {{ $crate::py_run_impl!($py, *$dict, $crate::indoc::indoc!($code)) }}; ($py:expr, *$dict:expr, $code:expr) => {{ - $crate::py_run_impl!($py, *$dict, &$crate::unindent::unindent($code)) + $crate::py_run_impl!($py, *$dict, $crate::unindent::unindent($code)) }}; } @@ -105,12 +105,12 @@ macro_rules! py_run_impl { ($py:expr, *$dict:expr, $code:expr) => {{ use ::std::option::Option::*; #[allow(unused_imports)] - if let ::std::result::Result::Err(e) = $py.run_bound($code, None, Some(&$dict)) { + if let ::std::result::Result::Err(e) = $py.run(&::std::ffi::CString::new($code).unwrap(), None, Some(&$dict)) { e.print($py); // So when this c api function the last line called printed the error to stderr, // the output is only written into a buffer which is never flushed because we // panic before flushing. This is where this hack comes into place - $py.run_bound("import sys; sys.stderr.flush()", None, None) + $py.run($crate::ffi::c_str!("import sys; sys.stderr.flush()"), None, None) .unwrap(); ::std::panic!("{}", $code) } diff --git a/src/marker.rs b/src/marker.rs index 37a48b2ec23..e8c978ffe27 100644 --- a/src/marker.rs +++ b/src/marker.rs @@ -384,10 +384,11 @@ impl Python<'_> { /// /// ``` /// use pyo3::prelude::*; + /// use pyo3::ffi::c_str; /// /// # fn main() -> PyResult<()> { /// Python::with_gil(|py| -> PyResult<()> { - /// let x: i32 = py.eval_bound("5", None, None)?.extract()?; + /// let x: i32 = py.eval(c_str!("5"), None, None)?.extract()?; /// assert_eq!(x, 5); /// Ok(()) /// }) @@ -527,19 +528,34 @@ impl<'py> Python<'py> { /// /// ``` /// # use pyo3::prelude::*; + /// # use pyo3::ffi::c_str; /// # Python::with_gil(|py| { - /// let result = py.eval_bound("[i * 10 for i in range(5)]", None, None).unwrap(); + /// let result = py.eval(c_str!("[i * 10 for i in range(5)]"), None, None).unwrap(); /// let res: Vec = result.extract().unwrap(); /// assert_eq!(res, vec![0, 10, 20, 30, 40]) /// # }); /// ``` + pub fn eval( + self, + code: &CStr, + globals: Option<&Bound<'py, PyDict>>, + locals: Option<&Bound<'py, PyDict>>, + ) -> PyResult> { + self.run_code(code, ffi::Py_eval_input, globals, locals) + } + + /// Deprecated name for [`Python::eval`]. + #[deprecated(since = "0.23.0", note = "renamed to `Python::eval`")] + #[track_caller] + #[inline] pub fn eval_bound( self, code: &str, globals: Option<&Bound<'py, PyDict>>, locals: Option<&Bound<'py, PyDict>>, ) -> PyResult> { - self.run_code(code, ffi::Py_eval_input, globals, locals) + let code = CString::new(code)?; + self.eval(&code, globals, locals) } /// Executes one or more Python statements in the given context. @@ -555,15 +571,16 @@ impl<'py> Python<'py> { /// use pyo3::{ /// prelude::*, /// types::{PyBytes, PyDict}, + /// ffi::c_str, /// }; /// Python::with_gil(|py| { /// let locals = PyDict::new(py); - /// py.run_bound( + /// py.run(c_str!( /// r#" /// import base64 /// s = 'Hello Rust!' /// ret = base64.b64encode(s.encode('utf-8')) - /// "#, + /// "#), /// None, /// Some(&locals), /// ) @@ -576,9 +593,9 @@ impl<'py> Python<'py> { /// /// You can use [`py_run!`](macro.py_run.html) for a handy alternative of `run` /// if you don't need `globals` and unwrapping is OK. - pub fn run_bound( + pub fn run( self, - code: &str, + code: &CStr, globals: Option<&Bound<'py, PyDict>>, locals: Option<&Bound<'py, PyDict>>, ) -> PyResult<()> { @@ -588,6 +605,20 @@ impl<'py> Python<'py> { }) } + /// Deprecated name for [`Python::run`]. + #[deprecated(since = "0.23.0", note = "renamed to `Python::run`")] + #[track_caller] + #[inline] + pub fn run_bound( + self, + code: &str, + globals: Option<&Bound<'py, PyDict>>, + locals: Option<&Bound<'py, PyDict>>, + ) -> PyResult<()> { + let code = CString::new(code)?; + self.run(&code, globals, locals) + } + /// Runs code in the given context. /// /// `start` indicates the type of input expected: one of `Py_single_input`, @@ -597,12 +628,11 @@ impl<'py> Python<'py> { /// If `locals` is `None`, it defaults to the value of `globals`. fn run_code( self, - code: &str, + code: &CStr, start: c_int, globals: Option<&Bound<'py, PyDict>>, locals: Option<&Bound<'py, PyDict>>, ) -> PyResult> { - let code = CString::new(code)?; unsafe { let mptr = ffi::PyImport_AddModule(ffi::c_str!("__main__").as_ptr()); if mptr.is_null() { @@ -830,7 +860,7 @@ mod tests { Python::with_gil(|py| { // Make sure builtin names are accessible let v: i32 = py - .eval_bound("min(1, 2)", None, None) + .eval(ffi::c_str!("min(1, 2)"), None, None) .map_err(|e| e.display(py)) .unwrap() .extract() @@ -841,7 +871,7 @@ mod tests { // Inject our own global namespace let v: i32 = py - .eval_bound("foo + 29", Some(&d), None) + .eval(ffi::c_str!("foo + 29"), Some(&d), None) .unwrap() .extract() .unwrap(); @@ -849,7 +879,7 @@ mod tests { // Inject our own local namespace let v: i32 = py - .eval_bound("foo + 29", None, Some(&d)) + .eval(ffi::c_str!("foo + 29"), None, Some(&d)) .unwrap() .extract() .unwrap(); @@ -857,7 +887,7 @@ mod tests { // Make sure builtin names are still accessible when using a local namespace let v: i32 = py - .eval_bound("min(foo, 2)", None, Some(&d)) + .eval(ffi::c_str!("min(foo, 2)"), None, Some(&d)) .unwrap() .extract() .unwrap(); @@ -952,7 +982,7 @@ mod tests { assert_eq!(py.Ellipsis().to_string(), "Ellipsis"); let v = py - .eval_bound("...", None, None) + .eval(ffi::c_str!("..."), None, None) .map_err(|e| e.display(py)) .unwrap(); @@ -966,8 +996,12 @@ mod tests { Python::with_gil(|py| { let namespace = PyDict::new(py); - py.run_bound("class Foo: pass", Some(&namespace), Some(&namespace)) - .unwrap(); + py.run( + ffi::c_str!("class Foo: pass"), + Some(&namespace), + Some(&namespace), + ) + .unwrap(); assert!(matches!(namespace.get_item("Foo"), Ok(Some(..)))); assert!(matches!(namespace.get_item("__builtins__"), Ok(Some(..)))); }) diff --git a/src/tests/common.rs b/src/tests/common.rs index 8180c4cd1af..83c2f911672 100644 --- a/src/tests/common.rs +++ b/src/tests/common.rs @@ -40,7 +40,7 @@ mod inner { }}; // Case2: dict & no err_msg ($py:expr, *$dict:expr, $code:expr, $err:ident) => {{ - let res = $py.run_bound($code, None, Some(&$dict.as_borrowed())); + let res = $py.run(&std::ffi::CString::new($code).unwrap(), None, Some(&$dict.as_borrowed())); let err = res.expect_err(&format!("Did not raise {}", stringify!($err))); if !err.matches($py, $py.get_type::()) { panic!("Expected {} but got {:?}", stringify!($err), err) diff --git a/src/types/any.rs b/src/types/any.rs index 06493e437c3..e7c7c578e3e 100644 --- a/src/types/any.rs +++ b/src/types/any.rs @@ -1535,6 +1535,7 @@ impl<'py> Bound<'py, PyAny> { mod tests { use crate::{ basic::CompareOp, + ffi, types::{IntoPyDict, PyAny, PyAnyMethods, PyBool, PyInt, PyList, PyModule, PyTypeMethods}, Bound, PyTypeInfo, Python, ToPyObject, }; @@ -1617,7 +1618,7 @@ class NonHeapNonDescriptorInt: #[test] fn test_call_for_non_existing_method() { Python::with_gil(|py| { - let a = py.eval_bound("42", None, None).unwrap(); + let a = py.eval(ffi::c_str!("42"), None, None).unwrap(); a.call_method0("__str__").unwrap(); // ok assert!(a.call_method("nonexistent_method", (1,), None).is_err()); assert!(a.call_method0("nonexistent_method").is_err()); @@ -1667,7 +1668,7 @@ class SimpleClass: #[test] fn test_type() { Python::with_gil(|py| { - let obj = py.eval_bound("42", None, None).unwrap(); + let obj = py.eval(ffi::c_str!("42"), None, None).unwrap(); assert_eq!(obj.get_type().as_type_ptr(), obj.get_type_ptr()); }); } @@ -1675,9 +1676,9 @@ class SimpleClass: #[test] fn test_dir() { Python::with_gil(|py| { - let obj = py.eval_bound("42", None, None).unwrap(); + let obj = py.eval(ffi::c_str!("42"), None, None).unwrap(); let dir = py - .eval_bound("dir(42)", None, None) + .eval(ffi::c_str!("dir(42)"), None, None) .unwrap() .downcast_into::() .unwrap(); @@ -1733,7 +1734,7 @@ class SimpleClass: #[test] fn test_nan_eq() { Python::with_gil(|py| { - let nan = py.eval_bound("float('nan')", None, None).unwrap(); + let nan = py.eval(ffi::c_str!("float('nan')"), None, None).unwrap(); assert!(nan.compare(&nan).is_err()); }); } @@ -1922,7 +1923,7 @@ class SimpleClass: fn test_is_ellipsis() { Python::with_gil(|py| { let v = py - .eval_bound("...", None, None) + .eval(ffi::c_str!("..."), None, None) .map_err(|e| e.display(py)) .unwrap(); diff --git a/src/types/bytearray.rs b/src/types/bytearray.rs index d29d67e7c3e..d1bbd0ac7e4 100644 --- a/src/types/bytearray.rs +++ b/src/types/bytearray.rs @@ -188,14 +188,14 @@ pub trait PyByteArrayMethods<'py>: crate::sealed::Sealed { /// # let locals = pyo3::types::PyDict::new(py); /// # locals.set_item("a_valid_function", fun)?; /// # - /// # py.run_bound( + /// # py.run(pyo3::ffi::c_str!( /// # r#"b = bytearray(b"hello world") /// # a_valid_function(b) /// # /// # try: /// # a_valid_function(bytearray()) /// # except RuntimeError as e: - /// # assert str(e) == 'input is not long enough'"#, + /// # assert str(e) == 'input is not long enough'"#), /// # None, /// # Some(&locals), /// # )?; diff --git a/src/types/dict.rs b/src/types/dict.rs index 0fb6a711013..50a9e139355 100644 --- a/src/types/dict.rs +++ b/src/types/dict.rs @@ -795,7 +795,7 @@ mod tests { fn test_set_item_refcnt() { Python::with_gil(|py| { let cnt; - let obj = py.eval_bound("object()", None, None).unwrap(); + let obj = py.eval(ffi::c_str!("object()"), None, None).unwrap(); { cnt = obj.get_refcnt(); let _dict = [(10, &obj)].into_py_dict(py); diff --git a/src/types/iterator.rs b/src/types/iterator.rs index 94d0fbf976b..788ed79c475 100644 --- a/src/types/iterator.rs +++ b/src/types/iterator.rs @@ -12,10 +12,11 @@ use crate::{ffi, Bound, PyAny, PyErr, PyResult, PyTypeCheck}; /// /// ```rust /// use pyo3::prelude::*; +/// use pyo3::ffi::c_str; /// /// # fn main() -> PyResult<()> { /// Python::with_gil(|py| -> PyResult<()> { -/// let list = py.eval_bound("iter([1, 2, 3, 4])", None, None)?; +/// let list = py.eval(c_str!("iter([1, 2, 3, 4])"), None, None)?; /// let numbers: PyResult> = list /// .iter()? /// .map(|i| i.and_then(|i|i.extract::())) @@ -107,7 +108,7 @@ mod tests { use super::PyIterator; use crate::exceptions::PyTypeError; use crate::types::{PyAnyMethods, PyDict, PyList, PyListMethods}; - use crate::{Python, ToPyObject}; + use crate::{ffi, Python, ToPyObject}; #[test] fn vec_iter() { @@ -154,7 +155,7 @@ mod tests { fn iter_item_refcnt() { Python::with_gil(|py| { let count; - let obj = py.eval_bound("object()", None, None).unwrap(); + let obj = py.eval(ffi::c_str!("object()"), None, None).unwrap(); let list = { let list = PyList::empty(py); list.append(10).unwrap(); @@ -180,21 +181,24 @@ mod tests { #[test] fn fibonacci_generator() { - let fibonacci_generator = r#" + let fibonacci_generator = ffi::c_str!( + r#" def fibonacci(target): a = 1 b = 1 for _ in range(target): yield a a, b = b, a + b -"#; +"# + ); Python::with_gil(|py| { let context = PyDict::new(py); - py.run_bound(fibonacci_generator, None, Some(&context)) - .unwrap(); + py.run(fibonacci_generator, None, Some(&context)).unwrap(); - let generator = py.eval_bound("fibonacci(5)", None, Some(&context)).unwrap(); + let generator = py + .eval(ffi::c_str!("fibonacci(5)"), None, Some(&context)) + .unwrap(); for (actual, expected) in generator.iter().unwrap().zip(&[1, 1, 2, 3, 5]) { let actual = actual.unwrap().extract::().unwrap(); assert_eq!(actual, *expected) @@ -207,22 +211,23 @@ def fibonacci(target): use crate::types::any::PyAnyMethods; use crate::Bound; - let fibonacci_generator = r#" + let fibonacci_generator = ffi::c_str!( + r#" def fibonacci(target): a = 1 b = 1 for _ in range(target): yield a a, b = b, a + b -"#; +"# + ); Python::with_gil(|py| { let context = PyDict::new(py); - py.run_bound(fibonacci_generator, None, Some(&context)) - .unwrap(); + py.run(fibonacci_generator, None, Some(&context)).unwrap(); let generator: Bound<'_, PyIterator> = py - .eval_bound("fibonacci(5)", None, Some(&context)) + .eval(ffi::c_str!("fibonacci(5)"), None, Some(&context)) .unwrap() .downcast_into() .unwrap(); @@ -321,7 +326,7 @@ def fibonacci(target): #[cfg(not(Py_LIMITED_API))] fn length_hint_becomes_size_hint_lower_bound() { Python::with_gil(|py| { - let list = py.eval_bound("[1, 2, 3]", None, None).unwrap(); + let list = py.eval(ffi::c_str!("[1, 2, 3]"), None, None).unwrap(); let iter = list.iter().unwrap(); let hint = iter.size_hint(); assert_eq!(hint, (3, None)); diff --git a/src/types/list.rs b/src/types/list.rs index 66e5f4661a3..0fa33d41a44 100644 --- a/src/types/list.rs +++ b/src/types/list.rs @@ -543,7 +543,7 @@ mod tests { use crate::types::list::PyListMethods; use crate::types::sequence::PySequenceMethods; use crate::types::{PyList, PyTuple}; - use crate::Python; + use crate::{ffi, Python}; use crate::{IntoPy, PyObject, ToPyObject}; #[test] @@ -603,7 +603,7 @@ mod tests { #[test] fn test_set_item_refcnt() { Python::with_gil(|py| { - let obj = py.eval_bound("object()", None, None).unwrap(); + let obj = py.eval(ffi::c_str!("object()"), None, None).unwrap(); let cnt; { let v = vec![2]; @@ -638,7 +638,7 @@ mod tests { fn test_insert_refcnt() { Python::with_gil(|py| { let cnt; - let obj = py.eval_bound("object()", None, None).unwrap(); + let obj = py.eval(ffi::c_str!("object()"), None, None).unwrap(); { let list = PyList::empty(py); cnt = obj.get_refcnt(); @@ -663,7 +663,7 @@ mod tests { fn test_append_refcnt() { Python::with_gil(|py| { let cnt; - let obj = py.eval_bound("object()", None, None).unwrap(); + let obj = py.eval(ffi::c_str!("object()"), None, None).unwrap(); { let list = PyList::empty(py); cnt = obj.get_refcnt(); diff --git a/src/types/mod.rs b/src/types/mod.rs index 114bb116626..9a54eee9661 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -58,10 +58,11 @@ pub use self::weakref::{PyWeakref, PyWeakrefMethods, PyWeakrefProxy, PyWeakrefRe /// ```rust /// use pyo3::prelude::*; /// use pyo3::types::PyDict; +/// use pyo3::ffi::c_str; /// /// # pub fn main() -> PyResult<()> { /// Python::with_gil(|py| { -/// let dict = py.eval_bound("{'a':'b', 'c':'d'}", None, None)?.downcast_into::()?; +/// let dict = py.eval(c_str!("{'a':'b', 'c':'d'}"), None, None)?.downcast_into::()?; /// /// for (key, value) in &dict { /// println!("key: {}, value: {}", key, value); diff --git a/src/types/sequence.rs b/src/types/sequence.rs index 530b1969d5d..e413e578cc9 100644 --- a/src/types/sequence.rs +++ b/src/types/sequence.rs @@ -376,12 +376,12 @@ impl PyTypeCheck for PySequence { #[cfg(test)] mod tests { use crate::types::{PyAnyMethods, PyList, PySequence, PySequenceMethods, PyTuple}; - use crate::{PyObject, Python, ToPyObject}; + use crate::{ffi, PyObject, Python, ToPyObject}; fn get_object() -> PyObject { // Convenience function for getting a single unique object Python::with_gil(|py| { - let obj = py.eval_bound("object()", None, None).unwrap(); + let obj = py.eval(ffi::c_str!("object()"), None, None).unwrap(); obj.to_object(py) }) @@ -750,7 +750,7 @@ mod tests { fn test_extract_tuple_to_vec() { Python::with_gil(|py| { let v: Vec = py - .eval_bound("(1, 2)", None, None) + .eval(ffi::c_str!("(1, 2)"), None, None) .unwrap() .extract() .unwrap(); @@ -762,7 +762,7 @@ mod tests { fn test_extract_range_to_vec() { Python::with_gil(|py| { let v: Vec = py - .eval_bound("range(1, 5)", None, None) + .eval(ffi::c_str!("range(1, 5)"), None, None) .unwrap() .extract() .unwrap(); @@ -774,7 +774,7 @@ mod tests { fn test_extract_bytearray_to_vec() { Python::with_gil(|py| { let v: Vec = py - .eval_bound("bytearray(b'abc')", None, None) + .eval(ffi::c_str!("bytearray(b'abc')"), None, None) .unwrap() .extract() .unwrap(); diff --git a/src/types/set.rs b/src/types/set.rs index 4bfc45fe80b..fd65d4bcaa4 100644 --- a/src/types/set.rs +++ b/src/types/set.rs @@ -277,6 +277,7 @@ pub(crate) fn try_new_from_iter( mod tests { use super::PySet; use crate::{ + ffi, types::{PyAnyMethods, PySetMethods}, Python, ToPyObject, }; @@ -367,7 +368,11 @@ mod tests { let val2 = set.pop(); assert!(val2.is_none()); assert!(py - .eval_bound("print('Exception state should not be set.')", None, None) + .eval( + ffi::c_str!("print('Exception state should not be set.')"), + None, + None + ) .is_ok()); }); } diff --git a/src/types/string.rs b/src/types/string.rs index e455389e47d..eac0f26eff0 100644 --- a/src/types/string.rs +++ b/src/types/string.rs @@ -592,7 +592,7 @@ mod tests { fn test_to_cow_surrogate() { Python::with_gil(|py| { let py_string = py - .eval_bound(r"'\ud800'", None, None) + .eval(ffi::c_str!(r"'\ud800'"), None, None) .unwrap() .downcast_into::() .unwrap(); @@ -621,7 +621,10 @@ mod tests { #[test] fn test_encode_utf8_surrogate() { Python::with_gil(|py| { - let obj: PyObject = py.eval_bound(r"'\ud800'", None, None).unwrap().into(); + let obj: PyObject = py + .eval(ffi::c_str!(r"'\ud800'"), None, None) + .unwrap() + .into(); assert!(obj .bind(py) .downcast::() @@ -635,7 +638,7 @@ mod tests { fn test_to_string_lossy() { Python::with_gil(|py| { let py_string = py - .eval_bound(r"'🐈 Hello \ud800World'", None, None) + .eval(ffi::c_str!(r"'🐈 Hello \ud800World'"), None, None) .unwrap() .downcast_into::() .unwrap(); @@ -707,7 +710,7 @@ mod tests { #[cfg(not(any(Py_LIMITED_API, PyPy)))] fn test_string_data_ucs2() { Python::with_gil(|py| { - let s = py.eval_bound("'foo\\ud800'", None, None).unwrap(); + let s = py.eval(ffi::c_str!("'foo\\ud800'"), None, None).unwrap(); let py_string = s.downcast::().unwrap(); let data = unsafe { py_string.data().unwrap() }; @@ -823,7 +826,7 @@ mod tests { fn test_py_to_str_surrogate() { Python::with_gil(|py| { let py_string: Py = py - .eval_bound(r"'\ud800'", None, None) + .eval(ffi::c_str!(r"'\ud800'"), None, None) .unwrap() .extract() .unwrap(); @@ -839,7 +842,7 @@ mod tests { fn test_py_to_string_lossy() { Python::with_gil(|py| { let py_string: Py = py - .eval_bound(r"'🐈 Hello \ud800World'", None, None) + .eval(ffi::c_str!(r"'🐈 Hello \ud800World'"), None, None) .unwrap() .extract() .unwrap(); diff --git a/src/types/traceback.rs b/src/types/traceback.rs index 98e40632439..89b26d8df72 100644 --- a/src/types/traceback.rs +++ b/src/types/traceback.rs @@ -35,11 +35,11 @@ pub trait PyTracebackMethods<'py>: crate::sealed::Sealed { /// The following code formats a Python traceback and exception pair from Rust: /// /// ```rust - /// # use pyo3::{Python, PyResult, prelude::PyTracebackMethods}; + /// # use pyo3::{Python, PyResult, prelude::PyTracebackMethods, ffi::c_str}; /// # let result: PyResult<()> = /// Python::with_gil(|py| { /// let err = py - /// .run_bound("raise Exception('banana')", None, None) + /// .run(c_str!("raise Exception('banana')"), None, None) /// .expect_err("raise will create a Python error"); /// /// let traceback = err.traceback_bound(py).expect("raised exception will have a traceback"); @@ -81,6 +81,7 @@ impl<'py> PyTracebackMethods<'py> for Bound<'py, PyTraceback> { #[cfg(test)] mod tests { use crate::{ + ffi, types::{any::PyAnyMethods, dict::PyDictMethods, traceback::PyTracebackMethods, PyDict}, IntoPy, PyErr, Python, }; @@ -89,7 +90,7 @@ mod tests { fn format_traceback() { Python::with_gil(|py| { let err = py - .run_bound("raise Exception('banana')", None, None) + .run(ffi::c_str!("raise Exception('banana')"), None, None) .expect_err("raising should have given us an error"); assert_eq!( @@ -104,13 +105,15 @@ mod tests { Python::with_gil(|py| { let locals = PyDict::new(py); // Produce an error from python so that it has a traceback - py.run_bound( - r" + py.run( + ffi::c_str!( + r" try: raise ValueError('raised exception') except Exception as e: err = e -", +" + ), None, Some(&locals), ) @@ -126,11 +129,13 @@ except Exception as e: Python::with_gil(|py| { let locals = PyDict::new(py); // Produce an error from python so that it has a traceback - py.run_bound( - r" + py.run( + ffi::c_str!( + r" def f(): raise ValueError('raised exception') -", +" + ), None, Some(&locals), ) diff --git a/src/types/weakref/anyref.rs b/src/types/weakref/anyref.rs index d4e0aa5e447..4f88a014689 100644 --- a/src/types/weakref/anyref.rs +++ b/src/types/weakref/anyref.rs @@ -787,11 +787,13 @@ mod tests { mod python_class { use super::*; + use crate::ffi; use crate::{py_result_ext::PyResultExt, types::PyType}; fn get_type(py: Python<'_>) -> PyResult> { - py.run_bound("class A:\n pass\n", None, None)?; - py.eval_bound("A", None, None).downcast_into::() + py.run(ffi::c_str!("class A:\n pass\n"), None, None)?; + py.eval(ffi::c_str!("A"), None, None) + .downcast_into::() } #[test] diff --git a/src/types/weakref/proxy.rs b/src/types/weakref/proxy.rs index 09054defe6f..80d254a38cf 100644 --- a/src/types/weakref/proxy.rs +++ b/src/types/weakref/proxy.rs @@ -100,6 +100,7 @@ impl PyWeakrefProxy { )] /// use pyo3::prelude::*; /// use pyo3::types::PyWeakrefProxy; + /// use pyo3::ffi::c_str; /// /// #[pyclass(weakref)] /// struct Foo { /* fields omitted */ } @@ -108,13 +109,13 @@ impl PyWeakrefProxy { /// fn callback(wref: Bound<'_, PyWeakrefProxy>) -> PyResult<()> { /// let py = wref.py(); /// assert!(wref.upgrade_as::()?.is_none()); - /// py.run_bound("counter = 1", None, None) + /// py.run(c_str!("counter = 1"), None, None) /// } /// /// # fn main() -> PyResult<()> { /// Python::with_gil(|py| { - /// py.run_bound("counter = 0", None, None)?; - /// assert_eq!(py.eval_bound("counter", None, None)?.extract::()?, 0); + /// py.run(c_str!("counter = 0"), None, None)?; + /// assert_eq!(py.eval(c_str!("counter"), None, None)?.extract::()?, 0); /// let foo = Bound::new(py, Foo{})?; /// /// // This is fine. @@ -125,7 +126,7 @@ impl PyWeakrefProxy { /// weakref.upgrade() /// .map_or(false, |obj| obj.is(&foo)) /// ); - /// assert_eq!(py.eval_bound("counter", None, None)?.extract::()?, 0); + /// assert_eq!(py.eval(c_str!("counter"), None, None)?.extract::()?, 0); /// /// let weakref2 = PyWeakrefProxy::new_bound_with(&foo, wrap_pyfunction!(callback, py)?)?; /// assert!(!weakref.is(&weakref2)); // Not the same weakref @@ -134,7 +135,7 @@ impl PyWeakrefProxy { /// drop(foo); /// /// assert!(weakref.upgrade_as::()?.is_none()); - /// assert_eq!(py.eval_bound("counter", None, None)?.extract::()?, 1); + /// assert_eq!(py.eval(c_str!("counter"), None, None)?.extract::()?, 1); /// Ok(()) /// }) /// # } @@ -236,11 +237,13 @@ mod tests { mod python_class { use super::*; + use crate::ffi; use crate::{py_result_ext::PyResultExt, types::PyType}; fn get_type(py: Python<'_>) -> PyResult> { - py.run_bound("class A:\n pass\n", None, None)?; - py.eval_bound("A", None, None).downcast_into::() + py.run(ffi::c_str!("class A:\n pass\n"), None, None)?; + py.eval(ffi::c_str!("A"), None, None) + .downcast_into::() } #[test] @@ -778,15 +781,17 @@ mod tests { mod python_class { use super::*; + use crate::ffi; use crate::{py_result_ext::PyResultExt, types::PyType}; fn get_type(py: Python<'_>) -> PyResult> { - py.run_bound( - "class A:\n def __call__(self):\n return 'This class is callable!'\n", + py.run( + ffi::c_str!("class A:\n def __call__(self):\n return 'This class is callable!'\n"), None, None, )?; - py.eval_bound("A", None, None).downcast_into::() + py.eval(ffi::c_str!("A"), None, None) + .downcast_into::() } #[test] diff --git a/src/types/weakref/reference.rs b/src/types/weakref/reference.rs index 383754e33fb..59f6f5bf3be 100644 --- a/src/types/weakref/reference.rs +++ b/src/types/weakref/reference.rs @@ -107,6 +107,7 @@ impl PyWeakrefReference { )] /// use pyo3::prelude::*; /// use pyo3::types::PyWeakrefReference; + /// use pyo3::ffi::c_str; /// /// #[pyclass(weakref)] /// struct Foo { /* fields omitted */ } @@ -115,13 +116,13 @@ impl PyWeakrefReference { /// fn callback(wref: Bound<'_, PyWeakrefReference>) -> PyResult<()> { /// let py = wref.py(); /// assert!(wref.upgrade_as::()?.is_none()); - /// py.run_bound("counter = 1", None, None) + /// py.run(c_str!("counter = 1"), None, None) /// } /// /// # fn main() -> PyResult<()> { /// Python::with_gil(|py| { - /// py.run_bound("counter = 0", None, None)?; - /// assert_eq!(py.eval_bound("counter", None, None)?.extract::()?, 0); + /// py.run(c_str!("counter = 0"), None, None)?; + /// assert_eq!(py.eval(c_str!("counter"), None, None)?.extract::()?, 0); /// let foo = Bound::new(py, Foo{})?; /// /// // This is fine. @@ -132,7 +133,7 @@ impl PyWeakrefReference { /// weakref.upgrade() /// .map_or(false, |obj| obj.is(&foo)) /// ); - /// assert_eq!(py.eval_bound("counter", None, None)?.extract::()?, 0); + /// assert_eq!(py.eval(c_str!("counter"), None, None)?.extract::()?, 0); /// /// let weakref2 = PyWeakrefReference::new_bound_with(&foo, wrap_pyfunction!(callback, py)?)?; /// assert!(!weakref.is(&weakref2)); // Not the same weakref @@ -141,7 +142,7 @@ impl PyWeakrefReference { /// drop(foo); /// /// assert!(weakref.upgrade_as::()?.is_none()); - /// assert_eq!(py.eval_bound("counter", None, None)?.extract::()?, 1); + /// assert_eq!(py.eval(c_str!("counter"), None, None)?.extract::()?, 1); /// Ok(()) /// }) /// # } @@ -229,11 +230,13 @@ mod tests { mod python_class { use super::*; + use crate::ffi; use crate::{py_result_ext::PyResultExt, types::PyType}; fn get_type(py: Python<'_>) -> PyResult> { - py.run_bound("class A:\n pass\n", None, None)?; - py.eval_bound("A", None, None).downcast_into::() + py.run(ffi::c_str!("class A:\n pass\n"), None, None)?; + py.eval(ffi::c_str!("A"), None, None) + .downcast_into::() } #[test] diff --git a/tests/test_anyhow.rs b/tests/test_anyhow.rs index 55ef3e8c46d..faa8a70f9c0 100644 --- a/tests/test_anyhow.rs +++ b/tests/test_anyhow.rs @@ -1,6 +1,6 @@ #![cfg(feature = "anyhow")] -use pyo3::wrap_pyfunction; +use pyo3::{ffi, wrap_pyfunction}; #[test] fn test_anyhow_py_function_ok_result() { @@ -40,10 +40,12 @@ fn test_anyhow_py_function_err_result() { let locals = PyDict::new(py); locals.set_item("func", func).unwrap(); - py.run_bound( - r#" + py.run( + ffi::c_str!( + r#" func() - "#, + "# + ), None, Some(&locals), ) diff --git a/tests/test_append_to_inittab.rs b/tests/test_append_to_inittab.rs index 94deb16a128..fdf14a2aea9 100644 --- a/tests/test_append_to_inittab.rs +++ b/tests/test_append_to_inittab.rs @@ -22,18 +22,20 @@ mod module_mod_with_functions { #[cfg(not(PyPy))] #[test] fn test_module_append_to_inittab() { - use pyo3::append_to_inittab; + use pyo3::{append_to_inittab, ffi}; append_to_inittab!(module_fn_with_functions); append_to_inittab!(module_mod_with_functions); Python::with_gil(|py| { - py.run_bound( - r#" + py.run( + ffi::c_str!( + r#" import module_fn_with_functions assert module_fn_with_functions.foo() == 123 -"#, +"# + ), None, None, ) @@ -42,11 +44,13 @@ assert module_fn_with_functions.foo() == 123 }); Python::with_gil(|py| { - py.run_bound( - r#" + py.run( + ffi::c_str!( + r#" import module_mod_with_functions assert module_mod_with_functions.foo() == 123 -"#, +"# + ), None, None, ) diff --git a/tests/test_arithmetics.rs b/tests/test_arithmetics.rs index 0cee2f9cf84..e7914a0bb95 100644 --- a/tests/test_arithmetics.rs +++ b/tests/test_arithmetics.rs @@ -685,7 +685,7 @@ mod return_not_implemented { py_expect_exception!( py, c2, - &format!("class Other: pass\nc2 {} Other()", operator), + format!("class Other: pass\nc2 {} Other()", operator), PyTypeError ); }); diff --git a/tests/test_class_new.rs b/tests/test_class_new.rs index f5f980e32b9..fb5ca91db81 100644 --- a/tests/test_class_new.rs +++ b/tests/test_class_new.rs @@ -156,7 +156,7 @@ impl SuperClass { fn subclass_new() { Python::with_gil(|py| { let super_cls = py.get_type::(); - let source = pyo3::indoc::indoc!( + let source = pyo3_ffi::c_str!(pyo3::indoc::indoc!( r#" class Class(SuperClass): def __new__(cls): @@ -168,10 +168,10 @@ class Class(SuperClass): c = Class() assert c.from_rust is False "# - ); + )); let globals = PyModule::import(py, "__main__").unwrap().dict(); globals.set_item("SuperClass", super_cls).unwrap(); - py.run_bound(source, Some(&globals), None) + py.run(source, Some(&globals), None) .map_err(|e| e.display(py)) .unwrap(); }); diff --git a/tests/test_coroutine.rs b/tests/test_coroutine.rs index fc382489911..e098e21e89c 100644 --- a/tests/test_coroutine.rs +++ b/tests/test_coroutine.rs @@ -1,6 +1,6 @@ #![cfg(feature = "experimental-async")] #![cfg(not(target_arch = "wasm32"))] -use std::{task::Poll, thread, time::Duration}; +use std::{ffi::CString, task::Poll, thread, time::Duration}; use futures::{channel::oneshot, future::poll_fn, FutureExt}; #[cfg(not(target_has_atomic = "64"))] @@ -151,8 +151,8 @@ fn cancelled_coroutine() { let globals = gil.import("__main__").unwrap().dict(); globals.set_item("sleep", sleep).unwrap(); let err = gil - .run_bound( - &pyo3::unindent::unindent(&handle_windows(test)), + .run( + &CString::new(pyo3::unindent::unindent(&handle_windows(test))).unwrap(), Some(&globals), None, ) @@ -191,8 +191,8 @@ fn coroutine_cancel_handle() { globals .set_item("cancellable_sleep", cancellable_sleep) .unwrap(); - gil.run_bound( - &pyo3::unindent::unindent(&handle_windows(test)), + gil.run( + &CString::new(pyo3::unindent::unindent(&handle_windows(test))).unwrap(), Some(&globals), None, ) @@ -221,8 +221,8 @@ fn coroutine_is_cancelled() { "#; let globals = gil.import("__main__").unwrap().dict(); globals.set_item("sleep_loop", sleep_loop).unwrap(); - gil.run_bound( - &pyo3::unindent::unindent(&handle_windows(test)), + gil.run( + &CString::new(pyo3::unindent::unindent(&handle_windows(test))).unwrap(), Some(&globals), None, ) diff --git a/tests/test_datetime.rs b/tests/test_datetime.rs index 93492fc33a3..214e1313d94 100644 --- a/tests/test_datetime.rs +++ b/tests/test_datetime.rs @@ -1,8 +1,9 @@ #![cfg(not(Py_LIMITED_API))] -use pyo3::prelude::*; use pyo3::types::{timezone_utc_bound, IntoPyDict, PyDate, PyDateTime, PyTime}; +use pyo3::{ffi, prelude::*}; use pyo3_ffi::PyDateTime_IMPORT; +use std::ffi::CString; fn _get_subclasses<'py>( py: Python<'py>, @@ -14,21 +15,33 @@ fn _get_subclasses<'py>( let locals = [(py_type, datetime.getattr(py_type)?)].into_py_dict(py); - let make_subclass_py = format!("class Subklass({}):\n pass", py_type); + let make_subclass_py = CString::new(format!("class Subklass({}):\n pass", py_type))?; - let make_sub_subclass_py = "class SubSubklass(Subklass):\n pass"; + let make_sub_subclass_py = ffi::c_str!("class SubSubklass(Subklass):\n pass"); - py.run_bound(&make_subclass_py, None, Some(&locals))?; - py.run_bound(make_sub_subclass_py, None, Some(&locals))?; + py.run(&make_subclass_py, None, Some(&locals))?; + py.run(make_sub_subclass_py, None, Some(&locals))?; // Construct an instance of the base class - let obj = py.eval_bound(&format!("{}({})", py_type, args), None, Some(&locals))?; + let obj = py.eval( + &CString::new(format!("{}({})", py_type, args))?, + None, + Some(&locals), + )?; // Construct an instance of the subclass - let sub_obj = py.eval_bound(&format!("Subklass({})", args), None, Some(&locals))?; + let sub_obj = py.eval( + &CString::new(format!("Subklass({})", args))?, + None, + Some(&locals), + )?; // Construct an instance of the sub-subclass - let sub_sub_obj = py.eval_bound(&format!("SubSubklass({})", args), None, Some(&locals))?; + let sub_sub_obj = py.eval( + &CString::new(format!("SubSubklass({})", args))?, + None, + Some(&locals), + )?; Ok((obj, sub_obj, sub_sub_obj)) } @@ -125,7 +138,11 @@ fn test_datetime_utc() { let locals = [("dt", dt)].into_py_dict(py); let offset: f32 = py - .eval_bound("dt.utcoffset().total_seconds()", None, Some(&locals)) + .eval( + ffi::c_str!("dt.utcoffset().total_seconds()"), + None, + Some(&locals), + ) .unwrap() .extract() .unwrap(); diff --git a/tests/test_frompyobject.rs b/tests/test_frompyobject.rs index c50541a30d7..96f34ffd5b6 100644 --- a/tests/test_frompyobject.rs +++ b/tests/test_frompyobject.rs @@ -510,8 +510,8 @@ pub struct Zap { fn test_from_py_with() { Python::with_gil(|py| { let py_zap = py - .eval_bound( - r#"{"name": "whatever", "my_object": [1, 2, 3]}"#, + .eval( + pyo3_ffi::c_str!(r#"{"name": "whatever", "my_object": [1, 2, 3]}"#), None, None, ) @@ -534,7 +534,7 @@ pub struct ZapTuple( fn test_from_py_with_tuple_struct() { Python::with_gil(|py| { let py_zap = py - .eval_bound(r#"("whatever", [1, 2, 3])"#, None, None) + .eval(pyo3_ffi::c_str!(r#"("whatever", [1, 2, 3])"#), None, None) .expect("failed to create tuple"); let zap = py_zap.extract::().unwrap(); @@ -548,7 +548,11 @@ fn test_from_py_with_tuple_struct() { fn test_from_py_with_tuple_struct_error() { Python::with_gil(|py| { let py_zap = py - .eval_bound(r#"("whatever", [1, 2, 3], "third")"#, None, None) + .eval( + pyo3_ffi::c_str!(r#"("whatever", [1, 2, 3], "third")"#), + None, + None, + ) .expect("failed to create tuple"); let f = py_zap.extract::(); @@ -574,7 +578,7 @@ pub enum ZapEnum { fn test_from_py_with_enum() { Python::with_gil(|py| { let py_zap = py - .eval_bound(r#"("whatever", [1, 2, 3])"#, None, None) + .eval(pyo3_ffi::c_str!(r#"("whatever", [1, 2, 3])"#), None, None) .expect("failed to create tuple"); let zap = py_zap.extract::().unwrap(); diff --git a/tests/test_gc.rs b/tests/test_gc.rs index 0d54065c9c9..b37901930be 100644 --- a/tests/test_gc.rs +++ b/tests/test_gc.rs @@ -2,6 +2,7 @@ use pyo3::class::PyTraverseError; use pyo3::class::PyVisit; +use pyo3::ffi; use pyo3::prelude::*; use pyo3::py_run; use std::cell::Cell; @@ -117,7 +118,8 @@ fn gc_integration() { }); Python::with_gil(|py| { - py.run_bound("import gc; gc.collect()", None, None).unwrap(); + py.run(ffi::c_str!("import gc; gc.collect()"), None, None) + .unwrap(); assert!(drop_called.load(Ordering::Relaxed)); }); } @@ -156,7 +158,8 @@ fn gc_null_traversal() { obj.borrow_mut(py).cycle = Some(obj.clone_ref(py)); // the object doesn't have to be cleaned up, it just needs to be traversed. - py.run_bound("import gc; gc.collect()", None, None).unwrap(); + py.run(ffi::c_str!("import gc; gc.collect()"), None, None) + .unwrap(); }); } @@ -471,7 +474,8 @@ fn drop_during_traversal_with_gil() { // (but not too many) collections to get `inst` actually dropped. for _ in 0..10 { Python::with_gil(|py| { - py.run_bound("import gc; gc.collect()", None, None).unwrap(); + py.run(ffi::c_str!("import gc; gc.collect()"), None, None) + .unwrap(); }); } assert!(drop_called.load(Ordering::Relaxed)); @@ -505,7 +509,8 @@ fn drop_during_traversal_without_gil() { // (but not too many) collections to get `inst` actually dropped. for _ in 0..10 { Python::with_gil(|py| { - py.run_bound("import gc; gc.collect()", None, None).unwrap(); + py.run(ffi::c_str!("import gc; gc.collect()"), None, None) + .unwrap(); }); } assert!(drop_called.load(Ordering::Relaxed)); diff --git a/tests/test_inheritance.rs b/tests/test_inheritance.rs index 5abdd28185c..a43ab57b6c1 100644 --- a/tests/test_inheritance.rs +++ b/tests/test_inheritance.rs @@ -3,6 +3,7 @@ use pyo3::prelude::*; use pyo3::py_run; +use pyo3::ffi; use pyo3::types::IntoPyDict; #[path = "../src/tests/common.rs"] @@ -22,8 +23,8 @@ fn subclass() { Python::with_gil(|py| { let d = [("SubclassAble", py.get_type::())].into_py_dict(py); - py.run_bound( - "class A(SubclassAble): pass\nassert issubclass(A, SubclassAble)", + py.run( + ffi::c_str!("class A(SubclassAble): pass\nassert issubclass(A, SubclassAble)"), None, Some(&d), ) @@ -99,8 +100,8 @@ fn mutation_fails() { let obj = Py::new(py, SubClass::new()).unwrap(); let global = [("obj", obj)].into_py_dict(py); let e = py - .run_bound( - "obj.base_set(lambda: obj.sub_set_and_ret(1))", + .run( + ffi::c_str!("obj.base_set(lambda: obj.sub_set_and_ret(1))"), Some(&global), None, ) @@ -244,7 +245,7 @@ mod inheriting_native_type { let dict_sub = pyo3::Py::new(py, DictWithName::new()).unwrap(); assert_eq!(dict_sub.get_refcnt(py), 1); - let item = &py.eval_bound("object()", None, None).unwrap(); + let item = &py.eval(ffi::c_str!("object()"), None, None).unwrap(); assert_eq!(item.get_refcnt(), 1); dict_sub.bind(py).set_item("foo", item).unwrap(); @@ -276,8 +277,8 @@ mod inheriting_native_type { Python::with_gil(|py| { let cls = py.get_type::(); let dict = [("cls", &cls)].into_py_dict(py); - let res = py.run_bound( - "e = cls('hello'); assert str(e) == 'hello'; assert e.context == 'Hello :)'; raise e", + let res = py.run( + ffi::c_str!("e = cls('hello'); assert str(e) == 'hello'; assert e.context == 'Hello :)'; raise e"), None, Some(&dict) ); diff --git a/tests/test_proto_methods.rs b/tests/test_proto_methods.rs index 40b66c934da..3cca16151aa 100644 --- a/tests/test_proto_methods.rs +++ b/tests/test_proto_methods.rs @@ -669,7 +669,8 @@ impl OnceFuture { fn test_await() { Python::with_gil(|py| { let once = py.get_type::(); - let source = r#" + let source = pyo3_ffi::c_str!( + r#" import asyncio import sys @@ -682,10 +683,11 @@ if sys.platform == "win32" and sys.version_info >= (3, 8, 0): asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) asyncio.run(main()) -"#; +"# + ); let globals = PyModule::import(py, "__main__").unwrap().dict(); globals.set_item("Once", once).unwrap(); - py.run_bound(source, Some(&globals), None) + py.run(source, Some(&globals), None) .map_err(|e| e.display(py)) .unwrap(); }); @@ -719,7 +721,8 @@ impl AsyncIterator { fn test_anext_aiter() { Python::with_gil(|py| { let once = py.get_type::(); - let source = r#" + let source = pyo3_ffi::c_str!( + r#" import asyncio import sys @@ -736,13 +739,14 @@ if sys.platform == "win32" and sys.version_info >= (3, 8, 0): asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) asyncio.run(main()) -"#; +"# + ); let globals = PyModule::import(py, "__main__").unwrap().dict(); globals.set_item("Once", once).unwrap(); globals .set_item("AsyncIterator", py.get_type::()) .unwrap(); - py.run_bound(source, Some(&globals), None) + py.run(source, Some(&globals), None) .map_err(|e| e.display(py)) .unwrap(); }); @@ -784,7 +788,7 @@ impl DescrCounter { fn descr_getset() { Python::with_gil(|py| { let counter = py.get_type::(); - let source = pyo3::indoc::indoc!( + let source = pyo3_ffi::c_str!(pyo3::indoc::indoc!( r#" class Class: counter = Counter() @@ -808,10 +812,10 @@ assert c.counter.count == 4 del c.counter assert c.counter.count == 1 "# - ); + )); let globals = PyModule::import(py, "__main__").unwrap().dict(); globals.set_item("Counter", counter).unwrap(); - py.run_bound(source, Some(&globals), None) + py.run(source, Some(&globals), None) .map_err(|e| e.display(py)) .unwrap(); }); From 7b2cf2401b6d6ab8f071e205bcaf271f5f0b29ba Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Thu, 15 Aug 2024 21:57:27 +0100 Subject: [PATCH 213/495] Pybytes specialization slices (#4442) * specialize collections holding bytes to turn into `PyBytes` rather than `PyList` * specialize `Cow<'_, T>` * add tests * add newsfragment * hide `iter_into_pyobject` * add migration entry * remove redundant generic * add benchmark for bytes into pyobject * bytes sequences conversions using `PyBytes::new` * restore `IntoPyObject for &u8` * update newsfragment PR number * bless ui test --------- Co-authored-by: Icxolu <10486322+Icxolu@users.noreply.github.com> --- guide/src/migration.md | 40 ++++++++++ newsfragments/4442.changed.md | 2 + pyo3-benches/Cargo.toml | 4 + pyo3-benches/benches/bench_intopyobject.rs | 93 ++++++++++++++++++++++ src/conversion.rs | 61 +++++++++++++- src/conversions/smallvec.rs | 40 +++++++--- src/conversions/std/array.rs | 57 ++++++------- src/conversions/std/num.rs | 89 ++++++++++++++++++++- src/conversions/std/slice.rs | 82 ++++++++++++++++--- src/conversions/std/vec.rs | 72 ++++++++++++++--- src/types/bytes.rs | 1 + tests/ui/missing_intopy.stderr | 4 +- 12 files changed, 479 insertions(+), 66 deletions(-) create mode 100644 newsfragments/4442.changed.md create mode 100644 pyo3-benches/benches/bench_intopyobject.rs diff --git a/guide/src/migration.md b/guide/src/migration.md index 1abb7202e4d..e0bd38a5153 100644 --- a/guide/src/migration.md +++ b/guide/src/migration.md @@ -92,6 +92,46 @@ where ``` +### Macro conversion changed for byte collections (`Vec`, `[u8; N]` and `SmallVec<[u8; N]>`). +
+Click to expand + +PyO3 0.23 introduced the new fallible conversion trait `IntoPyObject`. The `#[pyfunction]` and +`#[pymethods]` macros prefer `IntoPyObject` implementations over `IntoPy`. + +This change has an effect on functions and methods returning _byte_ collections like +- `Vec` +- `[u8; N]` +- `SmallVec<[u8; N]>` + +In their new `IntoPyObject` implementation these will now turn into `PyBytes` rather than a +`PyList`. All other `T`s are unaffected and still convert into a `PyList`. + +```rust +# #![allow(dead_code)] +# use pyo3::prelude::*; +#[pyfunction] +fn foo() -> Vec { // would previously turn into a `PyList`, now `PyBytes` + vec![0, 1, 2, 3] +} + +#[pyfunction] +fn bar() -> Vec { // unaffected, returns `PyList` + vec![0, 1, 2, 3] +} +``` + +If this conversion is _not_ desired, consider building a list manually using `PyList::new`. + +The following types were previously _only_ implemented for `u8` and now allow other `T`s turn into +`PyList` +- `&[T]` +- `Cow<[T]>` + +This is purely additional and should just extend the possible return types. + +
+ ## from 0.21.* to 0.22 ### Deprecation of `gil-refs` feature continues diff --git a/newsfragments/4442.changed.md b/newsfragments/4442.changed.md new file mode 100644 index 00000000000..44fbcbfe23c --- /dev/null +++ b/newsfragments/4442.changed.md @@ -0,0 +1,2 @@ +`IntoPyObject` impls for `Vec`, `&[u8]`, `[u8; N]`, `Cow<[u8]>` and `SmallVec<[u8; N]>` now +convert into `PyBytes` rather than `PyList`. \ No newline at end of file diff --git a/pyo3-benches/Cargo.toml b/pyo3-benches/Cargo.toml index e99ef09e19c..7a7f17d276b 100644 --- a/pyo3-benches/Cargo.toml +++ b/pyo3-benches/Cargo.toml @@ -47,6 +47,10 @@ harness = false name = "bench_gil" harness = false +[[bench]] +name = "bench_intopyobject" +harness = false + [[bench]] name = "bench_list" harness = false diff --git a/pyo3-benches/benches/bench_intopyobject.rs b/pyo3-benches/benches/bench_intopyobject.rs new file mode 100644 index 00000000000..16351c9088b --- /dev/null +++ b/pyo3-benches/benches/bench_intopyobject.rs @@ -0,0 +1,93 @@ +use std::hint::black_box; + +use codspeed_criterion_compat::{criterion_group, criterion_main, Bencher, Criterion}; + +use pyo3::conversion::IntoPyObject; +use pyo3::prelude::*; +use pyo3::types::PyBytes; + +fn bench_bytes_new(b: &mut Bencher<'_>, data: &[u8]) { + Python::with_gil(|py| { + b.iter_with_large_drop(|| PyBytes::new(py, black_box(data))); + }); +} + +fn bytes_new_small(b: &mut Bencher<'_>) { + bench_bytes_new(b, &[]); +} + +fn bytes_new_medium(b: &mut Bencher<'_>) { + let data = (0..u8::MAX).into_iter().collect::>(); + bench_bytes_new(b, &data); +} + +fn bytes_new_large(b: &mut Bencher<'_>) { + let data = vec![10u8; 100_000]; + bench_bytes_new(b, &data); +} + +fn bench_bytes_into_pyobject(b: &mut Bencher<'_>, data: &[u8]) { + Python::with_gil(|py| { + b.iter_with_large_drop(|| black_box(data).into_pyobject(py)); + }); +} + +fn byte_slice_into_pyobject_small(b: &mut Bencher<'_>) { + bench_bytes_into_pyobject(b, &[]); +} + +fn byte_slice_into_pyobject_medium(b: &mut Bencher<'_>) { + let data = (0..u8::MAX).into_iter().collect::>(); + bench_bytes_into_pyobject(b, &data); +} + +fn byte_slice_into_pyobject_large(b: &mut Bencher<'_>) { + let data = vec![10u8; 100_000]; + bench_bytes_into_pyobject(b, &data); +} + +fn byte_slice_into_py(b: &mut Bencher<'_>) { + Python::with_gil(|py| { + let data = (0..u8::MAX).into_iter().collect::>(); + let bytes = data.as_slice(); + b.iter_with_large_drop(|| black_box(bytes).into_py(py)); + }); +} + +fn vec_into_pyobject(b: &mut Bencher<'_>) { + Python::with_gil(|py| { + let bytes = (0..u8::MAX).into_iter().collect::>(); + b.iter_with_large_drop(|| black_box(&bytes).clone().into_pyobject(py)); + }); +} + +fn vec_into_py(b: &mut Bencher<'_>) { + Python::with_gil(|py| { + let bytes = (0..u8::MAX).into_iter().collect::>(); + b.iter_with_large_drop(|| black_box(&bytes).clone().into_py(py)); + }); +} + +fn criterion_benchmark(c: &mut Criterion) { + c.bench_function("bytes_new_small", bytes_new_small); + c.bench_function("bytes_new_medium", bytes_new_medium); + c.bench_function("bytes_new_large", bytes_new_large); + c.bench_function( + "byte_slice_into_pyobject_small", + byte_slice_into_pyobject_small, + ); + c.bench_function( + "byte_slice_into_pyobject_medium", + byte_slice_into_pyobject_medium, + ); + c.bench_function( + "byte_slice_into_pyobject_large", + byte_slice_into_pyobject_large, + ); + c.bench_function("byte_slice_into_py", byte_slice_into_py); + c.bench_function("vec_into_pyobject", vec_into_pyobject); + c.bench_function("vec_into_py", vec_into_py); +} + +criterion_group!(benches, criterion_benchmark); +criterion_main!(benches); diff --git a/src/conversion.rs b/src/conversion.rs index a5844bd5140..82710cf16fb 100644 --- a/src/conversion.rs +++ b/src/conversion.rs @@ -6,7 +6,7 @@ use crate::pyclass::boolean_struct::False; use crate::types::any::PyAnyMethods; use crate::types::PyTuple; use crate::{ - ffi, Borrowed, Bound, BoundObject, Py, PyAny, PyClass, PyObject, PyRef, PyRefMut, Python, + ffi, Borrowed, Bound, BoundObject, Py, PyAny, PyClass, PyErr, PyObject, PyRef, PyRefMut, Python, }; use std::convert::Infallible; @@ -200,6 +200,65 @@ pub trait IntoPyObject<'py>: Sized { /// Performs the conversion. fn into_pyobject(self, py: Python<'py>) -> Result; + + /// Converts sequence of Self into a Python object. Used to specialize `Vec`, `[u8; N]` + /// and `SmallVec<[u8; N]>` as a sequence of bytes into a `bytes` object. + #[doc(hidden)] + fn owned_sequence_into_pyobject( + iter: I, + py: Python<'py>, + _: private::Token, + ) -> Result, PyErr> + where + I: IntoIterator + AsRef<[Self]>, + I::IntoIter: ExactSizeIterator, + PyErr: From, + { + let mut iter = iter.into_iter().map(|e| { + e.into_pyobject(py) + .map(BoundObject::into_any) + .map(BoundObject::unbind) + .map_err(Into::into) + }); + let list = crate::types::list::try_new_from_iter(py, &mut iter); + list.map(Bound::into_any) + } + + /// Converts sequence of Self into a Python object. Used to specialize `&[u8]` and `Cow<[u8]>` + /// as a sequence of bytes into a `bytes` object. + #[doc(hidden)] + fn borrowed_sequence_into_pyobject( + iter: I, + py: Python<'py>, + _: private::Token, + ) -> Result, PyErr> + where + Self: private::Reference, + I: IntoIterator + AsRef<[::BaseType]>, + I::IntoIter: ExactSizeIterator, + PyErr: From, + { + let mut iter = iter.into_iter().map(|e| { + e.into_pyobject(py) + .map(BoundObject::into_any) + .map(BoundObject::unbind) + .map_err(Into::into) + }); + let list = crate::types::list::try_new_from_iter(py, &mut iter); + list.map(Bound::into_any) + } +} + +pub(crate) mod private { + pub struct Token; + + pub trait Reference { + type BaseType; + } + + impl Reference for &'_ T { + type BaseType = T; + } } impl<'py, T> IntoPyObject<'py> for Bound<'py, T> { diff --git a/src/conversions/smallvec.rs b/src/conversions/smallvec.rs index 090944d4412..091cbfc48ee 100644 --- a/src/conversions/smallvec.rs +++ b/src/conversions/smallvec.rs @@ -20,12 +20,12 @@ use crate::exceptions::PyTypeError; #[cfg(feature = "experimental-inspect")] use crate::inspect::types::TypeInfo; use crate::types::any::PyAnyMethods; -use crate::types::list::{new_from_iter, try_new_from_iter}; -use crate::types::{PyList, PySequence, PyString}; +use crate::types::list::new_from_iter; +use crate::types::{PySequence, PyString}; use crate::PyErr; use crate::{ - err::DowncastError, ffi, Bound, BoundObject, FromPyObject, IntoPy, PyAny, PyObject, PyResult, - Python, ToPyObject, + err::DowncastError, ffi, Bound, FromPyObject, IntoPy, PyAny, PyObject, PyResult, Python, + ToPyObject, }; use smallvec::{Array, SmallVec}; @@ -62,18 +62,17 @@ where A::Item: IntoPyObject<'py>, PyErr: From<>::Error>, { - type Target = PyList; + type Target = PyAny; type Output = Bound<'py, Self::Target>; type Error = PyErr; + /// Turns [`SmallVec`] into [`PyBytes`], all other `T`s will be turned into a [`PyList`] + /// + /// [`PyBytes`]: crate::types::PyBytes + /// [`PyList`]: crate::types::PyList + #[inline] fn into_pyobject(self, py: Python<'py>) -> Result { - let mut iter = self.into_iter().map(|e| { - e.into_pyobject(py) - .map(BoundObject::into_any) - .map(BoundObject::unbind) - .map_err(Into::into) - }); - try_new_from_iter(py, &mut iter) + ::owned_sequence_into_pyobject(self, py, crate::conversion::private::Token) } } @@ -120,7 +119,7 @@ where #[cfg(test)] mod tests { use super::*; - use crate::types::PyDict; + use crate::types::{PyBytes, PyBytesMethods, PyDict, PyList}; #[test] fn test_smallvec_into_py() { @@ -162,4 +161,19 @@ mod tests { assert!(l.eq(hso).unwrap()); }); } + + #[test] + fn test_smallvec_intopyobject_impl() { + Python::with_gil(|py| { + let bytes: SmallVec<[u8; 8]> = [1, 2, 3, 4, 5].iter().cloned().collect(); + let obj = bytes.clone().into_pyobject(py).unwrap(); + assert!(obj.is_instance_of::()); + let obj = obj.downcast_into::().unwrap(); + assert_eq!(obj.as_bytes(), &*bytes); + + let nums: SmallVec<[u16; 8]> = [1, 2, 3, 4, 5].iter().cloned().collect(); + let obj = nums.into_pyobject(py).unwrap(); + assert!(obj.is_instance_of::()); + }); + } } diff --git a/src/conversions/std/array.rs b/src/conversions/std/array.rs index bae5c6eb2fa..2780868ae04 100644 --- a/src/conversions/std/array.rs +++ b/src/conversions/std/array.rs @@ -1,8 +1,7 @@ use crate::conversion::IntoPyObject; -use crate::ffi_ptr_ext::FfiPtrExt; use crate::instance::Bound; use crate::types::any::PyAnyMethods; -use crate::types::{PyList, PySequence}; +use crate::types::PySequence; use crate::{ err::DowncastError, ffi, FromPyObject, IntoPy, Py, PyAny, PyObject, PyResult, Python, ToPyObject, @@ -41,34 +40,19 @@ where impl<'py, T, const N: usize> IntoPyObject<'py> for [T; N] where T: IntoPyObject<'py>, + PyErr: From, { - type Target = PyList; + type Target = PyAny; type Output = Bound<'py, Self::Target>; - type Error = T::Error; + type Error = PyErr; + /// Turns [`[u8; N]`](std::array) into [`PyBytes`], all other `T`s will be turned into a [`PyList`] + /// + /// [`PyBytes`]: crate::types::PyBytes + /// [`PyList`]: crate::types::PyList + #[inline] fn into_pyobject(self, py: Python<'py>) -> Result { - use crate::BoundObject; - unsafe { - let len = N as ffi::Py_ssize_t; - - let ptr = ffi::PyList_New(len); - - // We create the `Bound` pointer here for two reasons: - // - panics if the ptr is null - // - its Drop cleans up the list if user code errors or panics. - let list = ptr.assume_owned(py).downcast_into_unchecked::(); - - for (i, obj) in (0..len).zip(self) { - let obj = obj.into_pyobject(py)?.into_ptr(); - - #[cfg(not(Py_LIMITED_API))] - ffi::PyList_SET_ITEM(ptr, i, obj); - #[cfg(Py_LIMITED_API)] - ffi::PyList_SetItem(ptr, i, obj); - } - - Ok(list) - } + T::owned_sequence_into_pyobject(self, py, crate::conversion::private::Token) } } @@ -166,7 +150,11 @@ mod tests { sync::atomic::{AtomicUsize, Ordering}, }; - use crate::{ffi, types::any::PyAnyMethods}; + use crate::{ + conversion::IntoPyObject, + ffi, + types::{any::PyAnyMethods, PyBytes, PyBytesMethods}, + }; use crate::{types::PyList, IntoPy, PyResult, Python, ToPyObject}; #[test] @@ -257,6 +245,21 @@ mod tests { }); } + #[test] + fn test_array_intopyobject_impl() { + Python::with_gil(|py| { + let bytes: [u8; 6] = *b"foobar"; + let obj = bytes.into_pyobject(py).unwrap(); + assert!(obj.is_instance_of::()); + let obj = obj.downcast_into::().unwrap(); + assert_eq!(obj.as_bytes(), &bytes); + + let nums: [u16; 4] = [0, 1, 2, 3]; + let obj = nums.into_pyobject(py).unwrap(); + assert!(obj.is_instance_of::()); + }); + } + #[test] fn test_extract_non_iterable_to_array() { Python::with_gil(|py| { diff --git a/src/conversions/std/num.rs b/src/conversions/std/num.rs index 618ca8f142b..61c666a1cfe 100644 --- a/src/conversions/std/num.rs +++ b/src/conversions/std/num.rs @@ -1,9 +1,10 @@ +use crate::conversion::private::Reference; use crate::conversion::IntoPyObject; use crate::ffi_ptr_ext::FfiPtrExt; #[cfg(feature = "experimental-inspect")] use crate::inspect::types::TypeInfo; use crate::types::any::PyAnyMethods; -use crate::types::PyInt; +use crate::types::{PyBytes, PyInt}; use crate::{ exceptions, ffi, Bound, FromPyObject, IntoPy, PyAny, PyErr, PyObject, PyResult, Python, ToPyObject, @@ -161,6 +162,16 @@ macro_rules! int_fits_c_long { } } + impl<'py> IntoPyObject<'py> for &$rust_type { + type Target = PyInt; + type Output = Bound<'py, Self::Target>; + type Error = Infallible; + + fn into_pyobject(self, py: Python<'py>) -> Result { + (*self).into_pyobject(py) + } + } + impl<'py> FromPyObject<'py> for $rust_type { fn extract_bound(obj: &Bound<'_, PyAny>) -> PyResult { let val: c_long = extract_int!(obj, -1, ffi::PyLong_AsLong)?; @@ -176,8 +187,82 @@ macro_rules! int_fits_c_long { }; } +impl ToPyObject for u8 { + fn to_object(&self, py: Python<'_>) -> PyObject { + unsafe { PyObject::from_owned_ptr(py, ffi::PyLong_FromLong(*self as c_long)) } + } +} +impl IntoPy for u8 { + fn into_py(self, py: Python<'_>) -> PyObject { + unsafe { PyObject::from_owned_ptr(py, ffi::PyLong_FromLong(self as c_long)) } + } + #[cfg(feature = "experimental-inspect")] + fn type_output() -> TypeInfo { + TypeInfo::builtin("int") + } +} +impl<'py> IntoPyObject<'py> for u8 { + type Target = PyInt; + type Output = Bound<'py, Self::Target>; + type Error = Infallible; + + fn into_pyobject(self, py: Python<'py>) -> Result { + unsafe { + Ok(ffi::PyLong_FromLong(self as c_long) + .assume_owned(py) + .downcast_into_unchecked()) + } + } + + #[inline] + fn owned_sequence_into_pyobject( + iter: I, + py: Python<'py>, + _: crate::conversion::private::Token, + ) -> Result, PyErr> + where + I: AsRef<[u8]>, + { + Ok(PyBytes::new(py, iter.as_ref()).into_any()) + } +} + +impl<'py> IntoPyObject<'py> for &'_ u8 { + type Target = PyInt; + type Output = Bound<'py, Self::Target>; + type Error = Infallible; + + fn into_pyobject(self, py: Python<'py>) -> Result { + u8::into_pyobject(*self, py) + } + + #[inline] + fn borrowed_sequence_into_pyobject( + iter: I, + py: Python<'py>, + _: crate::conversion::private::Token, + ) -> Result, PyErr> + where + // I: AsRef<[u8]>, but the compiler needs it expressed via the trait for some reason + I: AsRef<[::BaseType]>, + { + Ok(PyBytes::new(py, iter.as_ref()).into_any()) + } +} + +impl<'py> FromPyObject<'py> for u8 { + fn extract_bound(obj: &Bound<'_, PyAny>) -> PyResult { + let val: c_long = extract_int!(obj, -1, ffi::PyLong_AsLong)?; + u8::try_from(val).map_err(|e| exceptions::PyOverflowError::new_err(e.to_string())) + } + + #[cfg(feature = "experimental-inspect")] + fn type_input() -> TypeInfo { + Self::type_output() + } +} + int_fits_c_long!(i8); -int_fits_c_long!(u8); int_fits_c_long!(i16); int_fits_c_long!(u16); int_fits_c_long!(i32); diff --git a/src/conversions/std/slice.rs b/src/conversions/std/slice.rs index 4c25305f777..9b9eb3dd3ec 100644 --- a/src/conversions/std/slice.rs +++ b/src/conversions/std/slice.rs @@ -1,11 +1,11 @@ -use std::{borrow::Cow, convert::Infallible}; +use std::borrow::Cow; #[cfg(feature = "experimental-inspect")] use crate::inspect::types::TypeInfo; use crate::{ conversion::IntoPyObject, types::{PyByteArray, PyByteArrayMethods, PyBytes}, - Bound, IntoPy, Py, PyAny, PyObject, PyResult, Python, ToPyObject, + Bound, IntoPy, Py, PyAny, PyErr, PyObject, PyResult, Python, ToPyObject, }; impl<'a> IntoPy for &'a [u8] { @@ -19,13 +19,22 @@ impl<'a> IntoPy for &'a [u8] { } } -impl<'py> IntoPyObject<'py> for &[u8] { - type Target = PyBytes; +impl<'a, 'py, T> IntoPyObject<'py> for &'a [T] +where + &'a T: IntoPyObject<'py>, + PyErr: From<<&'a T as IntoPyObject<'py>>::Error>, +{ + type Target = PyAny; type Output = Bound<'py, Self::Target>; - type Error = Infallible; + type Error = PyErr; + /// Turns [`&[u8]`](std::slice) into [`PyBytes`], all other `T`s will be turned into a [`PyList`] + /// + /// [`PyBytes`]: crate::types::PyBytes + /// [`PyList`]: crate::types::PyList + #[inline] fn into_pyobject(self, py: Python<'py>) -> Result { - Ok(PyBytes::new(py, self)) + <&T>::borrowed_sequence_into_pyobject(self, py, crate::conversion::private::Token) } } @@ -73,13 +82,23 @@ impl IntoPy> for Cow<'_, [u8]> { } } -impl<'py> IntoPyObject<'py> for Cow<'_, [u8]> { - type Target = PyBytes; +impl<'py, T> IntoPyObject<'py> for Cow<'_, [T]> +where + T: Clone, + for<'a> &'a T: IntoPyObject<'py>, + for<'a> PyErr: From<<&'a T as IntoPyObject<'py>>::Error>, +{ + type Target = PyAny; type Output = Bound<'py, Self::Target>; - type Error = Infallible; + type Error = PyErr; + /// Turns `Cow<[u8]>` into [`PyBytes`], all other `T`s will be turned into a [`PyList`] + /// + /// [`PyBytes`]: crate::types::PyBytes + /// [`PyList`]: crate::types::PyList + #[inline] fn into_pyobject(self, py: Python<'py>) -> Result { - Ok(PyBytes::new(py, &self)) + <&T>::borrowed_sequence_into_pyobject(self.as_ref(), py, crate::conversion::private::Token) } } @@ -88,8 +107,9 @@ mod tests { use std::borrow::Cow; use crate::{ + conversion::IntoPyObject, ffi, - types::{any::PyAnyMethods, PyBytes}, + types::{any::PyAnyMethods, PyBytes, PyBytesMethods, PyList}, Python, ToPyObject, }; @@ -127,4 +147,44 @@ mod tests { assert!(cow.bind(py).is_instance_of::()); }); } + + #[test] + fn test_slice_intopyobject_impl() { + Python::with_gil(|py| { + let bytes: &[u8] = b"foobar"; + let obj = bytes.into_pyobject(py).unwrap(); + assert!(obj.is_instance_of::()); + let obj = obj.downcast_into::().unwrap(); + assert_eq!(obj.as_bytes(), bytes); + + let nums: &[u16] = &[0, 1, 2, 3]; + let obj = nums.into_pyobject(py).unwrap(); + assert!(obj.is_instance_of::()); + }); + } + + #[test] + fn test_cow_intopyobject_impl() { + Python::with_gil(|py| { + let borrowed_bytes = Cow::<[u8]>::Borrowed(b"foobar"); + let obj = borrowed_bytes.clone().into_pyobject(py).unwrap(); + assert!(obj.is_instance_of::()); + let obj = obj.downcast_into::().unwrap(); + assert_eq!(obj.as_bytes(), &*borrowed_bytes); + + let owned_bytes = Cow::<[u8]>::Owned(b"foobar".to_vec()); + let obj = owned_bytes.clone().into_pyobject(py).unwrap(); + assert!(obj.is_instance_of::()); + let obj = obj.downcast_into::().unwrap(); + assert_eq!(obj.as_bytes(), &*owned_bytes); + + let borrowed_nums = Cow::<[u16]>::Borrowed(&[0, 1, 2, 3]); + let obj = borrowed_nums.into_pyobject(py).unwrap(); + assert!(obj.is_instance_of::()); + + let owned_nums = Cow::<[u16]>::Owned(vec![0, 1, 2, 3]); + let obj = owned_nums.into_pyobject(py).unwrap(); + assert!(obj.is_instance_of::()); + }); + } } diff --git a/src/conversions/std/vec.rs b/src/conversions/std/vec.rs index 9a759baeecf..40ad7eea8a0 100644 --- a/src/conversions/std/vec.rs +++ b/src/conversions/std/vec.rs @@ -1,9 +1,8 @@ use crate::conversion::IntoPyObject; #[cfg(feature = "experimental-inspect")] use crate::inspect::types::TypeInfo; -use crate::types::list::{new_from_iter, try_new_from_iter}; -use crate::types::PyList; -use crate::{Bound, BoundObject, IntoPy, PyErr, PyObject, Python, ToPyObject}; +use crate::types::list::new_from_iter; +use crate::{Bound, IntoPy, PyAny, PyErr, PyObject, Python, ToPyObject}; impl ToPyObject for [T] where @@ -46,18 +45,71 @@ where T: IntoPyObject<'py>, PyErr: From, { - type Target = PyList; + type Target = PyAny; type Output = Bound<'py, Self::Target>; type Error = PyErr; + /// Turns [`Vec`] into [`PyBytes`], all other `T`s will be turned into a [`PyList`] + /// + /// [`PyBytes`]: crate::types::PyBytes + /// [`PyList`]: crate::types::PyList + #[inline] fn into_pyobject(self, py: Python<'py>) -> Result { - let mut iter = self.into_iter().map(|e| { - e.into_pyobject(py) - .map(BoundObject::into_any) - .map(BoundObject::unbind) - .map_err(Into::into) + T::owned_sequence_into_pyobject(self, py, crate::conversion::private::Token) + } +} + +impl<'a, 'py, T> IntoPyObject<'py> for &'a Vec +where + &'a T: IntoPyObject<'py>, + PyErr: From<<&'a T as IntoPyObject<'py>>::Error>, +{ + type Target = PyAny; + type Output = Bound<'py, Self::Target>; + type Error = PyErr; + + #[inline] + fn into_pyobject(self, py: Python<'py>) -> Result { + // NB: we could actually not cast to `PyAny`, which would be nice for + // `&Vec`, but that'd be inconsistent with the `IntoPyObject` impl + // above which always returns a `PyAny` for `Vec`. + self.as_slice().into_pyobject(py).map(Bound::into_any) + } +} + +#[cfg(test)] +mod tests { + use crate::conversion::IntoPyObject; + use crate::types::{PyAnyMethods, PyBytes, PyBytesMethods, PyList}; + use crate::Python; + + #[test] + fn test_vec_intopyobject_impl() { + Python::with_gil(|py| { + let bytes: Vec = b"foobar".to_vec(); + let obj = bytes.clone().into_pyobject(py).unwrap(); + assert!(obj.is_instance_of::()); + let obj = obj.downcast_into::().unwrap(); + assert_eq!(obj.as_bytes(), &bytes); + + let nums: Vec = vec![0, 1, 2, 3]; + let obj = nums.into_pyobject(py).unwrap(); + assert!(obj.is_instance_of::()); }); + } - try_new_from_iter(py, &mut iter) + #[test] + fn test_vec_reference_intopyobject_impl() { + Python::with_gil(|py| { + let bytes: Vec = b"foobar".to_vec(); + let obj = (&bytes).into_pyobject(py).unwrap(); + assert!(obj.is_instance_of::()); + let obj = obj.downcast_into::().unwrap(); + assert_eq!(obj.as_bytes(), &bytes); + + let nums: Vec = vec![0, 1, 2, 3]; + let obj = (&nums).into_pyobject(py).unwrap(); + assert!(obj.is_instance_of::()); + }); } } diff --git a/src/types/bytes.rs b/src/types/bytes.rs index 876cf156ab8..397e2eb3848 100644 --- a/src/types/bytes.rs +++ b/src/types/bytes.rs @@ -95,6 +95,7 @@ impl PyBytes { /// }) /// # } /// ``` + #[inline] pub fn new_with(py: Python<'_>, len: usize, init: F) -> PyResult> where F: FnOnce(&mut [u8]) -> PyResult<()>, diff --git a/tests/ui/missing_intopy.stderr b/tests/ui/missing_intopy.stderr index e0818ec42ef..58ddbff0f22 100644 --- a/tests/ui/missing_intopy.stderr +++ b/tests/ui/missing_intopy.stderr @@ -11,11 +11,11 @@ error[E0277]: `Blah` cannot be converted to a Python object &'a Py &'a PyRef<'py, T> &'a PyRefMut<'py, T> + &'a Vec + &'a [T] &'a pyo3::Bound<'py, T> &OsStr &OsString - &Path - &PathBuf and $N others note: required by a bound in `UnknownReturnType::::wrap` --> src/impl_/wrap.rs From f4748103959a265f38bed80891ecea825d164564 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Fri, 16 Aug 2024 07:05:41 -0600 Subject: [PATCH 214/495] Initial free threaded bindings (#4421) * add support in pyo3-build-config for free-threaded python * update object.h bindings for free-threaded build * Add PyMutex bindings * fix po3-ffi-check with free-threaded build * error when building with limited api and Py_GIL_DISABLED * Add CI job for free-threaded build * fix issues building on older pythons * ci config fixup * fix clippy on gil-enabled 3.13 build * Apply suggestions from code review Co-authored-by: David Hewitt * make PyMutex and PyObject refcounting fields atomics * add new field on PyConfig in 3.13 debug ABI * warn and disable abi3 on gil-disabled build * fix conditional compilation for PyMutex usage * temporarily skip test that deadlocks * remove Py_GIL_DISABLED from py_sys_config cfg options * only expose PyMutex in 3.13 * make PyObject_HEAD_INIT a function * intialize ob_ref_local to _Py_IMMORTAL_REFCNT_LOCAL in HEAD_INIT * Fix clippy lint about static with interior mutability * add TODO comments about INCREF and DECREF in free-threaded build * make the _bits field of PyMutex pub(crate) * refactor so HEAD_INIT remains a constant * ignore clippy lint about interior mutability * revert unnecessary changes to pyo3-build-config * add changelog entries * use derive(Debug) for PyMutex * Add PhantomPinned field to PyMutex bindings * Update pyo3-build-config/src/impl_.rs Co-authored-by: David Hewitt * Update pyo3-ffi/src/object.rs Co-authored-by: David Hewitt * fix build config again --------- Co-authored-by: David Hewitt --- .github/workflows/ci.yml | 30 +++++++++++ newsfragments/4421.added.md | 1 + newsfragments/4421.fixed.md | 1 + pyo3-build-config/src/impl_.rs | 47 +++++++++++++++- pyo3-build-config/src/lib.rs | 1 + pyo3-ffi/build.rs | 5 ++ pyo3-ffi/src/cpython/initconfig.rs | 4 ++ pyo3-ffi/src/cpython/lock.rs | 14 +++++ pyo3-ffi/src/cpython/mod.rs | 4 ++ pyo3-ffi/src/cpython/weakrefobject.rs | 2 + pyo3-ffi/src/moduleobject.rs | 1 + pyo3-ffi/src/object.rs | 78 ++++++++++++++++++++++++--- src/impl_/pymodule.rs | 1 + tests/test_dict_iter.rs | 1 + 14 files changed, 182 insertions(+), 8 deletions(-) create mode 100644 newsfragments/4421.added.md create mode 100644 newsfragments/4421.fixed.md create mode 100644 pyo3-ffi/src/cpython/lock.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c8f4b5dcc67..2f2e093937d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -489,6 +489,35 @@ jobs: echo PYO3_CONFIG_FILE=$PYO3_CONFIG_FILE >> $GITHUB_ENV - run: python3 -m nox -s test + test-free-threaded: + if: ${{ contains(github.event.pull_request.labels.*.name, 'CI-build-full') || github.event_name != 'pull_request' }} + needs: [fmt] + runs-on: ubuntu-latest + env: + UNSAFE_PYO3_BUILD_FREE_THREADED: 1 + steps: + - uses: actions/checkout@v4 + - uses: Swatinem/rust-cache@v2 + with: + save-if: ${{ github.event_name != 'merge_group' }} + - uses: dtolnay/rust-toolchain@stable + with: + components: rust-src + # TODO: replace with setup-python when there is support + - uses: deadsnakes/action@v3.1.0 + with: + python-version: '3.13-dev' + nogil: true + - run: python3 -m sysconfig + - run: python3 -m pip install --upgrade pip && pip install nox + - run: nox -s ffi-check + - name: Run default nox sessions that should pass + run: nox -s clippy docs rustfmt ruff + - name: Run PyO3 tests with free-threaded Python (can fail) + # TODO fix the test crashes so we can unset this + continue-on-error: true + run: nox -s test + test-version-limits: needs: [fmt] if: ${{ contains(github.event.pull_request.labels.*.name, 'CI-build-full') || github.event_name != 'pull_request' }} @@ -627,6 +656,7 @@ jobs: - coverage - emscripten - test-debug + - test-free-threaded - test-version-limits - check-feature-powerset - test-cross-compilation diff --git a/newsfragments/4421.added.md b/newsfragments/4421.added.md new file mode 100644 index 00000000000..b0a85bea3ca --- /dev/null +++ b/newsfragments/4421.added.md @@ -0,0 +1 @@ +* Added bindings for PyMutex. diff --git a/newsfragments/4421.fixed.md b/newsfragments/4421.fixed.md new file mode 100644 index 00000000000..075b1fa7b5a --- /dev/null +++ b/newsfragments/4421.fixed.md @@ -0,0 +1 @@ +* Updated FFI bindings for free-threaded CPython 3.13 ABI diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index d38d41ed552..c8f68864727 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -177,12 +177,18 @@ impl InterpreterConfig { PythonImplementation::GraalPy => out.push("cargo:rustc-cfg=GraalPy".to_owned()), } - if self.abi3 { + // If Py_GIL_DISABLED is set, do not build with limited API support + if self.abi3 && !self.build_flags.0.contains(&BuildFlag::Py_GIL_DISABLED) { out.push("cargo:rustc-cfg=Py_LIMITED_API".to_owned()); } for flag in &self.build_flags.0 { - out.push(format!("cargo:rustc-cfg=py_sys_config=\"{}\"", flag)); + match flag { + BuildFlag::Py_GIL_DISABLED => { + out.push("cargo:rustc-cfg=Py_GIL_DISABLED".to_owned()) + } + flag => out.push(format!("cargo:rustc-cfg=py_sys_config=\"{}\"", flag)), + } } out @@ -2733,6 +2739,43 @@ mod tests { ); } + #[test] + fn test_build_script_outputs_gil_disabled() { + let mut build_flags = BuildFlags::default(); + build_flags.0.insert(BuildFlag::Py_GIL_DISABLED); + let interpreter_config = InterpreterConfig { + implementation: PythonImplementation::CPython, + version: PythonVersion { + major: 3, + minor: 13, + }, + 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_3_8".to_owned(), + "cargo:rustc-cfg=Py_3_9".to_owned(), + "cargo:rustc-cfg=Py_3_10".to_owned(), + "cargo:rustc-cfg=Py_3_11".to_owned(), + "cargo:rustc-cfg=Py_3_12".to_owned(), + "cargo:rustc-cfg=Py_3_13".to_owned(), + "cargo:rustc-cfg=Py_GIL_DISABLED".to_owned(), + ] + ); + } + #[test] fn test_build_script_outputs_debug() { let mut build_flags = BuildFlags::default(); diff --git a/pyo3-build-config/src/lib.rs b/pyo3-build-config/src/lib.rs index 0bc2274e0e5..033e7b46540 100644 --- a/pyo3-build-config/src/lib.rs +++ b/pyo3-build-config/src/lib.rs @@ -166,6 +166,7 @@ pub fn print_expected_cfgs() { } println!("cargo:rustc-check-cfg=cfg(Py_LIMITED_API)"); + println!("cargo:rustc-check-cfg=cfg(Py_GIL_DISABLED)"); println!("cargo:rustc-check-cfg=cfg(PyPy)"); println!("cargo:rustc-check-cfg=cfg(GraalPy)"); println!("cargo:rustc-check-cfg=cfg(py_sys_config, values(\"Py_DEBUG\", \"Py_REF_DEBUG\", \"Py_TRACE_REFS\", \"COUNT_ALLOCS\"))"); diff --git a/pyo3-ffi/build.rs b/pyo3-ffi/build.rs index 83408b31222..8b3a98bf9f7 100644 --- a/pyo3-ffi/build.rs +++ b/pyo3-ffi/build.rs @@ -135,6 +135,11 @@ fn ensure_gil_enabled(interpreter_config: &InterpreterConfig) -> Result<()> { = help: set UNSAFE_PYO3_BUILD_FREE_THREADED=1 to suppress this check and build anyway for free-threaded Python", std::env::var("CARGO_PKG_VERSION").unwrap() ); + if interpreter_config.abi3 { + warn!( + "The free-threaded build of CPython does not yet support abi3 so the build artifacts will be version-specific." + ) + } Ok(()) } diff --git a/pyo3-ffi/src/cpython/initconfig.rs b/pyo3-ffi/src/cpython/initconfig.rs index 32931415888..321d200e141 100644 --- a/pyo3-ffi/src/cpython/initconfig.rs +++ b/pyo3-ffi/src/cpython/initconfig.rs @@ -143,6 +143,8 @@ pub struct PyConfig { pub int_max_str_digits: c_int, #[cfg(Py_3_13)] pub cpu_count: c_int, + #[cfg(Py_GIL_DISABLED)] + pub enable_gil: c_int, pub pathconfig_warnings: c_int, #[cfg(Py_3_10)] pub program_name: *mut wchar_t, @@ -177,6 +179,8 @@ pub struct PyConfig { pub _is_python_build: c_int, #[cfg(all(Py_3_9, not(Py_3_10)))] pub _orig_argv: PyWideStringList, + #[cfg(all(Py_3_13, py_sys_config = "Py_DEBUG"))] + pub run_presite: *mut wchar_t, } extern "C" { diff --git a/pyo3-ffi/src/cpython/lock.rs b/pyo3-ffi/src/cpython/lock.rs new file mode 100644 index 00000000000..05778dfe573 --- /dev/null +++ b/pyo3-ffi/src/cpython/lock.rs @@ -0,0 +1,14 @@ +use std::marker::PhantomPinned; +use std::sync::atomic::AtomicU8; + +#[repr(transparent)] +#[derive(Debug)] +pub struct PyMutex { + pub(crate) _bits: AtomicU8, + pub(crate) _pin: PhantomPinned, +} + +extern "C" { + pub fn PyMutex_Lock(m: *mut PyMutex); + pub fn PyMutex_UnLock(m: *mut PyMutex); +} diff --git a/pyo3-ffi/src/cpython/mod.rs b/pyo3-ffi/src/cpython/mod.rs index 1710dbc4122..8f850104def 100644 --- a/pyo3-ffi/src/cpython/mod.rs +++ b/pyo3-ffi/src/cpython/mod.rs @@ -18,6 +18,8 @@ pub(crate) mod import; pub(crate) mod initconfig; // skipped interpreteridobject.h pub(crate) mod listobject; +#[cfg(Py_3_13)] +pub(crate) mod lock; pub(crate) mod longobject; #[cfg(all(Py_3_9, not(PyPy)))] pub(crate) mod methodobject; @@ -54,6 +56,8 @@ pub use self::import::*; #[cfg(all(Py_3_8, not(PyPy)))] pub use self::initconfig::*; pub use self::listobject::*; +#[cfg(Py_3_13)] +pub use self::lock::*; pub use self::longobject::*; #[cfg(all(Py_3_9, not(PyPy)))] pub use self::methodobject::*; diff --git a/pyo3-ffi/src/cpython/weakrefobject.rs b/pyo3-ffi/src/cpython/weakrefobject.rs index 3a232c7ed38..88bb501bcc5 100644 --- a/pyo3-ffi/src/cpython/weakrefobject.rs +++ b/pyo3-ffi/src/cpython/weakrefobject.rs @@ -8,6 +8,8 @@ pub struct _PyWeakReference { pub wr_next: *mut crate::PyWeakReference, #[cfg(Py_3_11)] pub vectorcall: Option, + #[cfg(all(Py_3_13, Py_GIL_DISABLED))] + pub weakrefs_lock: *mut crate::PyMutex, } // skipped _PyWeakref_GetWeakrefCount diff --git a/pyo3-ffi/src/moduleobject.rs b/pyo3-ffi/src/moduleobject.rs index 04b0f4ac25f..ff6458f4b15 100644 --- a/pyo3-ffi/src/moduleobject.rs +++ b/pyo3-ffi/src/moduleobject.rs @@ -60,6 +60,7 @@ pub struct PyModuleDef_Base { pub m_copy: *mut PyObject, } +#[allow(clippy::declare_interior_mutable_const)] pub const PyModuleDef_HEAD_INIT: PyModuleDef_Base = PyModuleDef_Base { ob_base: PyObject_HEAD_INIT, m_init: None, diff --git a/pyo3-ffi/src/object.rs b/pyo3-ffi/src/object.rs index 9181cb17dd2..27436c208a9 100644 --- a/pyo3-ffi/src/object.rs +++ b/pyo3-ffi/src/object.rs @@ -1,7 +1,13 @@ use crate::pyport::{Py_hash_t, Py_ssize_t}; +#[cfg(Py_GIL_DISABLED)] +use crate::PyMutex; +#[cfg(Py_GIL_DISABLED)] +use std::marker::PhantomPinned; use std::mem; use std::os::raw::{c_char, c_int, c_uint, c_ulong, c_void}; use std::ptr; +#[cfg(Py_GIL_DISABLED)] +use std::sync::atomic::{AtomicIsize, AtomicU32, AtomicU8, Ordering::Relaxed}; #[cfg(Py_LIMITED_API)] opaque_struct!(PyTypeObject); @@ -22,12 +28,33 @@ pub const _Py_IMMORTAL_REFCNT: Py_ssize_t = { } }; +#[cfg(Py_GIL_DISABLED)] +pub const _Py_IMMORTAL_REFCNT_LOCAL: u32 = u32::MAX; +#[cfg(Py_GIL_DISABLED)] +pub const _Py_REF_SHARED_SHIFT: isize = 2; + +#[allow(clippy::declare_interior_mutable_const)] pub const PyObject_HEAD_INIT: PyObject = PyObject { #[cfg(py_sys_config = "Py_TRACE_REFS")] _ob_next: std::ptr::null_mut(), #[cfg(py_sys_config = "Py_TRACE_REFS")] _ob_prev: std::ptr::null_mut(), - #[cfg(Py_3_12)] + #[cfg(Py_GIL_DISABLED)] + ob_tid: 0, + #[cfg(Py_GIL_DISABLED)] + _padding: 0, + #[cfg(Py_GIL_DISABLED)] + ob_mutex: PyMutex { + _bits: AtomicU8::new(0), + _pin: PhantomPinned, + }, + #[cfg(Py_GIL_DISABLED)] + ob_gc_bits: 0, + #[cfg(Py_GIL_DISABLED)] + ob_ref_local: AtomicU32::new(_Py_IMMORTAL_REFCNT_LOCAL), + #[cfg(Py_GIL_DISABLED)] + ob_ref_shared: AtomicIsize::new(0), + #[cfg(all(not(Py_GIL_DISABLED), Py_3_12))] ob_refcnt: PyObjectObRefcnt { ob_refcnt: 1 }, #[cfg(not(Py_3_12))] ob_refcnt: 1, @@ -67,6 +94,19 @@ pub struct PyObject { pub _ob_next: *mut PyObject, #[cfg(py_sys_config = "Py_TRACE_REFS")] pub _ob_prev: *mut PyObject, + #[cfg(Py_GIL_DISABLED)] + pub ob_tid: libc::uintptr_t, + #[cfg(Py_GIL_DISABLED)] + pub _padding: u16, + #[cfg(Py_GIL_DISABLED)] + pub ob_mutex: PyMutex, // per-object lock + #[cfg(Py_GIL_DISABLED)] + pub ob_gc_bits: u8, // gc-related state + #[cfg(Py_GIL_DISABLED)] + pub ob_ref_local: AtomicU32, // local reference count + #[cfg(Py_GIL_DISABLED)] + pub ob_ref_shared: AtomicIsize, // shared reference count + #[cfg(not(Py_GIL_DISABLED))] pub ob_refcnt: PyObjectObRefcnt, #[cfg(PyPy)] pub ob_pypy_link: Py_ssize_t, @@ -91,6 +131,18 @@ pub unsafe fn Py_Is(x: *mut PyObject, y: *mut PyObject) -> c_int { } #[inline] +#[cfg(Py_GIL_DISABLED)] +pub unsafe fn Py_REFCNT(ob: *mut PyObject) -> Py_ssize_t { + let local = (*ob).ob_ref_local.load(Relaxed); + if local == _Py_IMMORTAL_REFCNT_LOCAL { + return _Py_IMMORTAL_REFCNT; + } + let shared = (*ob).ob_ref_shared.load(Relaxed); + local as Py_ssize_t + Py_ssize_t::from(shared >> _Py_REF_SHARED_SHIFT) +} + +#[inline] +#[cfg(not(Py_GIL_DISABLED))] #[cfg(Py_3_12)] pub unsafe fn Py_REFCNT(ob: *mut PyObject) -> Py_ssize_t { (*ob).ob_refcnt.ob_refcnt @@ -134,7 +186,7 @@ pub unsafe fn Py_IS_TYPE(ob: *mut PyObject, tp: *mut PyTypeObject) -> c_int { } #[inline(always)] -#[cfg(all(Py_3_12, target_pointer_width = "64"))] +#[cfg(all(not(Py_GIL_DISABLED), Py_3_12, target_pointer_width = "64"))] pub unsafe fn _Py_IsImmortal(op: *mut PyObject) -> c_int { (((*op).ob_refcnt.ob_refcnt as crate::PY_INT32_T) < 0) as c_int } @@ -507,8 +559,14 @@ extern "C" { #[inline(always)] pub unsafe fn Py_INCREF(op: *mut PyObject) { - // On limited API or with refcount debugging, let the interpreter do refcounting - #[cfg(any(Py_LIMITED_API, py_sys_config = "Py_REF_DEBUG", GraalPy))] + // On limited API, the free-threaded build, or with refcount debugging, let the interpreter do refcounting + // TODO: reimplement the logic in the header in the free-threaded build, for a little bit of performance. + #[cfg(any( + Py_GIL_DISABLED, + Py_LIMITED_API, + py_sys_config = "Py_REF_DEBUG", + GraalPy + ))] { // _Py_IncRef was added to the ABI in 3.10; skips null checks #[cfg(all(Py_3_10, not(PyPy)))] @@ -523,7 +581,12 @@ pub unsafe fn Py_INCREF(op: *mut PyObject) { } // version-specific builds are allowed to directly manipulate the reference count - #[cfg(not(any(any(Py_LIMITED_API, py_sys_config = "Py_REF_DEBUG", GraalPy))))] + #[cfg(not(any( + Py_GIL_DISABLED, + Py_LIMITED_API, + py_sys_config = "Py_REF_DEBUG", + GraalPy + )))] { #[cfg(all(Py_3_12, target_pointer_width = "64"))] { @@ -559,9 +622,11 @@ pub unsafe fn Py_INCREF(op: *mut PyObject) { track_caller )] pub unsafe fn Py_DECREF(op: *mut PyObject) { - // On limited API or with refcount debugging, let the interpreter do refcounting + // On limited API, the free-threaded build, or with refcount debugging, let the interpreter do refcounting // On 3.12+ we implement refcount debugging to get better assertion locations on negative refcounts + // TODO: reimplement the logic in the header in the free-threaded build, for a little bit of performance. #[cfg(any( + Py_GIL_DISABLED, Py_LIMITED_API, all(py_sys_config = "Py_REF_DEBUG", not(Py_3_12)), GraalPy @@ -580,6 +645,7 @@ pub unsafe fn Py_DECREF(op: *mut PyObject) { } #[cfg(not(any( + Py_GIL_DISABLED, Py_LIMITED_API, all(py_sys_config = "Py_REF_DEBUG", not(Py_3_12)), GraalPy diff --git a/src/impl_/pymodule.rs b/src/impl_/pymodule.rs index d199bf95aac..8ce169fffa2 100644 --- a/src/impl_/pymodule.rs +++ b/src/impl_/pymodule.rs @@ -55,6 +55,7 @@ impl ModuleDef { doc: &'static CStr, initializer: ModuleInitializer, ) -> Self { + #[allow(clippy::declare_interior_mutable_const)] const INIT: ffi::PyModuleDef = ffi::PyModuleDef { m_base: ffi::PyModuleDef_HEAD_INIT, m_name: std::ptr::null(), diff --git a/tests/test_dict_iter.rs b/tests/test_dict_iter.rs index dc32eb61fd7..5b3573d10ad 100644 --- a/tests/test_dict_iter.rs +++ b/tests/test_dict_iter.rs @@ -3,6 +3,7 @@ use pyo3::types::IntoPyDict; #[test] #[cfg_attr(target_arch = "wasm32", ignore)] // Not sure why this fails. +#[cfg_attr(Py_GIL_DISABLED, ignore)] // test deadlocks in GIL-disabled build, TODO: fix deadlock fn iter_dict_nosegv() { Python::with_gil(|py| { const LEN: usize = 10_000_000; From 6087a1558c085bd1cc973bc05d53a18d8c95a6cf Mon Sep 17 00:00:00 2001 From: Dmitry Mottl Date: Fri, 16 Aug 2024 22:26:27 +0800 Subject: [PATCH 215/495] Update rust-from-python.md (#4444) --- guide/src/rust-from-python.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/guide/src/rust-from-python.md b/guide/src/rust-from-python.md index 470d5719098..cbf846981ed 100644 --- a/guide/src/rust-from-python.md +++ b/guide/src/rust-from-python.md @@ -4,7 +4,7 @@ This chapter of the guide is dedicated to explaining how to wrap Rust code into PyO3 uses Rust's "procedural macros" to provide a powerful yet simple API to denote what Rust code should map into Python objects. -The three types of Python objects which PyO3 can produce are: +PyO3 can create three types of Python objects: - Python modules, via the `#[pymodule]` macro - Python functions, via the `#[pyfunction]` macro From 52dc139142df0c48fab6f77f9edd96204bfa02c1 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Fri, 16 Aug 2024 15:26:39 +0100 Subject: [PATCH 216/495] use "fastcall" convention on abi3, if >3.10 (#4415) * use "fastcall" convention on abi3, if >3.10 * improve coverage * coverage, clippy * use apis available on all versions --- examples/string-sum/src/lib.rs | 2 +- newsfragments/4415.added.md | 1 + newsfragments/4415.changed.md | 1 + pyo3-ffi/README.md | 2 +- pyo3-ffi/src/lib.rs | 2 +- pyo3-ffi/src/methodobject.rs | 30 ++++++++++++---- pyo3-macros-backend/src/method.rs | 15 ++++---- pyo3-macros-backend/src/pyclass.rs | 11 +++--- pyo3-macros-backend/src/pyversions.rs | 5 ++- pyo3-macros-backend/src/utils.rs | 4 --- src/impl_/extract_argument.rs | 2 +- src/impl_/pymethods.rs | 49 +++++++++++++++++++++++---- 12 files changed, 89 insertions(+), 35 deletions(-) create mode 100644 newsfragments/4415.added.md create mode 100644 newsfragments/4415.changed.md diff --git a/examples/string-sum/src/lib.rs b/examples/string-sum/src/lib.rs index ce71ab38f87..23cdae7b5af 100644 --- a/examples/string-sum/src/lib.rs +++ b/examples/string-sum/src/lib.rs @@ -19,7 +19,7 @@ static mut METHODS: &[PyMethodDef] = &[ PyMethodDef { ml_name: c_str!("sum_as_string").as_ptr(), ml_meth: PyMethodDefPointer { - _PyCFunctionFast: sum_as_string, + PyCFunctionFast: sum_as_string, }, ml_flags: METH_FASTCALL, ml_doc: c_str!("returns the sum of two integers as a string").as_ptr(), diff --git a/newsfragments/4415.added.md b/newsfragments/4415.added.md new file mode 100644 index 00000000000..51796b3c816 --- /dev/null +++ b/newsfragments/4415.added.md @@ -0,0 +1 @@ +Add FFI definitions `PyCFunctionFast` and `PyCFunctionFastWithKeywords` diff --git a/newsfragments/4415.changed.md b/newsfragments/4415.changed.md new file mode 100644 index 00000000000..47c5e8bfb6d --- /dev/null +++ b/newsfragments/4415.changed.md @@ -0,0 +1 @@ +Use "fastcall" Python calling convention for `#[pyfunction]`s when compiling on abi3 for Python 3.10 and up. diff --git a/pyo3-ffi/README.md b/pyo3-ffi/README.md index 200c78cec14..75a34b6e72a 100644 --- a/pyo3-ffi/README.md +++ b/pyo3-ffi/README.md @@ -65,7 +65,7 @@ static mut METHODS: [PyMethodDef; 2] = [ PyMethodDef { ml_name: c_str!("sum_as_string").as_ptr(), ml_meth: PyMethodDefPointer { - _PyCFunctionFast: sum_as_string, + PyCFunctionFast: sum_as_string, }, ml_flags: METH_FASTCALL, ml_doc: c_str!("returns the sum of two integers as a string").as_ptr(), diff --git a/pyo3-ffi/src/lib.rs b/pyo3-ffi/src/lib.rs index 55c7f31404f..4b5b3e390ad 100644 --- a/pyo3-ffi/src/lib.rs +++ b/pyo3-ffi/src/lib.rs @@ -103,7 +103,7 @@ //! PyMethodDef { //! ml_name: c_str!("sum_as_string").as_ptr(), //! ml_meth: PyMethodDefPointer { -//! _PyCFunctionFast: sum_as_string, +//! PyCFunctionFast: sum_as_string, //! }, //! ml_flags: METH_FASTCALL, //! ml_doc: c_str!("returns the sum of two integers as a string").as_ptr(), diff --git a/pyo3-ffi/src/methodobject.rs b/pyo3-ffi/src/methodobject.rs index 74f7840ef58..8af41eda817 100644 --- a/pyo3-ffi/src/methodobject.rs +++ b/pyo3-ffi/src/methodobject.rs @@ -43,26 +43,34 @@ pub type PyCFunction = unsafe extern "C" fn(slf: *mut PyObject, args: *mut PyObject) -> *mut PyObject; #[cfg(any(Py_3_10, not(Py_LIMITED_API)))] -pub type _PyCFunctionFast = unsafe extern "C" fn( +pub type PyCFunctionFast = unsafe extern "C" fn( slf: *mut PyObject, args: *mut *mut PyObject, nargs: crate::pyport::Py_ssize_t, ) -> *mut PyObject; +#[cfg(any(Py_3_10, not(Py_LIMITED_API)))] +#[deprecated(note = "renamed to `PyCFunctionFast`")] +pub type _PyCFunctionFast = PyCFunctionFast; + pub type PyCFunctionWithKeywords = unsafe extern "C" fn( slf: *mut PyObject, args: *mut PyObject, kwds: *mut PyObject, ) -> *mut PyObject; -#[cfg(not(Py_LIMITED_API))] -pub type _PyCFunctionFastWithKeywords = unsafe extern "C" fn( +#[cfg(any(Py_3_10, not(Py_LIMITED_API)))] +pub type PyCFunctionFastWithKeywords = unsafe extern "C" fn( slf: *mut PyObject, args: *const *mut PyObject, nargs: crate::pyport::Py_ssize_t, kwnames: *mut PyObject, ) -> *mut PyObject; +#[cfg(any(Py_3_10, not(Py_LIMITED_API)))] +#[deprecated(note = "renamed to `PyCFunctionFastWithKeywords`")] +pub type _PyCFunctionFastWithKeywords = PyCFunctionFastWithKeywords; + #[cfg(all(Py_3_9, not(Py_LIMITED_API)))] pub type PyCMethod = unsafe extern "C" fn( slf: *mut PyObject, @@ -144,11 +152,21 @@ pub union PyMethodDefPointer { /// This variant corresponds with [`METH_FASTCALL`]. #[cfg(any(Py_3_10, not(Py_LIMITED_API)))] - pub _PyCFunctionFast: _PyCFunctionFast, + #[deprecated(note = "renamed to `PyCFunctionFast`")] + pub _PyCFunctionFast: PyCFunctionFast, + + /// This variant corresponds with [`METH_FASTCALL`]. + #[cfg(any(Py_3_10, not(Py_LIMITED_API)))] + pub PyCFunctionFast: PyCFunctionFast, + + /// This variant corresponds with [`METH_FASTCALL`] | [`METH_KEYWORDS`]. + #[cfg(any(Py_3_10, not(Py_LIMITED_API)))] + #[deprecated(note = "renamed to `PyCFunctionFastWithKeywords`")] + pub _PyCFunctionFastWithKeywords: PyCFunctionFastWithKeywords, /// This variant corresponds with [`METH_FASTCALL`] | [`METH_KEYWORDS`]. - #[cfg(not(Py_LIMITED_API))] - pub _PyCFunctionFastWithKeywords: _PyCFunctionFastWithKeywords, + #[cfg(any(Py_3_10, not(Py_LIMITED_API)))] + pub PyCFunctionFastWithKeywords: PyCFunctionFastWithKeywords, /// This variant corresponds with [`METH_METHOD`] | [`METH_FASTCALL`] | [`METH_KEYWORDS`]. #[cfg(all(Py_3_9, not(Py_LIMITED_API)))] diff --git a/pyo3-macros-backend/src/method.rs b/pyo3-macros-backend/src/method.rs index c850f67b2b9..633083dea95 100644 --- a/pyo3-macros-backend/src/method.rs +++ b/pyo3-macros-backend/src/method.rs @@ -7,6 +7,7 @@ use quote::{format_ident, quote, quote_spanned, ToTokens}; use syn::{ext::IdentExt, spanned::Spanned, Ident, Result}; use crate::deprecations::deprecate_trailing_option_default; +use crate::pyversions::is_abi3_before; use crate::utils::{Ctx, LitCStr}; use crate::{ attributes::{FromPyWithAttribute, TextSignatureAttribute, TextSignatureAttributeValue}, @@ -15,7 +16,7 @@ use crate::{ FunctionSignature, PyFunctionArgPyO3Attributes, PyFunctionOptions, SignatureAttribute, }, quotes, - utils::{self, is_abi3, PythonDoc}, + utils::{self, PythonDoc}, }; #[derive(Clone, Debug)] @@ -374,7 +375,7 @@ impl SelfType { pub enum CallingConvention { Noargs, // METH_NOARGS Varargs, // METH_VARARGS | METH_KEYWORDS - Fastcall, // METH_FASTCALL | METH_KEYWORDS (not compatible with `abi3` feature) + Fastcall, // METH_FASTCALL | METH_KEYWORDS (not compatible with `abi3` feature before 3.10) TpNew, // special convention for tp_new } @@ -386,11 +387,11 @@ impl CallingConvention { pub fn from_signature(signature: &FunctionSignature<'_>) -> Self { if signature.python_signature.has_no_args() { Self::Noargs - } else if signature.python_signature.kwargs.is_some() { - // for functions that accept **kwargs, always prefer varargs - Self::Varargs - } else if !is_abi3() { - // FIXME: available in the stable ABI since 3.10 + } else if signature.python_signature.kwargs.is_none() && !is_abi3_before(3, 10) { + // For functions that accept **kwargs, always prefer varargs for now based on + // historical performance testing. + // + // FASTCALL not compatible with `abi3` before 3.10 Self::Fastcall } else { Self::Varargs diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index 5ad5e61da26..9e3dbceaa91 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -22,9 +22,8 @@ use crate::pymethod::{ impl_py_getter_def, impl_py_setter_def, MethodAndMethodDef, MethodAndSlotDef, PropertyType, SlotDef, __GETITEM__, __HASH__, __INT__, __LEN__, __REPR__, __RICHCMP__, __STR__, }; -use crate::pyversions; -use crate::utils::{self, apply_renaming_rule, LitCStr, PythonDoc}; -use crate::utils::{is_abi3, Ctx}; +use crate::pyversions::is_abi3_before; +use crate::utils::{self, apply_renaming_rule, Ctx, LitCStr, PythonDoc}; use crate::PyFunctionOptions; /// If the class is derived from a Rust `struct` or `enum`. @@ -186,13 +185,11 @@ impl PyClassPyO3Options { }; } - let python_version = pyo3_build_config::get().version; - match option { PyClassPyO3Option::Crate(krate) => set_option!(krate), PyClassPyO3Option::Dict(dict) => { ensure_spanned!( - python_version >= pyversions::PY_3_9 || !is_abi3(), + !is_abi3_before(3, 9), dict.span() => "`dict` requires Python >= 3.9 when using the `abi3` feature" ); set_option!(dict); @@ -216,7 +213,7 @@ impl PyClassPyO3Options { PyClassPyO3Option::Unsendable(unsendable) => set_option!(unsendable), PyClassPyO3Option::Weakref(weakref) => { ensure_spanned!( - python_version >= pyversions::PY_3_9 || !is_abi3(), + !is_abi3_before(3, 9), weakref.span() => "`weakref` requires Python >= 3.9 when using the `abi3` feature" ); set_option!(weakref); diff --git a/pyo3-macros-backend/src/pyversions.rs b/pyo3-macros-backend/src/pyversions.rs index 23d25bf8cce..4c0998667d8 100644 --- a/pyo3-macros-backend/src/pyversions.rs +++ b/pyo3-macros-backend/src/pyversions.rs @@ -1,3 +1,6 @@ use pyo3_build_config::PythonVersion; -pub const PY_3_9: PythonVersion = PythonVersion { major: 3, minor: 9 }; +pub fn is_abi3_before(major: u8, minor: u8) -> bool { + let config = pyo3_build_config::get(); + config.abi3 && config.version < PythonVersion { major, minor } +} diff --git a/pyo3-macros-backend/src/utils.rs b/pyo3-macros-backend/src/utils.rs index 350abb6bbf6..191ee165bbc 100644 --- a/pyo3-macros-backend/src/utils.rs +++ b/pyo3-macros-backend/src/utils.rs @@ -288,10 +288,6 @@ pub fn apply_renaming_rule(rule: RenamingRule, name: &str) -> String { } } -pub(crate) fn is_abi3() -> bool { - pyo3_build_config::get().abi3 -} - pub(crate) enum IdentOrStr<'a> { Str(&'a str), Ident(syn::Ident), diff --git a/src/impl_/extract_argument.rs b/src/impl_/extract_argument.rs index e2a184ce7dd..5402f33e6fa 100644 --- a/src/impl_/extract_argument.rs +++ b/src/impl_/extract_argument.rs @@ -262,7 +262,7 @@ impl FunctionDescription { /// - `args` must be a pointer to a C-style array of valid `ffi::PyObject` pointers, or NULL. /// - `kwnames` must be a pointer to a PyTuple, or NULL. /// - `nargs + kwnames.len()` is the total length of the `args` array. - #[cfg(not(Py_LIMITED_API))] + #[cfg(any(Py_3_10, not(Py_LIMITED_API)))] pub unsafe fn extract_arguments_fastcall<'py, V, K>( &self, py: Python<'py>, diff --git a/src/impl_/pymethods.rs b/src/impl_/pymethods.rs index 77c6e6991ac..b150f474c72 100644 --- a/src/impl_/pymethods.rs +++ b/src/impl_/pymethods.rs @@ -72,8 +72,8 @@ pub enum PyMethodDefType { pub enum PyMethodType { PyCFunction(ffi::PyCFunction), PyCFunctionWithKeywords(ffi::PyCFunctionWithKeywords), - #[cfg(not(Py_LIMITED_API))] - PyCFunctionFastWithKeywords(ffi::_PyCFunctionFastWithKeywords), + #[cfg(any(Py_3_10, not(Py_LIMITED_API)))] + PyCFunctionFastWithKeywords(ffi::PyCFunctionFastWithKeywords), } pub type PyClassAttributeFactory = for<'p> fn(Python<'p>) -> PyResult; @@ -145,10 +145,10 @@ impl PyMethodDef { } /// Define a function that can take `*args` and `**kwargs`. - #[cfg(not(Py_LIMITED_API))] + #[cfg(any(Py_3_10, not(Py_LIMITED_API)))] pub const fn fastcall_cfunction_with_keywords( ml_name: &'static CStr, - cfunction: ffi::_PyCFunctionFastWithKeywords, + cfunction: ffi::PyCFunctionFastWithKeywords, ml_doc: &'static CStr, ) -> Self { Self { @@ -171,9 +171,9 @@ impl PyMethodDef { PyMethodType::PyCFunctionWithKeywords(meth) => ffi::PyMethodDefPointer { PyCFunctionWithKeywords: meth, }, - #[cfg(not(Py_LIMITED_API))] + #[cfg(any(Py_3_10, not(Py_LIMITED_API)))] PyMethodType::PyCFunctionFastWithKeywords(meth) => ffi::PyMethodDefPointer { - _PyCFunctionFastWithKeywords: meth, + PyCFunctionFastWithKeywords: meth, }, }; @@ -519,3 +519,40 @@ pub unsafe fn tp_new_impl( .create_class_object_of_type(py, target_type) .map(Bound::into_ptr) } + +#[cfg(test)] +mod tests { + #[test] + #[cfg(any(Py_3_10, not(Py_LIMITED_API)))] + fn test_fastcall_function_with_keywords() { + use super::PyMethodDef; + use crate::types::{PyAnyMethods, PyCFunction}; + use crate::{ffi, Python}; + + Python::with_gil(|py| { + unsafe extern "C" fn accepts_no_arguments( + _slf: *mut ffi::PyObject, + _args: *const *mut ffi::PyObject, + nargs: ffi::Py_ssize_t, + kwargs: *mut ffi::PyObject, + ) -> *mut ffi::PyObject { + assert_eq!(nargs, 0); + assert!(kwargs.is_null()); + Python::assume_gil_acquired().None().into_ptr() + } + + let f = PyCFunction::internal_new( + py, + &PyMethodDef::fastcall_cfunction_with_keywords( + ffi::c_str!("test"), + accepts_no_arguments, + ffi::c_str!("doc"), + ), + None, + ) + .unwrap(); + + f.call0().unwrap(); + }); + } +} From 144b199f1dab66391f70573e29946d0a7c63bfd8 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Sat, 17 Aug 2024 08:22:45 +0100 Subject: [PATCH 217/495] ffi: define compat for `Py_NewRef` and `Py_XNewRef` (#4445) * ffi: define compat for `Py_NewRef` and `Py_XNewRef` * add missing inline hint Co-authored-by: Nathan Goldbaum * don't use std::ffi::c_int (requires MSRV 1.64) * add test to guard against ambiguity * fix `Py_NewRef` cfg on PyPy --------- Co-authored-by: Nathan Goldbaum --- newsfragments/4445.added.md | 1 + newsfragments/4445.removed.md | 1 + pyo3-ffi/Cargo.toml | 3 ++ pyo3-ffi/src/compat.rs | 60 ------------------------------- pyo3-ffi/src/compat/mod.rs | 57 +++++++++++++++++++++++++++++ pyo3-ffi/src/compat/py_3_10.rs | 19 ++++++++++ pyo3-ffi/src/compat/py_3_13.rs | 39 ++++++++++++++++++++ pyo3-ffi/src/object.rs | 31 +++++++--------- src/pyclass/create_type_object.rs | 2 +- 9 files changed, 133 insertions(+), 80 deletions(-) create mode 100644 newsfragments/4445.added.md create mode 100644 newsfragments/4445.removed.md delete mode 100644 pyo3-ffi/src/compat.rs create mode 100644 pyo3-ffi/src/compat/mod.rs create mode 100644 pyo3-ffi/src/compat/py_3_10.rs create mode 100644 pyo3-ffi/src/compat/py_3_13.rs diff --git a/newsfragments/4445.added.md b/newsfragments/4445.added.md new file mode 100644 index 00000000000..ee6af52d97e --- /dev/null +++ b/newsfragments/4445.added.md @@ -0,0 +1 @@ +Add FFI definitions `compat::Py_NewRef` and `compat::Py_XNewRef`. diff --git a/newsfragments/4445.removed.md b/newsfragments/4445.removed.md new file mode 100644 index 00000000000..b5b7e450331 --- /dev/null +++ b/newsfragments/4445.removed.md @@ -0,0 +1 @@ +Remove private FFI definitions `_Py_NewRef` and `_Py_XNewRef`. diff --git a/pyo3-ffi/Cargo.toml b/pyo3-ffi/Cargo.toml index 8e1b203fcd1..0e58197bbfd 100644 --- a/pyo3-ffi/Cargo.toml +++ b/pyo3-ffi/Cargo.toml @@ -37,6 +37,9 @@ 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"] +[dev-dependencies] +paste = "1" + [build-dependencies] pyo3-build-config = { path = "../pyo3-build-config", version = "=0.23.0-dev", features = ["resolve-config"] } diff --git a/pyo3-ffi/src/compat.rs b/pyo3-ffi/src/compat.rs deleted file mode 100644 index 85986817d93..00000000000 --- a/pyo3-ffi/src/compat.rs +++ /dev/null @@ -1,60 +0,0 @@ -//! C API Compatibility Shims -//! -//! Some CPython C API functions added in recent versions of Python are -//! inherently safer to use than older C API constructs. This module -//! exposes functions available on all Python versions that wrap the -//! old C API on old Python versions and wrap the function directly -//! on newer Python versions. - -// Unless otherwise noted, the compatibility shims are adapted from -// the pythoncapi-compat project: https://github.com/python/pythoncapi-compat - -#[cfg(not(Py_3_13))] -use crate::object::PyObject; -#[cfg(not(Py_3_13))] -use crate::pyport::Py_ssize_t; -#[cfg(not(Py_3_13))] -use std::os::raw::c_int; - -#[cfg_attr(docsrs, doc(cfg(all())))] -#[cfg(Py_3_13)] -pub use crate::dictobject::PyDict_GetItemRef; - -#[cfg_attr(docsrs, doc(cfg(all())))] -#[cfg(not(Py_3_13))] -pub unsafe fn PyDict_GetItemRef( - dp: *mut PyObject, - key: *mut PyObject, - result: *mut *mut PyObject, -) -> c_int { - { - use crate::dictobject::PyDict_GetItemWithError; - use crate::object::_Py_NewRef; - use crate::pyerrors::PyErr_Occurred; - - let item: *mut PyObject = PyDict_GetItemWithError(dp, key); - if !item.is_null() { - *result = _Py_NewRef(item); - return 1; // found - } - *result = std::ptr::null_mut(); - if PyErr_Occurred().is_null() { - return 0; // not found - } - -1 - } -} - -#[cfg_attr(docsrs, doc(cfg(all())))] -#[cfg(Py_3_13)] -pub use crate::PyList_GetItemRef; - -#[cfg(not(Py_3_13))] -#[cfg_attr(docsrs, doc(cfg(all())))] -pub unsafe fn PyList_GetItemRef(arg1: *mut PyObject, arg2: Py_ssize_t) -> *mut PyObject { - use crate::{PyList_GetItem, Py_XINCREF}; - - let item: *mut PyObject = PyList_GetItem(arg1, arg2); - Py_XINCREF(item); - item -} diff --git a/pyo3-ffi/src/compat/mod.rs b/pyo3-ffi/src/compat/mod.rs new file mode 100644 index 00000000000..db41c834b3d --- /dev/null +++ b/pyo3-ffi/src/compat/mod.rs @@ -0,0 +1,57 @@ +//! C API Compatibility Shims +//! +//! Some CPython C API functions added in recent versions of Python are +//! inherently safer to use than older C API constructs. This module +//! exposes functions available on all Python versions that wrap the +//! old C API on old Python versions and wrap the function directly +//! on newer Python versions. + +// Unless otherwise noted, the compatibility shims are adapted from +// the pythoncapi-compat project: https://github.com/python/pythoncapi-compat + +/// Internal helper macro which defines compatibility shims for C API functions, deferring to a +/// re-export when that's available. +macro_rules! compat_function { + ( + originally_defined_for($cfg:meta); + + $(#[$attrs:meta])* + pub unsafe fn $name:ident($($arg_names:ident: $arg_types:ty),* $(,)?) -> $ret:ty $body:block + ) => { + // Define as a standalone function under docsrs cfg so that this shows as a unique function in the docs, + // not a re-export (the re-export has the wrong visibility) + #[cfg(any(docsrs, not($cfg)))] + #[cfg_attr(docsrs, doc(cfg(all())))] + $(#[$attrs])* + pub unsafe fn $name( + $($arg_names: $arg_types,)* + ) -> $ret $body + + #[cfg(all($cfg, not(docsrs)))] + pub use $crate::$name; + + #[cfg(test)] + paste::paste! { + // Test that the compat function does not overlap with the original function. If the + // cfgs line up, then the the two glob imports will resolve to the same item via the + // re-export. If the cfgs mismatch, then the use of $name will be ambiguous in cases + // where the function is defined twice, and the test will fail to compile. + #[allow(unused_imports)] + mod [] { + use $crate::*; + use $crate::compat::*; + + #[test] + fn test_export() { + let _ = $name; + } + } + } + }; +} + +mod py_3_10; +mod py_3_13; + +pub use self::py_3_10::*; +pub use self::py_3_13::*; diff --git a/pyo3-ffi/src/compat/py_3_10.rs b/pyo3-ffi/src/compat/py_3_10.rs new file mode 100644 index 00000000000..c6e8c2cb5ca --- /dev/null +++ b/pyo3-ffi/src/compat/py_3_10.rs @@ -0,0 +1,19 @@ +compat_function!( + originally_defined_for(Py_3_10); + + #[inline] + pub unsafe fn Py_NewRef(obj: *mut crate::PyObject) -> *mut crate::PyObject { + crate::Py_INCREF(obj); + obj + } +); + +compat_function!( + originally_defined_for(Py_3_10); + + #[inline] + pub unsafe fn Py_XNewRef(obj: *mut crate::PyObject) -> *mut crate::PyObject { + crate::Py_XINCREF(obj); + obj + } +); diff --git a/pyo3-ffi/src/compat/py_3_13.rs b/pyo3-ffi/src/compat/py_3_13.rs new file mode 100644 index 00000000000..75c4cd101ae --- /dev/null +++ b/pyo3-ffi/src/compat/py_3_13.rs @@ -0,0 +1,39 @@ +compat_function!( + originally_defined_for(Py_3_13); + + #[inline] + pub unsafe fn PyDict_GetItemRef( + dp: *mut crate::PyObject, + key: *mut crate::PyObject, + result: *mut *mut crate::PyObject, + ) -> std::os::raw::c_int { + use crate::{compat::Py_NewRef, PyDict_GetItemWithError, PyErr_Occurred}; + + let item = PyDict_GetItemWithError(dp, key); + if !item.is_null() { + *result = Py_NewRef(item); + return 1; // found + } + *result = std::ptr::null_mut(); + if PyErr_Occurred().is_null() { + return 0; // not found + } + -1 + } +); + +compat_function!( + originally_defined_for(Py_3_13); + + #[inline] + pub unsafe fn PyList_GetItemRef( + arg1: *mut crate::PyObject, + arg2: crate::Py_ssize_t, + ) -> *mut crate::PyObject { + use crate::{PyList_GetItem, Py_XINCREF}; + + let item = PyList_GetItem(arg1, arg2); + Py_XINCREF(item); + item + } +); diff --git a/pyo3-ffi/src/object.rs b/pyo3-ffi/src/object.rs index 27436c208a9..5e8b6af44a1 100644 --- a/pyo3-ffi/src/object.rs +++ b/pyo3-ffi/src/object.rs @@ -713,40 +713,33 @@ pub unsafe fn Py_XDECREF(op: *mut PyObject) { } extern "C" { - #[cfg(all(Py_3_10, Py_LIMITED_API))] + #[cfg(all(Py_3_10, Py_LIMITED_API, not(PyPy)))] + #[cfg_attr(docsrs, doc(cfg(Py_3_10)))] pub fn Py_NewRef(obj: *mut PyObject) -> *mut PyObject; - #[cfg(all(Py_3_10, Py_LIMITED_API))] + #[cfg(all(Py_3_10, Py_LIMITED_API, not(PyPy)))] + #[cfg_attr(docsrs, doc(cfg(Py_3_10)))] pub fn Py_XNewRef(obj: *mut PyObject) -> *mut PyObject; } -// Technically these macros are only available in the C header from 3.10 and up, however their -// implementation works on all supported Python versions so we define these macros on all -// versions for simplicity. +// macro _Py_NewRef not public; reimplemented directly inside Py_NewRef here +// macro _Py_XNewRef not public; reimplemented directly inside Py_XNewRef here +#[cfg(all(Py_3_10, any(not(Py_LIMITED_API), PyPy)))] +#[cfg_attr(docsrs, doc(cfg(Py_3_10)))] #[inline] -pub unsafe fn _Py_NewRef(obj: *mut PyObject) -> *mut PyObject { +pub unsafe fn Py_NewRef(obj: *mut PyObject) -> *mut PyObject { Py_INCREF(obj); obj } +#[cfg(all(Py_3_10, any(not(Py_LIMITED_API), PyPy)))] +#[cfg_attr(docsrs, doc(cfg(Py_3_10)))] #[inline] -pub unsafe fn _Py_XNewRef(obj: *mut PyObject) -> *mut PyObject { +pub unsafe fn Py_XNewRef(obj: *mut PyObject) -> *mut PyObject { Py_XINCREF(obj); obj } -#[cfg(all(Py_3_10, not(Py_LIMITED_API)))] -#[inline] -pub unsafe fn Py_NewRef(obj: *mut PyObject) -> *mut PyObject { - _Py_NewRef(obj) -} - -#[cfg(all(Py_3_10, not(Py_LIMITED_API)))] -#[inline] -pub unsafe fn Py_XNewRef(obj: *mut PyObject) -> *mut PyObject { - _Py_XNewRef(obj) -} - #[cfg_attr(windows, link(name = "pythonXY"))] extern "C" { #[cfg(not(GraalPy))] diff --git a/src/pyclass/create_type_object.rs b/src/pyclass/create_type_object.rs index ac5b6287e86..b05d164e2b0 100644 --- a/src/pyclass/create_type_object.rs +++ b/src/pyclass/create_type_object.rs @@ -250,7 +250,7 @@ impl PyTypeBuilder { if (*dict_ptr).is_null() { std::ptr::write(dict_ptr, ffi::PyDict_New()); } - Ok(ffi::_Py_XNewRef(*dict_ptr)) + Ok(ffi::compat::Py_XNewRef(*dict_ptr)) }) } } From f38be291abd0e51ee248a7acece810feb2c41fac Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Sat, 17 Aug 2024 15:18:40 +0200 Subject: [PATCH 218/495] Add more `IntoPyObject` impls (#4446) * add `IntoPyObject` for `bool` * add `IntoPyObject` for `PyErr` * add `IntoPyObject` for `&Duration` and `&SystemTime` * add `IntoPyObject` for string references * add `IntoPyObject` for set references * add `IntoPyObject` for `&Option` * add `IntoPyObject` for map references * add `IntoPyObject` for `&Cell` * add `IntoPyObject` for ipaddr references * add `IntoPyObject` for `&Decimal` * add `IntoPyObject` for `Ratio` * add `IntoPyObject` for `&Complex` * add `IntoPyObject` for bigint references * add `IntoPyObject` for `&Either` * add `IntoPyObject` for chrono references * add `IntoPyObject` for `&SmallVec` * add `IntoPyObject` for `&[T; N]` * add `IntoPyObject` for path and osstr references * add `IntoPyObject` for `&Borrowed` * bless ui test * implement `ToPyObject` and `IntoPy` in terms of `IntoPyObject` --- src/conversion.rs | 10 ++ src/conversions/chrono.rs | 218 ++++++++++++++++++-------------- src/conversions/chrono_tz.rs | 23 ++-- src/conversions/either.rs | 38 +++++- src/conversions/hashbrown.rs | 50 +++++++- src/conversions/indexmap.rs | 25 +++- src/conversions/num_bigint.rs | 74 +++-------- src/conversions/num_complex.rs | 12 ++ src/conversions/num_rational.rs | 36 +++++- src/conversions/rust_decimal.rs | 25 ++-- src/conversions/smallvec.rs | 16 +++ src/conversions/std/array.rs | 15 +++ src/conversions/std/cell.rs | 12 ++ src/conversions/std/ipaddr.rs | 60 ++++++--- src/conversions/std/map.rs | 49 ++++++- src/conversions/std/num.rs | 149 +++++++++++++++++----- src/conversions/std/option.rs | 14 ++ src/conversions/std/osstr.rs | 29 ++++- src/conversions/std/path.rs | 32 ++++- src/conversions/std/set.rs | 47 ++++++- src/conversions/std/string.rs | 60 +++++++-- src/conversions/std/time.rs | 124 ++++++++---------- src/err/mod.rs | 33 ++++- src/types/boolobject.rs | 38 ++++-- src/types/float.rs | 58 ++++++++- tests/ui/missing_intopy.stderr | 14 +- 26 files changed, 902 insertions(+), 359 deletions(-) diff --git a/src/conversion.rs b/src/conversion.rs index 82710cf16fb..d909382bdb9 100644 --- a/src/conversion.rs +++ b/src/conversion.rs @@ -291,6 +291,16 @@ impl<'a, 'py, T> IntoPyObject<'py> for Borrowed<'a, 'py, T> { } } +impl<'a, 'py, T> IntoPyObject<'py> for &Borrowed<'a, 'py, T> { + type Target = T; + type Output = Borrowed<'a, 'py, Self::Target>; + type Error = Infallible; + + fn into_pyobject(self, _py: Python<'py>) -> Result { + Ok(*self) + } +} + impl<'py, T> IntoPyObject<'py> for Py { type Target = T; type Output = Bound<'py, Self::Target>; diff --git a/src/conversions/chrono.rs b/src/conversions/chrono.rs index 25c5acd9963..44cee349c2f 100644 --- a/src/conversions/chrono.rs +++ b/src/conversions/chrono.rs @@ -61,49 +61,16 @@ use chrono::{ }; impl ToPyObject for Duration { + #[inline] fn to_object(&self, py: Python<'_>) -> PyObject { - // Total number of days - let days = self.num_days(); - // Remainder of seconds - let secs_dur = *self - Duration::days(days); - let secs = secs_dur.num_seconds(); - // Fractional part of the microseconds - let micros = (secs_dur - Duration::seconds(secs_dur.num_seconds())) - .num_microseconds() - // This should never panic since we are just getting the fractional - // part of the total microseconds, which should never overflow. - .unwrap(); - - #[cfg(not(Py_LIMITED_API))] - { - // We do not need to check the days i64 to i32 cast from rust because - // python will panic with OverflowError. - // We pass true as the `normalize` parameter since we'd need to do several checks here to - // avoid that, and it shouldn't have a big performance impact. - // The seconds and microseconds cast should never overflow since it's at most the number of seconds per day - PyDelta::new_bound( - py, - days.try_into().unwrap_or(i32::MAX), - secs.try_into().unwrap(), - micros.try_into().unwrap(), - true, - ) - .expect("failed to construct delta") - .into() - } - #[cfg(Py_LIMITED_API)] - { - DatetimeTypes::get(py) - .timedelta - .call1(py, (days, secs, micros)) - .expect("failed to construct datetime.timedelta") - } + self.into_pyobject(py).unwrap().into_any().unbind() } } impl IntoPy for Duration { + #[inline] fn into_py(self, py: Python<'_>) -> PyObject { - self.to_object(py) + self.into_pyobject(py).unwrap().into_any().unbind() } } @@ -152,6 +119,20 @@ impl<'py> IntoPyObject<'py> for Duration { } } +impl<'py> IntoPyObject<'py> for &Duration { + #[cfg(Py_LIMITED_API)] + type Target = PyAny; + #[cfg(not(Py_LIMITED_API))] + type Target = PyDelta; + type Output = Bound<'py, Self::Target>; + type Error = PyErr; + + #[inline] + fn into_pyobject(self, py: Python<'py>) -> Result { + (*self).into_pyobject(py) + } +} + impl FromPyObject<'_> for Duration { fn extract_bound(ob: &Bound<'_, PyAny>) -> PyResult { // Python size are much lower than rust size so we do not need bound checks. @@ -185,27 +166,16 @@ impl FromPyObject<'_> for Duration { } impl ToPyObject for NaiveDate { + #[inline] fn to_object(&self, py: Python<'_>) -> PyObject { - let DateArgs { year, month, day } = self.into(); - #[cfg(not(Py_LIMITED_API))] - { - PyDate::new_bound(py, year, month, day) - .expect("failed to construct date") - .into() - } - #[cfg(Py_LIMITED_API)] - { - DatetimeTypes::get(py) - .date - .call1(py, (year, month, day)) - .expect("failed to construct datetime.date") - } + self.into_pyobject(py).unwrap().into_any().unbind() } } impl IntoPy for NaiveDate { + #[inline] fn into_py(self, py: Python<'_>) -> PyObject { - self.to_object(py) + self.into_pyobject(py).unwrap().into_any().unbind() } } @@ -231,6 +201,20 @@ impl<'py> IntoPyObject<'py> for NaiveDate { } } +impl<'py> IntoPyObject<'py> for &NaiveDate { + #[cfg(Py_LIMITED_API)] + type Target = PyAny; + #[cfg(not(Py_LIMITED_API))] + type Target = PyDate; + type Output = Bound<'py, Self::Target>; + type Error = PyErr; + + #[inline] + fn into_pyobject(self, py: Python<'py>) -> Result { + (*self).into_pyobject(py) + } +} + impl FromPyObject<'_> for NaiveDate { fn extract_bound(ob: &Bound<'_, PyAny>) -> PyResult { #[cfg(not(Py_LIMITED_API))] @@ -247,33 +231,16 @@ impl FromPyObject<'_> for NaiveDate { } impl ToPyObject for NaiveTime { + #[inline] fn to_object(&self, py: Python<'_>) -> PyObject { - let TimeArgs { - hour, - min, - sec, - micro, - truncated_leap_second, - } = self.into(); - #[cfg(not(Py_LIMITED_API))] - let time = - PyTime::new_bound(py, hour, min, sec, micro, None).expect("Failed to construct time"); - #[cfg(Py_LIMITED_API)] - let time = DatetimeTypes::get(py) - .time - .bind(py) - .call1((hour, min, sec, micro)) - .expect("failed to construct datetime.time"); - if truncated_leap_second { - warn_truncated_leap_second(&time); - } - time.into() + self.into_pyobject(py).unwrap().into_any().unbind() } } impl IntoPy for NaiveTime { + #[inline] fn into_py(self, py: Python<'_>) -> PyObject { - self.to_object(py) + self.into_pyobject(py).unwrap().into_any().unbind() } } @@ -309,6 +276,20 @@ impl<'py> IntoPyObject<'py> for NaiveTime { } } +impl<'py> IntoPyObject<'py> for &NaiveTime { + #[cfg(Py_LIMITED_API)] + type Target = PyAny; + #[cfg(not(Py_LIMITED_API))] + type Target = PyTime; + type Output = Bound<'py, Self::Target>; + type Error = PyErr; + + #[inline] + fn into_pyobject(self, py: Python<'py>) -> Result { + (*self).into_pyobject(py) + } +} + impl FromPyObject<'_> for NaiveTime { fn extract_bound(ob: &Bound<'_, PyAny>) -> PyResult { #[cfg(not(Py_LIMITED_API))] @@ -325,14 +306,16 @@ impl FromPyObject<'_> for NaiveTime { } impl ToPyObject for NaiveDateTime { + #[inline] fn to_object(&self, py: Python<'_>) -> PyObject { - naive_datetime_to_py_datetime(py, self, None) + self.into_pyobject(py).unwrap().into_any().unbind() } } impl IntoPy for NaiveDateTime { + #[inline] fn into_py(self, py: Python<'_>) -> PyObject { - self.to_object(py) + self.into_pyobject(py).unwrap().into_any().unbind() } } @@ -372,6 +355,20 @@ impl<'py> IntoPyObject<'py> for NaiveDateTime { } } +impl<'py> IntoPyObject<'py> for &NaiveDateTime { + #[cfg(Py_LIMITED_API)] + type Target = PyAny; + #[cfg(not(Py_LIMITED_API))] + type Target = PyDateTime; + type Output = Bound<'py, Self::Target>; + type Error = PyErr; + + #[inline] + fn into_pyobject(self, py: Python<'py>) -> Result { + (*self).into_pyobject(py) + } +} + impl FromPyObject<'_> for NaiveDateTime { fn extract_bound(dt: &Bound<'_, PyAny>) -> PyResult { #[cfg(not(Py_LIMITED_API))] @@ -419,6 +416,20 @@ impl<'py, Tz: TimeZone> IntoPyObject<'py> for DateTime { type Output = Bound<'py, Self::Target>; type Error = PyErr; + #[inline] + fn into_pyobject(self, py: Python<'py>) -> Result { + (&self).into_pyobject(py) + } +} + +impl<'py, Tz: TimeZone> IntoPyObject<'py> for &DateTime { + #[cfg(Py_LIMITED_API)] + type Target = PyAny; + #[cfg(not(Py_LIMITED_API))] + type Target = PyDateTime; + type Output = Bound<'py, Self::Target>; + type Error = PyErr; + fn into_pyobject(self, py: Python<'py>) -> Result { let tz = self.offset().fix().into_pyobject(py)?; let DateArgs { year, month, day } = (&self.naive_local().date()).into(); @@ -479,31 +490,16 @@ impl FromPyObject<'py>> FromPyObject<'_> for DateTime) -> PyObject { - let seconds_offset = self.local_minus_utc(); - - #[cfg(not(Py_LIMITED_API))] - { - let td = PyDelta::new_bound(py, 0, seconds_offset, 0, true) - .expect("failed to construct timedelta"); - timezone_from_offset(&td) - .expect("Failed to construct PyTimezone") - .into() - } - #[cfg(Py_LIMITED_API)] - { - let td = Duration::seconds(seconds_offset.into()).into_py(py); - DatetimeTypes::get(py) - .timezone - .call1(py, (td,)) - .expect("failed to construct datetime.timezone") - } + self.into_pyobject(py).unwrap().into_any().unbind() } } impl IntoPy for FixedOffset { + #[inline] fn into_py(self, py: Python<'_>) -> PyObject { - self.to_object(py) + self.into_pyobject(py).unwrap().into_any().unbind() } } @@ -531,6 +527,20 @@ impl<'py> IntoPyObject<'py> for FixedOffset { } } +impl<'py> IntoPyObject<'py> for &FixedOffset { + #[cfg(Py_LIMITED_API)] + type Target = PyAny; + #[cfg(not(Py_LIMITED_API))] + type Target = PyTzInfo; + type Output = Bound<'py, Self::Target>; + type Error = PyErr; + + #[inline] + fn into_pyobject(self, py: Python<'py>) -> Result { + (*self).into_pyobject(py) + } +} + impl FromPyObject<'_> for FixedOffset { /// Convert python tzinfo to rust [`FixedOffset`]. /// @@ -563,14 +573,16 @@ impl FromPyObject<'_> for FixedOffset { } impl ToPyObject for Utc { + #[inline] fn to_object(&self, py: Python<'_>) -> PyObject { - timezone_utc_bound(py).into() + self.into_pyobject(py).unwrap().into_any().unbind() } } impl IntoPy for Utc { + #[inline] fn into_py(self, py: Python<'_>) -> PyObject { - self.to_object(py) + self.into_pyobject(py).unwrap().into_any().unbind() } } @@ -594,6 +606,20 @@ impl<'py> IntoPyObject<'py> for Utc { } } +impl<'py> IntoPyObject<'py> for &Utc { + #[cfg(Py_LIMITED_API)] + type Target = PyAny; + #[cfg(not(Py_LIMITED_API))] + type Target = PyTzInfo; + type Output = Bound<'py, Self::Target>; + type Error = PyErr; + + #[inline] + fn into_pyobject(self, py: Python<'py>) -> Result { + (*self).into_pyobject(py) + } +} + impl FromPyObject<'_> for Utc { fn extract_bound(ob: &Bound<'_, PyAny>) -> PyResult { let py_utc = timezone_utc_bound(ob.py()); diff --git a/src/conversions/chrono_tz.rs b/src/conversions/chrono_tz.rs index 88bf39de64a..52c156eaba2 100644 --- a/src/conversions/chrono_tz.rs +++ b/src/conversions/chrono_tz.rs @@ -45,20 +45,16 @@ use chrono_tz::Tz; use std::str::FromStr; impl ToPyObject for Tz { + #[inline] fn to_object(&self, py: Python<'_>) -> PyObject { - static ZONE_INFO: GILOnceCell> = GILOnceCell::new(); - ZONE_INFO - .get_or_try_init_type_ref(py, "zoneinfo", "ZoneInfo") - .unwrap() - .call1((self.name(),)) - .unwrap() - .unbind() + self.into_pyobject(py).unwrap().unbind() } } impl IntoPy for Tz { + #[inline] fn into_py(self, py: Python<'_>) -> PyObject { - self.to_object(py) + self.into_pyobject(py).unwrap().unbind() } } @@ -75,6 +71,17 @@ impl<'py> IntoPyObject<'py> for Tz { } } +impl<'py> IntoPyObject<'py> for &Tz { + type Target = PyAny; + type Output = Bound<'py, Self::Target>; + type Error = PyErr; + + #[inline] + fn into_pyobject(self, py: Python<'py>) -> Result { + (*self).into_pyobject(py) + } +} + impl FromPyObject<'_> for Tz { fn extract_bound(ob: &Bound<'_, PyAny>) -> PyResult { Tz::from_str( diff --git a/src/conversions/either.rs b/src/conversions/either.rs index 2dd79ba3807..3d4aeafa32b 100644 --- a/src/conversions/either.rs +++ b/src/conversions/either.rs @@ -67,12 +67,40 @@ where } #[cfg_attr(docsrs, doc(cfg(feature = "either")))] -impl<'py, L, R, E1, E2> IntoPyObject<'py> for Either +impl<'py, L, R> IntoPyObject<'py> for Either where - L: IntoPyObject<'py, Error = E1>, - R: IntoPyObject<'py, Error = E2>, - E1: Into, - E2: Into, + L: IntoPyObject<'py>, + R: IntoPyObject<'py>, + L::Error: Into, + R::Error: Into, +{ + type Target = PyAny; + type Output = Bound<'py, Self::Target>; + type Error = PyErr; + + fn into_pyobject(self, py: Python<'py>) -> Result { + match self { + Either::Left(l) => l + .into_pyobject(py) + .map(BoundObject::into_any) + .map(BoundObject::into_bound) + .map_err(Into::into), + Either::Right(r) => r + .into_pyobject(py) + .map(BoundObject::into_any) + .map(BoundObject::into_bound) + .map_err(Into::into), + } + } +} + +#[cfg_attr(docsrs, doc(cfg(feature = "either")))] +impl<'a, 'py, L, R> IntoPyObject<'py> for &'a Either +where + &'a L: IntoPyObject<'py>, + &'a R: IntoPyObject<'py>, + <&'a L as IntoPyObject<'py>>::Error: Into, + <&'a R as IntoPyObject<'py>>::Error: Into, { type Target = PyAny; type Output = Bound<'py, Self::Target>; diff --git a/src/conversions/hashbrown.rs b/src/conversions/hashbrown.rs index d09205fce40..7104fe35679 100644 --- a/src/conversions/hashbrown.rs +++ b/src/conversions/hashbrown.rs @@ -56,7 +56,7 @@ where impl<'py, K, V, H> IntoPyObject<'py> for hashbrown::HashMap where - K: hash::Hash + cmp::Eq + IntoPyObject<'py>, + K: IntoPyObject<'py> + cmp::Eq + hash::Hash, V: IntoPyObject<'py>, H: hash::BuildHasher, PyErr: From + From, @@ -77,6 +77,29 @@ where } } +impl<'a, 'py, K, V, H> IntoPyObject<'py> for &'a hashbrown::HashMap +where + &'a K: IntoPyObject<'py> + cmp::Eq + hash::Hash, + &'a V: IntoPyObject<'py>, + H: hash::BuildHasher, + PyErr: From<<&'a K as IntoPyObject<'py>>::Error> + From<<&'a V as IntoPyObject<'py>>::Error>, +{ + type Target = PyDict; + type Output = Bound<'py, Self::Target>; + type Error = PyErr; + + fn into_pyobject(self, py: Python<'py>) -> Result { + let dict = PyDict::new(py); + for (k, v) in self { + dict.set_item( + k.into_pyobject(py)?.into_bound(), + v.into_pyobject(py)?.into_bound(), + )?; + } + Ok(dict) + } +} + impl<'py, K, V, S> FromPyObject<'py> for hashbrown::HashMap where K: FromPyObject<'py> + cmp::Eq + hash::Hash, @@ -118,7 +141,7 @@ where impl<'py, K, H> IntoPyObject<'py> for hashbrown::HashSet where - K: hash::Hash + cmp::Eq + IntoPyObject<'py>, + K: IntoPyObject<'py> + cmp::Eq + hash::Hash, H: hash::BuildHasher, PyErr: From, { @@ -139,6 +162,29 @@ where } } +impl<'a, 'py, K, H> IntoPyObject<'py> for &'a hashbrown::HashSet +where + &'a K: IntoPyObject<'py> + cmp::Eq + hash::Hash, + &'a H: hash::BuildHasher, + PyErr: From<<&'a K as IntoPyObject<'py>>::Error>, +{ + type Target = PySet; + type Output = Bound<'py, Self::Target>; + type Error = PyErr; + + fn into_pyobject(self, py: Python<'py>) -> Result { + try_new_from_iter( + py, + self.into_iter().map(|item| { + item.into_pyobject(py) + .map(BoundObject::into_any) + .map(BoundObject::unbind) + .map_err(Into::into) + }), + ) + } +} + impl<'py, K, S> FromPyObject<'py> for hashbrown::HashSet where K: FromPyObject<'py> + cmp::Eq + hash::Hash, diff --git a/src/conversions/indexmap.rs b/src/conversions/indexmap.rs index 3725bab70fc..bd45d4d3164 100644 --- a/src/conversions/indexmap.rs +++ b/src/conversions/indexmap.rs @@ -119,7 +119,7 @@ where impl<'py, K, V, H> IntoPyObject<'py> for indexmap::IndexMap where - K: hash::Hash + cmp::Eq + IntoPyObject<'py>, + K: IntoPyObject<'py> + cmp::Eq + hash::Hash, V: IntoPyObject<'py>, H: hash::BuildHasher, PyErr: From + From, @@ -140,6 +140,29 @@ where } } +impl<'a, 'py, K, V, H> IntoPyObject<'py> for &'a indexmap::IndexMap +where + &'a K: IntoPyObject<'py> + cmp::Eq + hash::Hash, + &'a V: IntoPyObject<'py>, + H: hash::BuildHasher, + PyErr: From<<&'a K as IntoPyObject<'py>>::Error> + From<<&'a V as IntoPyObject<'py>>::Error>, +{ + type Target = PyDict; + type Output = Bound<'py, Self::Target>; + type Error = PyErr; + + fn into_pyobject(self, py: Python<'py>) -> Result { + let dict = PyDict::new(py); + for (k, v) in self { + dict.set_item( + k.into_pyobject(py)?.into_bound(), + v.into_pyobject(py)?.into_bound(), + )?; + } + Ok(dict) + } +} + impl<'py, K, V, S> FromPyObject<'py> for indexmap::IndexMap where K: FromPyObject<'py> + cmp::Eq + hash::Hash, diff --git a/src/conversions/num_bigint.rs b/src/conversions/num_bigint.rs index 5ac3bf9ef61..3c699868c52 100644 --- a/src/conversions/num_bigint.rs +++ b/src/conversions/num_bigint.rs @@ -47,8 +47,6 @@ //! assert n + 1 == value //! ``` -#[cfg(not(Py_LIMITED_API))] -use crate::ffi_ptr_ext::FfiPtrExt; #[cfg(Py_LIMITED_API)] use crate::types::{bytes::PyBytesMethods, PyBytes}; use crate::{ @@ -69,69 +67,17 @@ macro_rules! bigint_conversion { ($rust_ty: ty, $is_signed: literal, $to_bytes: path) => { #[cfg_attr(docsrs, doc(cfg(feature = "num-bigint")))] impl ToPyObject for $rust_ty { - #[cfg(not(Py_LIMITED_API))] + #[inline] fn to_object(&self, py: Python<'_>) -> PyObject { - let bytes = $to_bytes(self); - #[cfg(not(Py_3_13))] - { - unsafe { - ffi::_PyLong_FromByteArray( - bytes.as_ptr().cast(), - bytes.len(), - 1, - $is_signed.into(), - ) - .assume_owned(py) - .unbind() - } - } - #[cfg(Py_3_13)] - { - if $is_signed { - unsafe { - ffi::PyLong_FromNativeBytes( - bytes.as_ptr().cast(), - bytes.len(), - ffi::Py_ASNATIVEBYTES_LITTLE_ENDIAN, - ) - .assume_owned(py) - } - } else { - unsafe { - ffi::PyLong_FromUnsignedNativeBytes( - bytes.as_ptr().cast(), - bytes.len(), - ffi::Py_ASNATIVEBYTES_LITTLE_ENDIAN, - ) - .assume_owned(py) - } - } - .unbind() - } - } - - #[cfg(Py_LIMITED_API)] - fn to_object(&self, py: Python<'_>) -> PyObject { - let bytes = $to_bytes(self); - let bytes_obj = PyBytes::new(py, &bytes); - let kwargs = if $is_signed { - let kwargs = crate::types::PyDict::new(py); - kwargs.set_item(crate::intern!(py, "signed"), true).unwrap(); - Some(kwargs) - } else { - None - }; - py.get_type::() - .call_method("from_bytes", (bytes_obj, "little"), kwargs.as_ref()) - .expect("int.from_bytes() failed during to_object()") // FIXME: #1813 or similar - .into() + self.into_pyobject(py).unwrap().into_any().unbind() } } #[cfg_attr(docsrs, doc(cfg(feature = "num-bigint")))] impl IntoPy for $rust_ty { + #[inline] fn into_py(self, py: Python<'_>) -> PyObject { - self.to_object(py) + self.into_pyobject(py).unwrap().into_any().unbind() } } @@ -141,6 +87,18 @@ macro_rules! bigint_conversion { type Output = Bound<'py, Self::Target>; type Error = PyErr; + #[inline] + fn into_pyobject(self, py: Python<'py>) -> Result { + (&self).into_pyobject(py) + } + } + + #[cfg_attr(docsrs, doc(cfg(feature = "num-bigint")))] + impl<'py> IntoPyObject<'py> for &$rust_ty { + type Target = PyInt; + type Output = Bound<'py, Self::Target>; + type Error = PyErr; + #[cfg(not(Py_LIMITED_API))] fn into_pyobject(self, py: Python<'py>) -> Result { use crate::ffi_ptr_ext::FfiPtrExt; diff --git a/src/conversions/num_complex.rs b/src/conversions/num_complex.rs index 645d704c672..ee6a3d46ec7 100644 --- a/src/conversions/num_complex.rs +++ b/src/conversions/num_complex.rs @@ -154,6 +154,18 @@ macro_rules! complex_conversion { } } + #[cfg_attr(docsrs, doc(cfg(feature = "num-complex")))] + impl<'py> crate::conversion::IntoPyObject<'py> for &Complex<$float> { + type Target = PyComplex; + type Output = Bound<'py, Self::Target>; + type Error = std::convert::Infallible; + + #[inline] + fn into_pyobject(self, py: Python<'py>) -> Result { + (*self).into_pyobject(py) + } + } + #[cfg_attr(docsrs, doc(cfg(feature = "num-complex")))] impl FromPyObject<'_> for Complex<$float> { fn extract_bound(obj: &Bound<'_, PyAny>) -> PyResult> { diff --git a/src/conversions/num_rational.rs b/src/conversions/num_rational.rs index 3843af95344..146e9973119 100644 --- a/src/conversions/num_rational.rs +++ b/src/conversions/num_rational.rs @@ -43,11 +43,14 @@ //! assert fraction + 5 == fraction_plus_five //! ``` +use crate::conversion::IntoPyObject; use crate::ffi; use crate::sync::GILOnceCell; use crate::types::any::PyAnyMethods; use crate::types::PyType; -use crate::{Bound, FromPyObject, IntoPy, Py, PyAny, PyObject, PyResult, Python, ToPyObject}; +use crate::{ + Bound, FromPyObject, IntoPy, Py, PyAny, PyErr, PyObject, PyResult, Python, ToPyObject, +}; #[cfg(feature = "num-bigint")] use num_bigint::BigInt; @@ -82,17 +85,36 @@ macro_rules! rational_conversion { } impl ToPyObject for Ratio<$int> { + #[inline] fn to_object(&self, py: Python<'_>) -> PyObject { - let fraction_cls = get_fraction_cls(py).expect("failed to load fractions.Fraction"); - let ret = fraction_cls - .call1((self.numer().clone(), self.denom().clone())) - .expect("failed to call fractions.Fraction(value)"); - ret.to_object(py) + self.into_pyobject(py).unwrap().into_any().unbind() } } impl IntoPy for Ratio<$int> { + #[inline] fn into_py(self, py: Python<'_>) -> PyObject { - self.to_object(py) + self.into_pyobject(py).unwrap().into_any().unbind() + } + } + + impl<'py> IntoPyObject<'py> for Ratio<$int> { + type Target = PyAny; + type Output = Bound<'py, Self::Target>; + type Error = PyErr; + + #[inline] + fn into_pyobject(self, py: Python<'py>) -> Result { + (&self).into_pyobject(py) + } + } + + impl<'py> IntoPyObject<'py> for &Ratio<$int> { + type Target = PyAny; + type Output = Bound<'py, Self::Target>; + type Error = PyErr; + + fn into_pyobject(self, py: Python<'py>) -> Result { + get_fraction_cls(py)?.call1((self.numer().clone(), self.denom().clone())) } } }; diff --git a/src/conversions/rust_decimal.rs b/src/conversions/rust_decimal.rs index c7056dd8a0e..8dbecad9fa5 100644 --- a/src/conversions/rust_decimal.rs +++ b/src/conversions/rust_decimal.rs @@ -83,22 +83,16 @@ fn get_decimal_cls(py: Python<'_>) -> PyResult<&Bound<'_, PyType>> { } impl ToPyObject for Decimal { + #[inline] fn to_object(&self, py: Python<'_>) -> PyObject { - // TODO: handle error gracefully when ToPyObject can error - // look up the decimal.Decimal - let dec_cls = get_decimal_cls(py).expect("failed to load decimal.Decimal"); - // now call the constructor with the Rust Decimal string-ified - // to not be lossy - let ret = dec_cls - .call1((self.to_string(),)) - .expect("failed to call decimal.Decimal(value)"); - ret.to_object(py) + self.into_pyobject(py).unwrap().into_any().unbind() } } impl IntoPy for Decimal { + #[inline] fn into_py(self, py: Python<'_>) -> PyObject { - self.to_object(py) + self.into_pyobject(py).unwrap().into_any().unbind() } } @@ -115,6 +109,17 @@ impl<'py> IntoPyObject<'py> for Decimal { } } +impl<'py> IntoPyObject<'py> for &Decimal { + type Target = PyAny; + type Output = Bound<'py, Self::Target>; + type Error = PyErr; + + #[inline] + fn into_pyobject(self, py: Python<'py>) -> Result { + (*self).into_pyobject(py) + } +} + #[cfg(test)] mod test_rust_decimal { use super::*; diff --git a/src/conversions/smallvec.rs b/src/conversions/smallvec.rs index 091cbfc48ee..bffa54d00b9 100644 --- a/src/conversions/smallvec.rs +++ b/src/conversions/smallvec.rs @@ -76,6 +76,22 @@ where } } +impl<'a, 'py, A> IntoPyObject<'py> for &'a SmallVec
+where + A: Array, + &'a A::Item: IntoPyObject<'py>, + PyErr: From<<&'a A::Item as IntoPyObject<'py>>::Error>, +{ + type Target = PyAny; + type Output = Bound<'py, Self::Target>; + type Error = PyErr; + + #[inline] + fn into_pyobject(self, py: Python<'py>) -> Result { + self.as_slice().into_pyobject(py) + } +} + impl<'py, A> FromPyObject<'py> for SmallVec where A: Array, diff --git a/src/conversions/std/array.rs b/src/conversions/std/array.rs index 2780868ae04..5645d87cfe5 100644 --- a/src/conversions/std/array.rs +++ b/src/conversions/std/array.rs @@ -56,6 +56,21 @@ where } } +impl<'a, 'py, T, const N: usize> IntoPyObject<'py> for &'a [T; N] +where + &'a T: IntoPyObject<'py>, + PyErr: From<<&'a T as IntoPyObject<'py>>::Error>, +{ + type Target = PyAny; + type Output = Bound<'py, Self::Target>; + type Error = PyErr; + + #[inline] + fn into_pyobject(self, py: Python<'py>) -> Result { + self.as_slice().into_pyobject(py) + } +} + impl ToPyObject for [T; N] where T: ToPyObject, diff --git a/src/conversions/std/cell.rs b/src/conversions/std/cell.rs index c3ea4d97bab..75a8a13a787 100644 --- a/src/conversions/std/cell.rs +++ b/src/conversions/std/cell.rs @@ -22,6 +22,18 @@ impl<'py, T: Copy + IntoPyObject<'py>> IntoPyObject<'py> for Cell { type Output = T::Output; type Error = T::Error; + #[inline] + fn into_pyobject(self, py: Python<'py>) -> Result { + self.get().into_pyobject(py) + } +} + +impl<'a, 'py, T: Copy + IntoPyObject<'py>> IntoPyObject<'py> for &'a Cell { + type Target = T::Target; + type Output = T::Output; + type Error = T::Error; + + #[inline] fn into_pyobject(self, py: Python<'py>) -> Result { self.get().into_pyobject(py) } diff --git a/src/conversions/std/ipaddr.rs b/src/conversions/std/ipaddr.rs index a7fd7fde241..8c308ead3b8 100755 --- a/src/conversions/std/ipaddr.rs +++ b/src/conversions/std/ipaddr.rs @@ -32,14 +32,9 @@ impl FromPyObject<'_> for IpAddr { } impl ToPyObject for Ipv4Addr { + #[inline] fn to_object(&self, py: Python<'_>) -> PyObject { - static IPV4_ADDRESS: GILOnceCell> = GILOnceCell::new(); - IPV4_ADDRESS - .get_or_try_init_type_ref(py, "ipaddress", "IPv4Address") - .expect("failed to load ipaddress.IPv4Address") - .call1((u32::from_be_bytes(self.octets()),)) - .expect("failed to construct ipaddress.IPv4Address") - .unbind() + self.into_pyobject(py).unwrap().unbind() } } @@ -56,15 +51,21 @@ impl<'py> IntoPyObject<'py> for Ipv4Addr { } } +impl<'py> IntoPyObject<'py> for &Ipv4Addr { + type Target = PyAny; + type Output = Bound<'py, Self::Target>; + type Error = PyErr; + + #[inline] + fn into_pyobject(self, py: Python<'py>) -> Result { + (*self).into_pyobject(py) + } +} + impl ToPyObject for Ipv6Addr { + #[inline] fn to_object(&self, py: Python<'_>) -> PyObject { - static IPV6_ADDRESS: GILOnceCell> = GILOnceCell::new(); - IPV6_ADDRESS - .get_or_try_init_type_ref(py, "ipaddress", "IPv6Address") - .expect("failed to load ipaddress.IPv6Address") - .call1((u128::from_be_bytes(self.octets()),)) - .expect("failed to construct ipaddress.IPv6Address") - .unbind() + self.into_pyobject(py).unwrap().unbind() } } @@ -81,18 +82,28 @@ impl<'py> IntoPyObject<'py> for Ipv6Addr { } } +impl<'py> IntoPyObject<'py> for &Ipv6Addr { + type Target = PyAny; + type Output = Bound<'py, Self::Target>; + type Error = PyErr; + + #[inline] + fn into_pyobject(self, py: Python<'py>) -> Result { + (*self).into_pyobject(py) + } +} + impl ToPyObject for IpAddr { + #[inline] fn to_object(&self, py: Python<'_>) -> PyObject { - match self { - IpAddr::V4(ip) => ip.to_object(py), - IpAddr::V6(ip) => ip.to_object(py), - } + self.into_pyobject(py).unwrap().unbind() } } impl IntoPy for IpAddr { + #[inline] fn into_py(self, py: Python<'_>) -> PyObject { - self.to_object(py) + self.into_pyobject(py).unwrap().unbind() } } @@ -109,6 +120,17 @@ impl<'py> IntoPyObject<'py> for IpAddr { } } +impl<'py> IntoPyObject<'py> for &IpAddr { + type Target = PyAny; + type Output = Bound<'py, Self::Target>; + type Error = PyErr; + + #[inline] + fn into_pyobject(self, py: Python<'py>) -> Result { + (*self).into_pyobject(py) + } +} + #[cfg(test)] mod test_ipaddr { use std::str::FromStr; diff --git a/src/conversions/std/map.rs b/src/conversions/std/map.rs index 2a966177693..f19b5671d7a 100644 --- a/src/conversions/std/map.rs +++ b/src/conversions/std/map.rs @@ -51,7 +51,7 @@ where impl<'py, K, V, H> IntoPyObject<'py> for collections::HashMap where - K: hash::Hash + cmp::Eq + IntoPyObject<'py>, + K: IntoPyObject<'py> + cmp::Eq + hash::Hash, V: IntoPyObject<'py>, H: hash::BuildHasher, PyErr: From + From, @@ -72,6 +72,29 @@ where } } +impl<'a, 'py, K, V, H> IntoPyObject<'py> for &'a collections::HashMap +where + &'a K: IntoPyObject<'py> + cmp::Eq + hash::Hash, + &'a V: IntoPyObject<'py>, + &'a H: hash::BuildHasher, + PyErr: From<<&'a K as IntoPyObject<'py>>::Error> + From<<&'a V as IntoPyObject<'py>>::Error>, +{ + type Target = PyDict; + type Output = Bound<'py, Self::Target>; + type Error = PyErr; + + fn into_pyobject(self, py: Python<'py>) -> Result { + let dict = PyDict::new(py); + for (k, v) in self { + dict.set_item( + k.into_pyobject(py)?.into_bound(), + v.into_pyobject(py)?.into_bound(), + )?; + } + Ok(dict) + } +} + impl IntoPy for collections::BTreeMap where K: cmp::Eq + IntoPy, @@ -92,7 +115,7 @@ where impl<'py, K, V> IntoPyObject<'py> for collections::BTreeMap where - K: cmp::Eq + IntoPyObject<'py>, + K: IntoPyObject<'py> + cmp::Eq, V: IntoPyObject<'py>, PyErr: From + From, { @@ -112,6 +135,28 @@ where } } +impl<'a, 'py, K, V> IntoPyObject<'py> for &'a collections::BTreeMap +where + &'a K: IntoPyObject<'py> + cmp::Eq, + &'a V: IntoPyObject<'py>, + PyErr: From<<&'a K as IntoPyObject<'py>>::Error> + From<<&'a V as IntoPyObject<'py>>::Error>, +{ + type Target = PyDict; + type Output = Bound<'py, Self::Target>; + type Error = PyErr; + + fn into_pyobject(self, py: Python<'py>) -> Result { + let dict = PyDict::new(py); + for (k, v) in self { + dict.set_item( + k.into_pyobject(py)?.into_bound(), + v.into_pyobject(py)?.into_bound(), + )?; + } + Ok(dict) + } +} + impl<'py, K, V, S> FromPyObject<'py> for collections::HashMap where K: FromPyObject<'py> + cmp::Eq + hash::Hash, diff --git a/src/conversions/std/num.rs b/src/conversions/std/num.rs index 61c666a1cfe..954aebb14a3 100644 --- a/src/conversions/std/num.rs +++ b/src/conversions/std/num.rs @@ -21,12 +21,13 @@ macro_rules! int_fits_larger_int { impl ToPyObject for $rust_type { #[inline] fn to_object(&self, py: Python<'_>) -> PyObject { - (*self as $larger_type).into_py(py) + self.into_pyobject(py).unwrap().into_any().unbind() } } impl IntoPy for $rust_type { + #[inline] fn into_py(self, py: Python<'_>) -> PyObject { - (self as $larger_type).into_py(py) + self.into_pyobject(py).unwrap().into_any().unbind() } #[cfg(feature = "experimental-inspect")] @@ -90,13 +91,13 @@ macro_rules! int_convert_u64_or_i64 { impl ToPyObject for $rust_type { #[inline] fn to_object(&self, py: Python<'_>) -> PyObject { - unsafe { PyObject::from_owned_ptr(py, $pylong_from_ll_or_ull(*self)) } + self.into_pyobject(py).unwrap().into_any().unbind() } } impl IntoPy for $rust_type { #[inline] fn into_py(self, py: Python<'_>) -> PyObject { - unsafe { PyObject::from_owned_ptr(py, $pylong_from_ll_or_ull(self)) } + self.into_pyobject(py).unwrap().into_any().unbind() } #[cfg(feature = "experimental-inspect")] @@ -117,6 +118,16 @@ macro_rules! int_convert_u64_or_i64 { } } } + impl<'py> IntoPyObject<'py> for &$rust_type { + type Target = PyInt; + type Output = Bound<'py, Self::Target>; + type Error = Infallible; + + #[inline] + fn into_pyobject(self, py: Python<'py>) -> Result { + (*self).into_pyobject(py) + } + } impl FromPyObject<'_> for $rust_type { fn extract_bound(obj: &Bound<'_, PyAny>) -> PyResult<$rust_type> { extract_int!(obj, !0, $pylong_as_ll_or_ull, $force_index_call) @@ -133,13 +144,15 @@ macro_rules! int_convert_u64_or_i64 { macro_rules! int_fits_c_long { ($rust_type:ty) => { impl ToPyObject for $rust_type { + #[inline] fn to_object(&self, py: Python<'_>) -> PyObject { - unsafe { PyObject::from_owned_ptr(py, ffi::PyLong_FromLong(*self as c_long)) } + self.into_pyobject(py).unwrap().into_any().unbind() } } impl IntoPy for $rust_type { + #[inline] fn into_py(self, py: Python<'_>) -> PyObject { - unsafe { PyObject::from_owned_ptr(py, ffi::PyLong_FromLong(self as c_long)) } + self.into_pyobject(py).unwrap().into_any().unbind() } #[cfg(feature = "experimental-inspect")] @@ -167,6 +180,7 @@ macro_rules! int_fits_c_long { type Output = Bound<'py, Self::Target>; type Error = Infallible; + #[inline] fn into_pyobject(self, py: Python<'py>) -> Result { (*self).into_pyobject(py) } @@ -188,13 +202,15 @@ macro_rules! int_fits_c_long { } impl ToPyObject for u8 { + #[inline] fn to_object(&self, py: Python<'_>) -> PyObject { - unsafe { PyObject::from_owned_ptr(py, ffi::PyLong_FromLong(*self as c_long)) } + self.into_pyobject(py).unwrap().into_any().unbind() } } impl IntoPy for u8 { + #[inline] fn into_py(self, py: Python<'_>) -> PyObject { - unsafe { PyObject::from_owned_ptr(py, ffi::PyLong_FromLong(self as c_long)) } + self.into_pyobject(py).unwrap().into_any().unbind() } #[cfg(feature = "experimental-inspect")] fn type_output() -> TypeInfo { @@ -305,23 +321,39 @@ mod fast_128bit_int_conversion { impl ToPyObject for $rust_type { #[inline] fn to_object(&self, py: Python<'_>) -> PyObject { - (*self).into_py(py) + self.into_pyobject(py).unwrap().into_any().unbind() } } impl IntoPy for $rust_type { + #[inline] fn into_py(self, py: Python<'_>) -> PyObject { + self.into_pyobject(py).unwrap().into_any().unbind() + } + + #[cfg(feature = "experimental-inspect")] + fn type_output() -> TypeInfo { + TypeInfo::builtin("int") + } + } + + impl<'py> IntoPyObject<'py> for $rust_type { + type Target = PyInt; + type Output = Bound<'py, Self::Target>; + type Error = Infallible; + + fn into_pyobject(self, py: Python<'py>) -> Result { #[cfg(not(Py_3_13))] { let bytes = self.to_le_bytes(); unsafe { - ffi::_PyLong_FromByteArray( + Ok(ffi::_PyLong_FromByteArray( bytes.as_ptr().cast(), bytes.len(), 1, $is_signed.into(), ) .assume_owned(py) - .unbind() + .downcast_into_unchecked()) } } #[cfg(Py_3_13)] @@ -330,30 +362,37 @@ mod fast_128bit_int_conversion { if $is_signed { unsafe { - ffi::PyLong_FromNativeBytes( + Ok(ffi::PyLong_FromNativeBytes( bytes.as_ptr().cast(), bytes.len(), ffi::Py_ASNATIVEBYTES_NATIVE_ENDIAN, ) .assume_owned(py) + .downcast_into_unchecked()) } } else { unsafe { - ffi::PyLong_FromUnsignedNativeBytes( + Ok(ffi::PyLong_FromUnsignedNativeBytes( bytes.as_ptr().cast(), bytes.len(), ffi::Py_ASNATIVEBYTES_NATIVE_ENDIAN, ) .assume_owned(py) + .downcast_into_unchecked()) } } - .unbind() } } + } - #[cfg(feature = "experimental-inspect")] - fn type_output() -> TypeInfo { - TypeInfo::builtin("int") + impl<'py> IntoPyObject<'py> for &$rust_type { + type Target = PyInt; + type Output = Bound<'py, Self::Target>; + type Error = Infallible; + + #[inline] + fn into_pyobject(self, py: Python<'py>) -> Result { + (*self).into_pyobject(py) } } @@ -428,25 +467,14 @@ mod slow_128bit_int_conversion { impl ToPyObject for $rust_type { #[inline] fn to_object(&self, py: Python<'_>) -> PyObject { - (*self).into_py(py) + self.into_pyobject(py).unwrap().into_any().unbind() } } impl IntoPy for $rust_type { + #[inline] fn into_py(self, py: Python<'_>) -> PyObject { - let lower = (self as u64).into_py(py); - let upper = ((self >> SHIFT) as $half_type).into_py(py); - let shift = SHIFT.into_py(py); - unsafe { - let shifted = PyObject::from_owned_ptr( - py, - ffi::PyNumber_Lshift(upper.as_ptr(), shift.as_ptr()), - ); - PyObject::from_owned_ptr( - py, - ffi::PyNumber_Or(shifted.as_ptr(), lower.as_ptr()), - ) - } + self.into_pyobject(py).unwrap().into_any().unbind() } #[cfg(feature = "experimental-inspect")] @@ -455,6 +483,37 @@ mod slow_128bit_int_conversion { } } + impl<'py> IntoPyObject<'py> for $rust_type { + type Target = PyInt; + type Output = Bound<'py, Self::Target>; + type Error = Infallible; + + fn into_pyobject(self, py: Python<'py>) -> Result { + let lower = (self as u64).into_pyobject(py)?; + let upper = ((self >> SHIFT) as $half_type).into_pyobject(py)?; + let shift = SHIFT.into_pyobject(py)?; + unsafe { + let shifted = + ffi::PyNumber_Lshift(upper.as_ptr(), shift.as_ptr()).assume_owned(py); + + Ok(ffi::PyNumber_Or(shifted.as_ptr(), lower.as_ptr()) + .assume_owned(py) + .downcast_into_unchecked()) + } + } + } + + impl<'py> IntoPyObject<'py> for &$rust_type { + type Target = PyInt; + type Output = Bound<'py, Self::Target>; + type Error = Infallible; + + #[inline] + fn into_pyobject(self, py: Python<'py>) -> Result { + (*self).into_pyobject(py) + } + } + impl FromPyObject<'_> for $rust_type { fn extract_bound(ob: &Bound<'_, PyAny>) -> PyResult<$rust_type> { let py = ob.py(); @@ -503,14 +562,38 @@ fn err_if_invalid_value( macro_rules! nonzero_int_impl { ($nonzero_type:ty, $primitive_type:ty) => { impl ToPyObject for $nonzero_type { + #[inline] fn to_object(&self, py: Python<'_>) -> PyObject { - self.get().to_object(py) + self.into_pyobject(py).unwrap().into_any().unbind() } } impl IntoPy for $nonzero_type { + #[inline] fn into_py(self, py: Python<'_>) -> PyObject { - self.get().into_py(py) + self.into_pyobject(py).unwrap().into_any().unbind() + } + } + + impl<'py> IntoPyObject<'py> for $nonzero_type { + type Target = PyInt; + type Output = Bound<'py, Self::Target>; + type Error = Infallible; + + #[inline] + fn into_pyobject(self, py: Python<'py>) -> Result { + self.get().into_pyobject(py) + } + } + + impl<'py> IntoPyObject<'py> for &$nonzero_type { + type Target = PyInt; + type Output = Bound<'py, Self::Target>; + type Error = Infallible; + + #[inline] + fn into_pyobject(self, py: Python<'py>) -> Result { + (*self).into_pyobject(py) } } diff --git a/src/conversions/std/option.rs b/src/conversions/std/option.rs index 2ef00acb550..38eaebd499b 100644 --- a/src/conversions/std/option.rs +++ b/src/conversions/std/option.rs @@ -44,6 +44,20 @@ where } } +impl<'a, 'py, T> IntoPyObject<'py> for &'a Option +where + &'a T: IntoPyObject<'py>, +{ + type Target = PyAny; + type Output = Bound<'py, Self::Target>; + type Error = <&'a T as IntoPyObject<'py>>::Error; + + #[inline] + fn into_pyobject(self, py: Python<'py>) -> Result { + self.as_ref().into_pyobject(py) + } +} + impl<'py, T> FromPyObject<'py> for Option where T: FromPyObject<'py>, diff --git a/src/conversions/std/osstr.rs b/src/conversions/std/osstr.rs index 3904010d35a..c140ef703d4 100644 --- a/src/conversions/std/osstr.rs +++ b/src/conversions/std/osstr.rs @@ -9,6 +9,7 @@ use std::convert::Infallible; use std::ffi::{OsStr, OsString}; impl ToPyObject for OsStr { + #[inline] fn to_object(&self, py: Python<'_>) -> PyObject { self.into_pyobject(py).unwrap().into_any().unbind() } @@ -122,21 +123,21 @@ impl FromPyObject<'_> for OsString { impl IntoPy for &'_ OsStr { #[inline] fn into_py(self, py: Python<'_>) -> PyObject { - self.to_object(py) + self.into_pyobject(py).unwrap().into_any().unbind() } } impl ToPyObject for Cow<'_, OsStr> { #[inline] fn to_object(&self, py: Python<'_>) -> PyObject { - (self as &OsStr).to_object(py) + self.into_pyobject(py).unwrap().into_any().unbind() } } impl IntoPy for Cow<'_, OsStr> { #[inline] fn into_py(self, py: Python<'_>) -> PyObject { - self.to_object(py) + self.into_pyobject(py).unwrap().into_any().unbind() } } @@ -145,21 +146,34 @@ impl<'py> IntoPyObject<'py> for Cow<'_, OsStr> { type Output = Bound<'py, Self::Target>; type Error = Infallible; + #[inline] fn into_pyobject(self, py: Python<'py>) -> Result { (*self).into_pyobject(py) } } +impl<'py> IntoPyObject<'py> for &Cow<'_, OsStr> { + type Target = PyString; + type Output = Bound<'py, Self::Target>; + type Error = Infallible; + + #[inline] + fn into_pyobject(self, py: Python<'py>) -> Result { + (&**self).into_pyobject(py) + } +} + impl ToPyObject for OsString { #[inline] fn to_object(&self, py: Python<'_>) -> PyObject { - (self as &OsStr).to_object(py) + self.into_pyobject(py).unwrap().into_any().unbind() } } impl IntoPy for OsString { + #[inline] fn into_py(self, py: Python<'_>) -> PyObject { - self.to_object(py) + self.into_pyobject(py).unwrap().into_any().unbind() } } @@ -168,14 +182,16 @@ impl<'py> IntoPyObject<'py> for OsString { type Output = Bound<'py, Self::Target>; type Error = Infallible; + #[inline] fn into_pyobject(self, py: Python<'py>) -> Result { self.as_os_str().into_pyobject(py) } } impl<'a> IntoPy for &'a OsString { + #[inline] fn into_py(self, py: Python<'_>) -> PyObject { - self.to_object(py) + self.into_pyobject(py).unwrap().into_any().unbind() } } @@ -184,6 +200,7 @@ impl<'py> IntoPyObject<'py> for &OsString { type Output = Bound<'py, Self::Target>; type Error = Infallible; + #[inline] fn into_pyobject(self, py: Python<'py>) -> Result { self.as_os_str().into_pyobject(py) } diff --git a/src/conversions/std/path.rs b/src/conversions/std/path.rs index 1540c852f05..758c4bbe198 100644 --- a/src/conversions/std/path.rs +++ b/src/conversions/std/path.rs @@ -10,8 +10,9 @@ use std::ffi::OsString; use std::path::{Path, PathBuf}; impl ToPyObject for Path { + #[inline] fn to_object(&self, py: Python<'_>) -> PyObject { - self.as_os_str().to_object(py) + self.into_pyobject(py).unwrap().into_any().unbind() } } @@ -28,7 +29,7 @@ impl FromPyObject<'_> for PathBuf { impl<'a> IntoPy for &'a Path { #[inline] fn into_py(self, py: Python<'_>) -> PyObject { - self.as_os_str().to_object(py) + self.into_pyobject(py).unwrap().into_any().unbind() } } @@ -37,6 +38,7 @@ impl<'py> IntoPyObject<'py> for &Path { type Output = Bound<'py, Self::Target>; type Error = Infallible; + #[inline] fn into_pyobject(self, py: Python<'py>) -> Result { self.as_os_str().into_pyobject(py) } @@ -45,14 +47,14 @@ impl<'py> IntoPyObject<'py> for &Path { impl<'a> ToPyObject for Cow<'a, Path> { #[inline] fn to_object(&self, py: Python<'_>) -> PyObject { - self.as_os_str().to_object(py) + self.into_pyobject(py).unwrap().into_any().unbind() } } impl<'a> IntoPy for Cow<'a, Path> { #[inline] fn into_py(self, py: Python<'_>) -> PyObject { - self.to_object(py) + self.into_pyobject(py).unwrap().into_any().unbind() } } @@ -61,6 +63,18 @@ impl<'py> IntoPyObject<'py> for Cow<'_, Path> { type Output = Bound<'py, Self::Target>; type Error = Infallible; + #[inline] + fn into_pyobject(self, py: Python<'py>) -> Result { + self.as_os_str().into_pyobject(py) + } +} + +impl<'py> IntoPyObject<'py> for &Cow<'_, Path> { + type Target = PyString; + type Output = Bound<'py, Self::Target>; + type Error = Infallible; + + #[inline] fn into_pyobject(self, py: Python<'py>) -> Result { self.as_os_str().into_pyobject(py) } @@ -69,13 +83,14 @@ impl<'py> IntoPyObject<'py> for Cow<'_, Path> { impl ToPyObject for PathBuf { #[inline] fn to_object(&self, py: Python<'_>) -> PyObject { - self.as_os_str().to_object(py) + self.into_pyobject(py).unwrap().into_any().unbind() } } impl IntoPy for PathBuf { + #[inline] fn into_py(self, py: Python<'_>) -> PyObject { - self.into_os_string().to_object(py) + self.into_pyobject(py).unwrap().into_any().unbind() } } @@ -84,14 +99,16 @@ impl<'py> IntoPyObject<'py> for PathBuf { type Output = Bound<'py, Self::Target>; type Error = Infallible; + #[inline] fn into_pyobject(self, py: Python<'py>) -> Result { self.as_os_str().into_pyobject(py) } } impl<'a> IntoPy for &'a PathBuf { + #[inline] fn into_py(self, py: Python<'_>) -> PyObject { - self.as_os_str().to_object(py) + self.into_pyobject(py).unwrap().into_any().unbind() } } @@ -100,6 +117,7 @@ impl<'py> IntoPyObject<'py> for &PathBuf { type Output = Bound<'py, Self::Target>; type Error = Infallible; + #[inline] fn into_pyobject(self, py: Python<'py>) -> Result { self.as_os_str().into_pyobject(py) } diff --git a/src/conversions/std/set.rs b/src/conversions/std/set.rs index 165194bbafc..b15013b81ba 100644 --- a/src/conversions/std/set.rs +++ b/src/conversions/std/set.rs @@ -77,6 +77,29 @@ where } } +impl<'a, 'py, K, H> IntoPyObject<'py> for &'a collections::HashSet +where + &'a K: IntoPyObject<'py> + Eq + hash::Hash, + &'a H: hash::BuildHasher, + PyErr: From<<&'a K as IntoPyObject<'py>>::Error>, +{ + type Target = PySet; + type Output = Bound<'py, Self::Target>; + type Error = PyErr; + + fn into_pyobject(self, py: Python<'py>) -> Result { + try_new_from_iter( + py, + self.iter().map(|item| { + item.into_pyobject(py) + .map(BoundObject::into_any) + .map(BoundObject::unbind) + .map_err(Into::into) + }), + ) + } +} + impl<'py, K, S> FromPyObject<'py> for collections::HashSet where K: FromPyObject<'py> + cmp::Eq + hash::Hash, @@ -119,7 +142,7 @@ where impl<'py, K> IntoPyObject<'py> for collections::BTreeSet where - K: IntoPyObject<'py> + Eq + hash::Hash, + K: IntoPyObject<'py> + cmp::Ord, PyErr: From, { type Target = PySet; @@ -139,6 +162,28 @@ where } } +impl<'a, 'py, K> IntoPyObject<'py> for &'a collections::BTreeSet +where + &'a K: IntoPyObject<'py> + cmp::Ord, + PyErr: From<<&'a K as IntoPyObject<'py>>::Error>, +{ + type Target = PySet; + type Output = Bound<'py, Self::Target>; + type Error = PyErr; + + fn into_pyobject(self, py: Python<'py>) -> Result { + try_new_from_iter( + py, + self.iter().map(|item| { + item.into_pyobject(py) + .map(BoundObject::into_any) + .map(BoundObject::unbind) + .map_err(Into::into) + }), + ) + } +} + impl<'py, K> FromPyObject<'py> for collections::BTreeSet where K: FromPyObject<'py> + cmp::Ord, diff --git a/src/conversions/std/string.rs b/src/conversions/std/string.rs index 92d0f13babe..02688641e78 100644 --- a/src/conversions/std/string.rs +++ b/src/conversions/std/string.rs @@ -14,14 +14,14 @@ use crate::{ impl ToPyObject for str { #[inline] fn to_object(&self, py: Python<'_>) -> PyObject { - PyString::new(py, self).into() + self.into_pyobject(py).unwrap().into_any().unbind() } } impl<'a> IntoPy for &'a str { #[inline] fn into_py(self, py: Python<'_>) -> PyObject { - PyString::new(py, self).into() + self.into_pyobject(py).unwrap().into_any().unbind() } #[cfg(feature = "experimental-inspect")] @@ -33,7 +33,7 @@ impl<'a> IntoPy for &'a str { impl<'a> IntoPy> for &'a str { #[inline] fn into_py(self, py: Python<'_>) -> Py { - PyString::new(py, self).into() + self.into_pyobject(py).unwrap().unbind() } #[cfg(feature = "experimental-inspect")] @@ -47,24 +47,36 @@ impl<'py> IntoPyObject<'py> for &str { type Output = Bound<'py, Self::Target>; type Error = Infallible; + #[inline] fn into_pyobject(self, py: Python<'py>) -> Result { Ok(PyString::new(py, self)) } } +impl<'py> IntoPyObject<'py> for &&str { + type Target = PyString; + type Output = Bound<'py, Self::Target>; + type Error = Infallible; + + #[inline] + fn into_pyobject(self, py: Python<'py>) -> Result { + (*self).into_pyobject(py) + } +} + /// Converts a Rust `Cow<'_, str>` to a Python object. /// See `PyString::new` for details on the conversion. impl ToPyObject for Cow<'_, str> { #[inline] fn to_object(&self, py: Python<'_>) -> PyObject { - PyString::new(py, self).into() + self.into_pyobject(py).unwrap().into_any().unbind() } } impl IntoPy for Cow<'_, str> { #[inline] fn into_py(self, py: Python<'_>) -> PyObject { - self.to_object(py) + self.into_pyobject(py).unwrap().into_any().unbind() } #[cfg(feature = "experimental-inspect")] @@ -78,30 +90,43 @@ impl<'py> IntoPyObject<'py> for Cow<'_, str> { type Output = Bound<'py, Self::Target>; type Error = Infallible; + #[inline] fn into_pyobject(self, py: Python<'py>) -> Result { (*self).into_pyobject(py) } } +impl<'py> IntoPyObject<'py> for &Cow<'_, str> { + type Target = PyString; + type Output = Bound<'py, Self::Target>; + type Error = Infallible; + + #[inline] + fn into_pyobject(self, py: Python<'py>) -> Result { + (&**self).into_pyobject(py) + } +} + /// Converts a Rust `String` to a Python object. /// See `PyString::new` for details on the conversion. impl ToPyObject for String { #[inline] fn to_object(&self, py: Python<'_>) -> PyObject { - PyString::new(py, self).into() + self.into_pyobject(py).unwrap().into_any().unbind() } } impl ToPyObject for char { + #[inline] fn to_object(&self, py: Python<'_>) -> PyObject { - self.into_py(py) + self.into_pyobject(py).unwrap().into_any().unbind() } } impl IntoPy for char { + #[inline] fn into_py(self, py: Python<'_>) -> PyObject { - let mut bytes = [0u8; 4]; - PyString::new(py, self.encode_utf8(&mut bytes)).into() + self.into_pyobject(py).unwrap().into_any().unbind() } #[cfg(feature = "experimental-inspect")] @@ -121,9 +146,21 @@ impl<'py> IntoPyObject<'py> for char { } } +impl<'py> IntoPyObject<'py> for &char { + type Target = PyString; + type Output = Bound<'py, Self::Target>; + type Error = Infallible; + + #[inline] + fn into_pyobject(self, py: Python<'py>) -> Result { + (*self).into_pyobject(py) + } +} + impl IntoPy for String { + #[inline] fn into_py(self, py: Python<'_>) -> PyObject { - PyString::new(py, &self).into() + self.into_pyobject(py).unwrap().into_any().unbind() } #[cfg(feature = "experimental-inspect")] @@ -145,7 +182,7 @@ impl<'py> IntoPyObject<'py> for String { impl<'a> IntoPy for &'a String { #[inline] fn into_py(self, py: Python<'_>) -> PyObject { - PyString::new(py, self).into() + self.into_pyobject(py).unwrap().into_any().unbind() } #[cfg(feature = "experimental-inspect")] @@ -159,6 +196,7 @@ impl<'py> IntoPyObject<'py> for &String { type Output = Bound<'py, Self::Target>; type Error = Infallible; + #[inline] fn into_pyobject(self, py: Python<'py>) -> Result { Ok(PyString::new(py, self)) } diff --git a/src/conversions/std/time.rs b/src/conversions/std/time.rs index 4c5c56e4cac..5623297c161 100755 --- a/src/conversions/std/time.rs +++ b/src/conversions/std/time.rs @@ -53,40 +53,16 @@ impl FromPyObject<'_> for Duration { } impl ToPyObject for Duration { + #[inline] fn to_object(&self, py: Python<'_>) -> PyObject { - let days = self.as_secs() / SECONDS_PER_DAY; - let seconds = self.as_secs() % SECONDS_PER_DAY; - let microseconds = self.subsec_micros(); - - #[cfg(not(Py_LIMITED_API))] - { - PyDelta::new_bound( - py, - days.try_into() - .expect("Too large Rust duration for timedelta"), - seconds.try_into().unwrap(), - microseconds.try_into().unwrap(), - false, - ) - .expect("failed to construct timedelta (overflow?)") - .into() - } - #[cfg(Py_LIMITED_API)] - { - static TIMEDELTA: GILOnceCell> = GILOnceCell::new(); - TIMEDELTA - .get_or_try_init_type_ref(py, "datetime", "timedelta") - .unwrap() - .call1((days, seconds, microseconds)) - .unwrap() - .into() - } + self.into_pyobject(py).unwrap().into_any().unbind() } } impl IntoPy for Duration { + #[inline] fn into_py(self, py: Python<'_>) -> PyObject { - self.to_object(py) + self.into_pyobject(py).unwrap().into_any().unbind() } } @@ -123,6 +99,20 @@ impl<'py> IntoPyObject<'py> for Duration { } } +impl<'py> IntoPyObject<'py> for &Duration { + #[cfg(not(Py_LIMITED_API))] + type Target = PyDelta; + #[cfg(Py_LIMITED_API)] + type Target = PyAny; + type Output = Bound<'py, Self::Target>; + type Error = PyErr; + + #[inline] + fn into_pyobject(self, py: Python<'py>) -> Result { + (*self).into_pyobject(py) + } +} + // Conversions between SystemTime and datetime do not rely on the floating point timestamp of the // timestamp/fromtimestamp APIs to avoid possible precision loss but goes through the // timedelta/std::time::Duration types by taking for reference point the UNIX epoch. @@ -132,7 +122,7 @@ impl<'py> IntoPyObject<'py> for Duration { impl FromPyObject<'_> for SystemTime { fn extract_bound(obj: &Bound<'_, PyAny>) -> PyResult { let duration_since_unix_epoch: Duration = obj - .call_method1(intern!(obj.py(), "__sub__"), (unix_epoch_py(obj.py()),))? + .call_method1(intern!(obj.py(), "__sub__"), (unix_epoch_py(obj.py())?,))? .extract()?; UNIX_EPOCH .checked_add(duration_since_unix_epoch) @@ -143,17 +133,16 @@ impl FromPyObject<'_> for SystemTime { } impl ToPyObject for SystemTime { + #[inline] fn to_object(&self, py: Python<'_>) -> PyObject { - let duration_since_unix_epoch = self.duration_since(UNIX_EPOCH).unwrap().into_py(py); - unix_epoch_py(py) - .call_method1(py, intern!(py, "__add__"), (duration_since_unix_epoch,)) - .unwrap() + self.into_pyobject(py).unwrap().into_any().unbind() } } impl IntoPy for SystemTime { + #[inline] fn into_py(self, py: Python<'_>) -> PyObject { - self.to_object(py) + self.into_pyobject(py).unwrap().into_any().unbind() } } @@ -165,47 +154,46 @@ impl<'py> IntoPyObject<'py> for SystemTime { fn into_pyobject(self, py: Python<'py>) -> Result { let duration_since_unix_epoch = self.duration_since(UNIX_EPOCH).unwrap().into_pyobject(py)?; - unix_epoch_py(py) + unix_epoch_py(py)? .bind(py) .call_method1(intern!(py, "__add__"), (duration_since_unix_epoch,)) } } -fn unix_epoch_py(py: Python<'_>) -> &PyObject { +impl<'py> IntoPyObject<'py> for &SystemTime { + type Target = PyAny; + type Output = Bound<'py, Self::Target>; + type Error = PyErr; + + #[inline] + fn into_pyobject(self, py: Python<'py>) -> Result { + (*self).into_pyobject(py) + } +} + +fn unix_epoch_py(py: Python<'_>) -> PyResult<&PyObject> { static UNIX_EPOCH: GILOnceCell = GILOnceCell::new(); - UNIX_EPOCH - .get_or_try_init(py, || { - #[cfg(not(Py_LIMITED_API))] - { - Ok::<_, PyErr>( - PyDateTime::new_bound( - py, - 1970, - 1, - 1, - 0, - 0, - 0, - 0, - Some(&timezone_utc_bound(py)), - )? + UNIX_EPOCH.get_or_try_init(py, || { + #[cfg(not(Py_LIMITED_API))] + { + Ok::<_, PyErr>( + PyDateTime::new_bound(py, 1970, 1, 1, 0, 0, 0, 0, Some(&timezone_utc_bound(py)))? .into(), - ) - } - #[cfg(Py_LIMITED_API)] - { - let datetime = py.import("datetime")?; - let utc = datetime.getattr("timezone")?.getattr("utc")?; - Ok::<_, PyErr>( - datetime - .getattr("datetime")? - .call1((1970, 1, 1, 0, 0, 0, 0, utc)) - .unwrap() - .into(), - ) - } - }) - .unwrap() + ) + } + #[cfg(Py_LIMITED_API)] + { + let datetime = py.import("datetime")?; + let utc = datetime.getattr("timezone")?.getattr("utc")?; + Ok::<_, PyErr>( + datetime + .getattr("datetime")? + .call1((1970, 1, 1, 0, 0, 0, 0, utc)) + .unwrap() + .into(), + ) + } + }) } #[cfg(test)] diff --git a/src/err/mod.rs b/src/err/mod.rs index bb5c80f2ca0..68d207c8fb8 100644 --- a/src/err/mod.rs +++ b/src/err/mod.rs @@ -15,8 +15,10 @@ use std::ffi::CString; mod err_state; mod impls; +use crate::conversion::IntoPyObject; pub use err_state::PyErrArguments; use err_state::{PyErrState, PyErrStateLazyFnOutput, PyErrStateNormalized}; +use std::convert::Infallible; /// Represents a Python exception. /// @@ -783,20 +785,45 @@ impl std::fmt::Display for PyErr { impl std::error::Error for PyErr {} impl IntoPy for PyErr { + #[inline] fn into_py(self, py: Python<'_>) -> PyObject { - self.into_value(py).into() + self.into_pyobject(py).unwrap().into_any().unbind() } } impl ToPyObject for PyErr { + #[inline] fn to_object(&self, py: Python<'_>) -> PyObject { - self.clone_ref(py).into_py(py) + self.into_pyobject(py).unwrap().into_any().unbind() } } impl<'a> IntoPy for &'a PyErr { + #[inline] fn into_py(self, py: Python<'_>) -> PyObject { - self.clone_ref(py).into_py(py) + self.into_pyobject(py).unwrap().into_any().unbind() + } +} + +impl<'py> IntoPyObject<'py> for PyErr { + type Target = PyBaseException; + type Output = Bound<'py, Self::Target>; + type Error = Infallible; + + #[inline] + fn into_pyobject(self, py: Python<'py>) -> Result { + Ok(self.into_value(py).into_bound(py)) + } +} + +impl<'py> IntoPyObject<'py> for &PyErr { + type Target = PyBaseException; + type Output = Bound<'py, Self::Target>; + type Error = Infallible; + + #[inline] + fn into_pyobject(self, py: Python<'py>) -> Result { + self.clone_ref(py).into_pyobject(py) } } diff --git a/src/types/boolobject.rs b/src/types/boolobject.rs index e678ca2c601..8f211a27a54 100644 --- a/src/types/boolobject.rs +++ b/src/types/boolobject.rs @@ -7,6 +7,9 @@ use crate::{ }; use super::any::PyAnyMethods; +use crate::conversion::IntoPyObject; +use crate::BoundObject; +use std::convert::Infallible; /// Represents a Python `bool`. /// @@ -145,23 +148,14 @@ impl PartialEq> for &'_ bool { impl ToPyObject for bool { #[inline] fn to_object(&self, py: Python<'_>) -> PyObject { - unsafe { - PyObject::from_borrowed_ptr( - py, - if *self { - ffi::Py_True() - } else { - ffi::Py_False() - }, - ) - } + self.into_pyobject(py).unwrap().into_any().unbind() } } impl IntoPy for bool { #[inline] fn into_py(self, py: Python<'_>) -> PyObject { - PyBool::new(py, self).into_py(py) + self.into_pyobject(py).unwrap().into_any().unbind() } #[cfg(feature = "experimental-inspect")] @@ -170,6 +164,28 @@ impl IntoPy for bool { } } +impl<'py> IntoPyObject<'py> for bool { + type Target = PyBool; + type Output = Borrowed<'py, 'py, Self::Target>; + type Error = Infallible; + + #[inline] + fn into_pyobject(self, py: Python<'py>) -> Result { + Ok(PyBool::new(py, self)) + } +} + +impl<'py> IntoPyObject<'py> for &bool { + type Target = PyBool; + type Output = Borrowed<'py, 'py, Self::Target>; + type Error = Infallible; + + #[inline] + fn into_pyobject(self, py: Python<'py>) -> Result { + (*self).into_pyobject(py) + } +} + /// Converts a Python `bool` to a Rust `bool`. /// /// Fails with `TypeError` if the input is not a Python `bool`. diff --git a/src/types/float.rs b/src/types/float.rs index 70c5e1cd9e3..5e637af3b62 100644 --- a/src/types/float.rs +++ b/src/types/float.rs @@ -1,10 +1,12 @@ use super::any::PyAnyMethods; +use crate::conversion::IntoPyObject; #[cfg(feature = "experimental-inspect")] use crate::inspect::types::TypeInfo; use crate::{ ffi, ffi_ptr_ext::FfiPtrExt, instance::Bound, Borrowed, FromPyObject, IntoPy, PyAny, PyErr, PyObject, PyResult, Python, ToPyObject, }; +use std::convert::Infallible; use std::os::raw::c_double; /// Represents a Python `float` object. @@ -73,14 +75,16 @@ impl<'py> PyFloatMethods<'py> for Bound<'py, PyFloat> { } impl ToPyObject for f64 { + #[inline] fn to_object(&self, py: Python<'_>) -> PyObject { - PyFloat::new(py, *self).into() + self.into_pyobject(py).unwrap().into_any().unbind() } } impl IntoPy for f64 { + #[inline] fn into_py(self, py: Python<'_>) -> PyObject { - PyFloat::new(py, self).into() + self.into_pyobject(py).unwrap().into_any().unbind() } #[cfg(feature = "experimental-inspect")] @@ -89,6 +93,28 @@ impl IntoPy for f64 { } } +impl<'py> IntoPyObject<'py> for f64 { + type Target = PyFloat; + type Output = Bound<'py, Self::Target>; + type Error = Infallible; + + #[inline] + fn into_pyobject(self, py: Python<'py>) -> Result { + Ok(PyFloat::new(py, self)) + } +} + +impl<'py> IntoPyObject<'py> for &f64 { + type Target = PyFloat; + type Output = Bound<'py, Self::Target>; + type Error = Infallible; + + #[inline] + fn into_pyobject(self, py: Python<'py>) -> Result { + (*self).into_pyobject(py) + } +} + impl<'py> FromPyObject<'py> for f64 { // PyFloat_AsDouble returns -1.0 upon failure #![allow(clippy::float_cmp)] @@ -120,14 +146,16 @@ impl<'py> FromPyObject<'py> for f64 { } impl ToPyObject for f32 { + #[inline] fn to_object(&self, py: Python<'_>) -> PyObject { - PyFloat::new(py, f64::from(*self)).into() + self.into_pyobject(py).unwrap().into_any().unbind() } } impl IntoPy for f32 { + #[inline] fn into_py(self, py: Python<'_>) -> PyObject { - PyFloat::new(py, f64::from(self)).into() + self.into_pyobject(py).unwrap().into_any().unbind() } #[cfg(feature = "experimental-inspect")] @@ -136,6 +164,28 @@ impl IntoPy for f32 { } } +impl<'py> IntoPyObject<'py> for f32 { + type Target = PyFloat; + type Output = Bound<'py, Self::Target>; + type Error = Infallible; + + #[inline] + fn into_pyobject(self, py: Python<'py>) -> Result { + Ok(PyFloat::new(py, self.into())) + } +} + +impl<'py> IntoPyObject<'py> for &f32 { + type Target = PyFloat; + type Output = Bound<'py, Self::Target>; + type Error = Infallible; + + #[inline] + fn into_pyobject(self, py: Python<'py>) -> Result { + (*self).into_pyobject(py) + } +} + impl<'py> FromPyObject<'py> for f32 { fn extract_bound(obj: &Bound<'py, PyAny>) -> PyResult { Ok(obj.extract::()? as f32) diff --git a/tests/ui/missing_intopy.stderr b/tests/ui/missing_intopy.stderr index 58ddbff0f22..f7dcca419bf 100644 --- a/tests/ui/missing_intopy.stderr +++ b/tests/ui/missing_intopy.stderr @@ -8,14 +8,14 @@ error[E0277]: `Blah` cannot be converted to a Python object = note: if you do not wish to have a corresponding Python type, implement it manually = note: if you do not own `Blah` you can perform a manual conversion to one of the types in `pyo3::types::*` = help: the following other types implement trait `IntoPyObject<'py>`: + &&str + &'a BTreeMap + &'a BTreeSet + &'a Cell + &'a HashMap + &'a HashSet + &'a Option &'a Py - &'a PyRef<'py, T> - &'a PyRefMut<'py, T> - &'a Vec - &'a [T] - &'a pyo3::Bound<'py, T> - &OsStr - &OsString and $N others note: required by a bound in `UnknownReturnType::::wrap` --> src/impl_/wrap.rs From 4211c575bf199cf570c2b21491b71c450d2f5f37 Mon Sep 17 00:00:00 2001 From: Sanskar Jethi <29942790+sansyrox@users.noreply.github.com> Date: Sat, 17 Aug 2024 22:42:35 +0100 Subject: [PATCH 219/495] docs: Add robyn in the list of examples (#4448) --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 63c15846d88..6cc9be4d6a9 100644 --- a/README.md +++ b/README.md @@ -217,6 +217,7 @@ about this topic. - [primp](https://github.com/deedy5/primp) _The fastest python HTTP client that can impersonate web browsers by mimicking their headers and TLS/JA3/JA4/HTTP2 fingerprints._ - [ril-py](https://github.com/Cryptex-github/ril-py) _A performant and high-level image processing library for Python written in Rust._ - [river](https://github.com/online-ml/river) _Online machine learning in python, the computationally heavy statistics algorithms are implemented in Rust._ +- [robyn](https://github.com/sparckles/Robyn) A Super Fast Async Python Web Framework with a Rust runtime. - [rust-python-coverage](https://github.com/cjermain/rust-python-coverage) _Example PyO3 project with automated test coverage for Rust and Python._ - [tiktoken](https://github.com/openai/tiktoken) _A fast BPE tokeniser for use with OpenAI's models._ - [tokenizers](https://github.com/huggingface/tokenizers/tree/main/bindings/python) _Python bindings to the Hugging Face tokenizers (NLP) written in Rust._ From 57d85b1f8b511d1e3daa16141f266eeca2d7eadd Mon Sep 17 00:00:00 2001 From: Emanuele Giaquinta Date: Sun, 18 Aug 2024 11:39:38 +0300 Subject: [PATCH 220/495] Restore _PyLong_NumBits on Python 3.13 and later (#4450) See https://github.com/python/cpython/commit/cc663b7e25539d938bf71bc1b4d69efd8824c2d3 --- newsfragments/4450.changed.md | 1 + pyo3-ffi/src/longobject.rs | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 newsfragments/4450.changed.md diff --git a/newsfragments/4450.changed.md b/newsfragments/4450.changed.md new file mode 100644 index 00000000000..efc02fc3032 --- /dev/null +++ b/newsfragments/4450.changed.md @@ -0,0 +1 @@ +Restore `_PyLong_NumBits` on Python 3.13 and later diff --git a/pyo3-ffi/src/longobject.rs b/pyo3-ffi/src/longobject.rs index 35a2bc1b0ff..25fa12dbbfc 100644 --- a/pyo3-ffi/src/longobject.rs +++ b/pyo3-ffi/src/longobject.rs @@ -92,7 +92,6 @@ extern "C" { #[cfg(not(Py_LIMITED_API))] extern "C" { #[cfg_attr(PyPy, link_name = "_PyPyLong_NumBits")] - #[cfg(not(Py_3_13))] pub fn _PyLong_NumBits(obj: *mut PyObject) -> size_t; } From 06df472a9434c2fd5f1123d1b681614a4ebb15de Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Tue, 20 Aug 2024 23:50:01 +0200 Subject: [PATCH 221/495] Reintroduces `datetime` constructors (#4457) --- pytests/src/datetime.rs | 20 ++-- src/conversions/chrono.rs | 33 +++--- src/conversions/std/time.rs | 7 +- src/ffi/tests.rs | 16 +-- src/types/datetime.rs | 204 ++++++++++++++++++++++++++++------ src/types/mod.rs | 5 +- tests/test_datetime.rs | 12 +- tests/test_datetime_import.rs | 2 +- 8 files changed, 217 insertions(+), 82 deletions(-) diff --git a/pytests/src/datetime.rs b/pytests/src/datetime.rs index 4e505caa0f8..744be279431 100644 --- a/pytests/src/datetime.rs +++ b/pytests/src/datetime.rs @@ -8,7 +8,7 @@ use pyo3::types::{ #[pyfunction] fn make_date(py: Python<'_>, year: i32, month: u8, day: u8) -> PyResult> { - PyDate::new_bound(py, year, month, day) + PyDate::new(py, year, month, day) } #[pyfunction] @@ -21,7 +21,7 @@ fn get_date_tuple<'py>(d: &Bound<'py, PyDate>) -> Bound<'py, PyTuple> { #[pyfunction] fn date_from_timestamp(py: Python<'_>, timestamp: i64) -> PyResult> { - PyDate::from_timestamp_bound(py, timestamp) + PyDate::from_timestamp(py, timestamp) } #[pyfunction] @@ -34,7 +34,7 @@ fn make_time<'py>( microsecond: u32, tzinfo: Option<&Bound<'py, PyTzInfo>>, ) -> PyResult> { - PyTime::new_bound(py, hour, minute, second, microsecond, tzinfo) + PyTime::new(py, hour, minute, second, microsecond, tzinfo) } #[pyfunction] @@ -48,7 +48,7 @@ fn time_with_fold<'py>( tzinfo: Option<&Bound<'py, PyTzInfo>>, fold: bool, ) -> PyResult> { - PyTime::new_bound_with_fold(py, hour, minute, second, microsecond, tzinfo, fold) + PyTime::new_with_fold(py, hour, minute, second, microsecond, tzinfo, fold) } #[pyfunction] @@ -85,7 +85,7 @@ fn make_delta( seconds: i32, microseconds: i32, ) -> PyResult> { - PyDelta::new_bound(py, days, seconds, microseconds, true) + PyDelta::new(py, days, seconds, microseconds, true) } #[pyfunction] @@ -114,7 +114,7 @@ fn make_datetime<'py>( microsecond: u32, tzinfo: Option<&Bound<'py, PyTzInfo>>, ) -> PyResult> { - PyDateTime::new_bound( + PyDateTime::new( py, year, month, @@ -167,17 +167,17 @@ fn datetime_from_timestamp<'py>( ts: f64, tz: Option<&Bound<'py, PyTzInfo>>, ) -> PyResult> { - PyDateTime::from_timestamp_bound(py, ts, tz) + PyDateTime::from_timestamp(py, ts, tz) } #[pyfunction] fn get_datetime_tzinfo<'py>(dt: &Bound<'py, PyDateTime>) -> Option> { - dt.get_tzinfo_bound() + dt.get_tzinfo() } #[pyfunction] fn get_time_tzinfo<'py>(dt: &Bound<'py, PyTime>) -> Option> { - dt.get_tzinfo_bound() + dt.get_tzinfo() } #[pyclass(extends=PyTzInfo)] @@ -191,7 +191,7 @@ impl TzClass { } fn utcoffset<'py>(&self, dt: &Bound<'py, PyDateTime>) -> PyResult> { - PyDelta::new_bound(dt.py(), 0, 3600, 0, true) + PyDelta::new(dt.py(), 0, 3600, 0, true) } fn tzname(&self, _dt: &Bound<'_, PyDateTime>) -> String { diff --git a/src/conversions/chrono.rs b/src/conversions/chrono.rs index 44cee349c2f..48246d827db 100644 --- a/src/conversions/chrono.rs +++ b/src/conversions/chrono.rs @@ -49,8 +49,8 @@ use crate::types::any::PyAnyMethods; use crate::types::datetime::timezone_from_offset; #[cfg(not(Py_LIMITED_API))] use crate::types::{ - timezone_utc_bound, PyDate, PyDateAccess, PyDateTime, PyDelta, PyDeltaAccess, PyTime, - PyTimeAccess, PyTzInfo, PyTzInfoAccess, + timezone_utc, PyDate, PyDateAccess, PyDateTime, PyDelta, PyDeltaAccess, PyTime, PyTimeAccess, + PyTzInfo, PyTzInfoAccess, }; #[cfg(Py_LIMITED_API)] use crate::{intern, DowncastError}; @@ -102,7 +102,7 @@ impl<'py> IntoPyObject<'py> for Duration { // We pass true as the `normalize` parameter since we'd need to do several checks here to // avoid that, and it shouldn't have a big performance impact. // The seconds and microseconds cast should never overflow since it's at most the number of seconds per day - PyDelta::new_bound( + PyDelta::new( py, days.try_into().unwrap_or(i32::MAX), secs.try_into()?, @@ -191,7 +191,7 @@ impl<'py> IntoPyObject<'py> for NaiveDate { let DateArgs { year, month, day } = (&self).into(); #[cfg(not(Py_LIMITED_API))] { - PyDate::new_bound(py, year, month, day) + PyDate::new(py, year, month, day) } #[cfg(Py_LIMITED_API)] @@ -262,7 +262,7 @@ impl<'py> IntoPyObject<'py> for NaiveTime { } = (&self).into(); #[cfg(not(Py_LIMITED_API))] - let time = PyTime::new_bound(py, hour, min, sec, micro, None)?; + let time = PyTime::new(py, hour, min, sec, micro, None)?; #[cfg(Py_LIMITED_API)] let time = DatetimeTypes::try_get(py) @@ -338,7 +338,7 @@ impl<'py> IntoPyObject<'py> for NaiveDateTime { } = (&self.time()).into(); #[cfg(not(Py_LIMITED_API))] - let datetime = PyDateTime::new_bound(py, year, month, day, hour, min, sec, micro, None)?; + let datetime = PyDateTime::new(py, year, month, day, hour, min, sec, micro, None)?; #[cfg(Py_LIMITED_API)] let datetime = DatetimeTypes::try_get(py).and_then(|dt| { @@ -380,7 +380,7 @@ impl FromPyObject<'_> for NaiveDateTime { // we return a hard error. We could silently remove tzinfo, or assume local timezone // and do a conversion, but better leave this decision to the user of the library. #[cfg(not(Py_LIMITED_API))] - let has_tzinfo = dt.get_tzinfo_bound().is_some(); + let has_tzinfo = dt.get_tzinfo().is_some(); #[cfg(Py_LIMITED_API)] let has_tzinfo = !dt.getattr(intern!(dt.py(), "tzinfo"))?.is_none(); if has_tzinfo { @@ -442,8 +442,7 @@ impl<'py, Tz: TimeZone> IntoPyObject<'py> for &DateTime { } = (&self.naive_local().time()).into(); #[cfg(not(Py_LIMITED_API))] - let datetime = - PyDateTime::new_bound(py, year, month, day, hour, min, sec, micro, Some(&tz))?; + let datetime = PyDateTime::new(py, year, month, day, hour, min, sec, micro, Some(&tz))?; #[cfg(Py_LIMITED_API)] let datetime = DatetimeTypes::try_get(py).and_then(|dt| { @@ -468,7 +467,7 @@ impl FromPyObject<'py>> FromPyObject<'_> for DateTime> = dt.getattr(intern!(dt.py(), "tzinfo"))?.extract()?; @@ -515,7 +514,7 @@ impl<'py> IntoPyObject<'py> for FixedOffset { let seconds_offset = self.local_minus_utc(); #[cfg(not(Py_LIMITED_API))] { - let td = PyDelta::new_bound(py, 0, seconds_offset, 0, true)?; + let td = PyDelta::new(py, 0, seconds_offset, 0, true)?; timezone_from_offset(&td) } @@ -597,11 +596,11 @@ impl<'py> IntoPyObject<'py> for Utc { fn into_pyobject(self, py: Python<'py>) -> Result { #[cfg(Py_LIMITED_API)] { - Ok(timezone_utc_bound(py).into_any()) + Ok(timezone_utc(py).into_any()) } #[cfg(not(Py_LIMITED_API))] { - Ok(timezone_utc_bound(py)) + Ok(timezone_utc(py)) } } } @@ -622,7 +621,7 @@ impl<'py> IntoPyObject<'py> for &Utc { impl FromPyObject<'_> for Utc { fn extract_bound(ob: &Bound<'_, PyAny>) -> PyResult { - let py_utc = timezone_utc_bound(ob.py()); + let py_utc = timezone_utc(ob.py()); if ob.eq(py_utc)? { Ok(Utc) } else { @@ -686,7 +685,7 @@ fn naive_datetime_to_py_datetime( truncated_leap_second, } = (&naive_datetime.time()).into(); #[cfg(not(Py_LIMITED_API))] - let datetime = PyDateTime::new_bound(py, year, month, day, hour, min, sec, micro, tzinfo) + let datetime = PyDateTime::new(py, year, month, day, hour, min, sec, micro, tzinfo) .expect("failed to construct datetime"); #[cfg(Py_LIMITED_API)] let datetime = DatetimeTypes::get(py) @@ -804,7 +803,7 @@ impl DatetimeTypes { } #[cfg(Py_LIMITED_API)] -fn timezone_utc_bound(py: Python<'_>) -> Bound<'_, PyAny> { +fn timezone_utc(py: Python<'_>) -> Bound<'_, PyAny> { DatetimeTypes::get(py).timezone_utc.bind(py).clone() } @@ -1160,7 +1159,7 @@ mod tests { let minute = 8; let second = 9; let micro = 999_999; - let tz_utc = timezone_utc_bound(py); + let tz_utc = timezone_utc(py); let py_datetime = new_py_datetime_ob( py, "datetime", diff --git a/src/conversions/std/time.rs b/src/conversions/std/time.rs index 5623297c161..50ec524d86d 100755 --- a/src/conversions/std/time.rs +++ b/src/conversions/std/time.rs @@ -5,7 +5,7 @@ use crate::types::any::PyAnyMethods; #[cfg(Py_LIMITED_API)] use crate::types::PyType; #[cfg(not(Py_LIMITED_API))] -use crate::types::{timezone_utc_bound, PyDateTime, PyDelta, PyDeltaAccess}; +use crate::types::{timezone_utc, PyDateTime, PyDelta, PyDeltaAccess}; #[cfg(Py_LIMITED_API)] use crate::Py; use crate::{ @@ -81,7 +81,7 @@ impl<'py> IntoPyObject<'py> for Duration { #[cfg(not(Py_LIMITED_API))] { - PyDelta::new_bound( + PyDelta::new( py, days.try_into()?, seconds.try_into().unwrap(), @@ -177,8 +177,7 @@ fn unix_epoch_py(py: Python<'_>) -> PyResult<&PyObject> { #[cfg(not(Py_LIMITED_API))] { Ok::<_, PyErr>( - PyDateTime::new_bound(py, 1970, 1, 1, 0, 0, 0, 0, Some(&timezone_utc_bound(py)))? - .into(), + PyDateTime::new(py, 1970, 1, 1, 0, 0, 0, 0, Some(&timezone_utc(py)))?.into(), ) } #[cfg(Py_LIMITED_API)] diff --git a/src/ffi/tests.rs b/src/ffi/tests.rs index ba2bd409cab..cd01eed4b1e 100644 --- a/src/ffi/tests.rs +++ b/src/ffi/tests.rs @@ -80,7 +80,7 @@ fn test_timezone_from_offset() { use crate::{ffi_ptr_ext::FfiPtrExt, types::PyDelta}; Python::with_gil(|py| { - let delta = PyDelta::new_bound(py, 0, 100, 0, false).unwrap(); + let delta = PyDelta::new(py, 0, 100, 0, false).unwrap(); let tz = unsafe { PyTimeZone_FromOffset(delta.as_ptr()).assume_owned(py) }; crate::py_run!( py, @@ -98,7 +98,7 @@ fn test_timezone_from_offset_and_name() { use crate::{ffi_ptr_ext::FfiPtrExt, types::PyDelta}; Python::with_gil(|py| { - let delta = PyDelta::new_bound(py, 0, 100, 0, false).unwrap(); + let delta = PyDelta::new(py, 0, 100, 0, false).unwrap(); let tzname = PyString::new(py, "testtz"); let tz = unsafe { PyTimeZone_FromOffsetAndName(delta.as_ptr(), tzname.as_ptr()).assume_owned(py) @@ -248,34 +248,34 @@ fn ucs4() { #[cfg_attr(target_arch = "wasm32", ignore)] // DateTime import fails on wasm for mysterious reasons #[cfg(not(PyPy))] fn test_get_tzinfo() { - use crate::types::timezone_utc_bound; + use crate::types::timezone_utc; crate::Python::with_gil(|py| { use crate::types::{PyDateTime, PyTime}; - let utc = &timezone_utc_bound(py); + let utc = &timezone_utc(py); - let dt = PyDateTime::new_bound(py, 2018, 1, 1, 0, 0, 0, 0, Some(utc)).unwrap(); + let dt = PyDateTime::new(py, 2018, 1, 1, 0, 0, 0, 0, Some(utc)).unwrap(); assert!( unsafe { Bound::from_borrowed_ptr(py, PyDateTime_DATE_GET_TZINFO(dt.as_ptr())) } .is(utc) ); - let dt = PyDateTime::new_bound(py, 2018, 1, 1, 0, 0, 0, 0, None).unwrap(); + let dt = PyDateTime::new(py, 2018, 1, 1, 0, 0, 0, 0, None).unwrap(); assert!( unsafe { Bound::from_borrowed_ptr(py, PyDateTime_DATE_GET_TZINFO(dt.as_ptr())) } .is_none() ); - let t = PyTime::new_bound(py, 0, 0, 0, 0, Some(utc)).unwrap(); + let t = PyTime::new(py, 0, 0, 0, 0, Some(utc)).unwrap(); assert!( unsafe { Bound::from_borrowed_ptr(py, PyDateTime_TIME_GET_TZINFO(t.as_ptr())) }.is(utc) ); - let t = PyTime::new_bound(py, 0, 0, 0, 0, None).unwrap(); + let t = PyTime::new(py, 0, 0, 0, 0, None).unwrap(); assert!( unsafe { Bound::from_borrowed_ptr(py, PyDateTime_TIME_GET_TZINFO(t.as_ptr())) } diff --git a/src/types/datetime.rs b/src/types/datetime.rs index a70cb6c885e..b2463bedde0 100644 --- a/src/types/datetime.rs +++ b/src/types/datetime.rs @@ -176,7 +176,14 @@ pub trait PyTzInfoAccess<'py> { /// Implementations should conform to the upstream documentation: /// /// - fn get_tzinfo_bound(&self) -> Option>; + fn get_tzinfo(&self) -> Option>; + + /// Deprecated name for [`PyTzInfoAccess::get_tzinfo`]. + #[deprecated(since = "0.23.0", note = "renamed to `PyTzInfoAccess::get_tzinfo`")] + #[inline] + fn get_tzinfo_bound(&self) -> Option> { + self.get_tzinfo() + } } /// Bindings around `datetime.date`. @@ -195,7 +202,7 @@ pyobject_native_type!( impl PyDate { /// Creates a new `datetime.date`. - pub fn new_bound(py: Python<'_>, year: i32, month: u8, day: u8) -> PyResult> { + pub fn new(py: Python<'_>, year: i32, month: u8, day: u8) -> PyResult> { let api = ensure_datetime_api(py)?; unsafe { (api.Date_FromDate)(year, c_int::from(month), c_int::from(day), api.DateType) @@ -204,10 +211,17 @@ impl PyDate { } } + /// Deprecated name for [`PyDate::new`]. + #[deprecated(since = "0.23.0", note = "renamed to `PyDate::new`")] + #[inline] + pub fn new_bound(py: Python<'_>, year: i32, month: u8, day: u8) -> PyResult> { + Self::new(py, year, month, day) + } + /// Construct a `datetime.date` from a POSIX timestamp /// /// This is equivalent to `datetime.date.fromtimestamp` - pub fn from_timestamp_bound(py: Python<'_>, timestamp: i64) -> PyResult> { + pub fn from_timestamp(py: Python<'_>, timestamp: i64) -> PyResult> { let time_tuple = PyTuple::new(py, [timestamp]); // safety ensure that the API is loaded @@ -219,6 +233,13 @@ impl PyDate { .downcast_into_unchecked() } } + + /// Deprecated name for [`PyDate::from_timestamp`]. + #[deprecated(since = "0.23.0", note = "renamed to `PyDate::from_timestamp`")] + #[inline] + pub fn from_timestamp_bound(py: Python<'_>, timestamp: i64) -> PyResult> { + Self::from_timestamp(py, timestamp) + } } impl PyDateAccess for Bound<'_, PyDate> { @@ -252,7 +273,7 @@ pyobject_native_type!( impl PyDateTime { /// Creates a new `datetime.datetime` object. #[allow(clippy::too_many_arguments)] - pub fn new_bound<'py>( + pub fn new<'py>( py: Python<'py>, year: i32, month: u8, @@ -281,6 +302,34 @@ impl PyDateTime { } } + /// Deprecated name for [`PyDateTime::new`]. + #[deprecated(since = "0.23.0", note = "renamed to `PyDateTime::new`")] + #[inline] + #[allow(clippy::too_many_arguments)] + pub fn new_bound<'py>( + py: Python<'py>, + year: i32, + month: u8, + day: u8, + hour: u8, + minute: u8, + second: u8, + microsecond: u32, + tzinfo: Option<&Bound<'py, PyTzInfo>>, + ) -> PyResult> { + Self::new( + py, + year, + month, + day, + hour, + minute, + second, + microsecond, + tzinfo, + ) + } + /// Alternate constructor that takes a `fold` parameter. A `true` value for this parameter /// signifies this this datetime is the later of two moments with the same representation, /// during a repeated interval. @@ -289,7 +338,7 @@ impl PyDateTime { /// represented time is ambiguous. /// See [PEP 495](https://www.python.org/dev/peps/pep-0495/) for more detail. #[allow(clippy::too_many_arguments)] - pub fn new_bound_with_fold<'py>( + pub fn new_with_fold<'py>( py: Python<'py>, year: i32, month: u8, @@ -320,10 +369,40 @@ impl PyDateTime { } } + /// Deprecated name for [`PyDateTime::new_with_fold`]. + #[deprecated(since = "0.23.0", note = "renamed to `PyDateTime::new_with_fold`")] + #[inline] + #[allow(clippy::too_many_arguments)] + pub fn new_bound_with_fold<'py>( + py: Python<'py>, + year: i32, + month: u8, + day: u8, + hour: u8, + minute: u8, + second: u8, + microsecond: u32, + tzinfo: Option<&Bound<'py, PyTzInfo>>, + fold: bool, + ) -> PyResult> { + Self::new_with_fold( + py, + year, + month, + day, + hour, + minute, + second, + microsecond, + tzinfo, + fold, + ) + } + /// Construct a `datetime` object from a POSIX timestamp /// /// This is equivalent to `datetime.datetime.fromtimestamp` - pub fn from_timestamp_bound<'py>( + pub fn from_timestamp<'py>( py: Python<'py>, timestamp: f64, tzinfo: Option<&Bound<'py, PyTzInfo>>, @@ -339,6 +418,17 @@ impl PyDateTime { .downcast_into_unchecked() } } + + /// Deprecated name for [`PyDateTime::from_timestamp`]. + #[deprecated(since = "0.23.0", note = "renamed to `PyDateTime::from_timestamp`")] + #[inline] + pub fn from_timestamp_bound<'py>( + py: Python<'py>, + timestamp: f64, + tzinfo: Option<&Bound<'py, PyTzInfo>>, + ) -> PyResult> { + Self::from_timestamp(py, timestamp, tzinfo) + } } impl PyDateAccess for Bound<'_, PyDateTime> { @@ -378,7 +468,7 @@ impl PyTimeAccess for Bound<'_, PyDateTime> { } impl<'py> PyTzInfoAccess<'py> for Bound<'py, PyDateTime> { - fn get_tzinfo_bound(&self) -> Option> { + fn get_tzinfo(&self) -> Option> { let ptr = self.as_ptr() as *mut ffi::PyDateTime_DateTime; #[cfg(not(GraalPy))] unsafe { @@ -427,7 +517,7 @@ pyobject_native_type!( impl PyTime { /// Creates a new `datetime.time` object. - pub fn new_bound<'py>( + pub fn new<'py>( py: Python<'py>, hour: u8, minute: u8, @@ -450,8 +540,22 @@ impl PyTime { } } + /// Deprecated name for [`PyTime::new`]. + #[deprecated(since = "0.23.0", note = "renamed to `PyTime::new`")] + #[inline] + pub fn new_bound<'py>( + py: Python<'py>, + hour: u8, + minute: u8, + second: u8, + microsecond: u32, + tzinfo: Option<&Bound<'py, PyTzInfo>>, + ) -> PyResult> { + Self::new(py, hour, minute, second, microsecond, tzinfo) + } + /// Alternate constructor that takes a `fold` argument. See [`PyDateTime::new_bound_with_fold`]. - pub fn new_bound_with_fold<'py>( + pub fn new_with_fold<'py>( py: Python<'py>, hour: u8, minute: u8, @@ -475,6 +579,21 @@ impl PyTime { .downcast_into_unchecked() } } + + /// Deprecated name for [`PyTime::new_with_fold`]. + #[deprecated(since = "0.23.0", note = "renamed to `PyTime::new_with_fold`")] + #[inline] + pub fn new_bound_with_fold<'py>( + py: Python<'py>, + hour: u8, + minute: u8, + second: u8, + microsecond: u32, + tzinfo: Option<&Bound<'py, PyTzInfo>>, + fold: bool, + ) -> PyResult> { + Self::new_with_fold(py, hour, minute, second, microsecond, tzinfo, fold) + } } impl PyTimeAccess for Bound<'_, PyTime> { @@ -500,7 +619,7 @@ impl PyTimeAccess for Bound<'_, PyTime> { } impl<'py> PyTzInfoAccess<'py> for Bound<'py, PyTime> { - fn get_tzinfo_bound(&self) -> Option> { + fn get_tzinfo(&self) -> Option> { let ptr = self.as_ptr() as *mut ffi::PyDateTime_Time; #[cfg(not(GraalPy))] unsafe { @@ -552,7 +671,7 @@ pyobject_native_type!( ); /// Equivalent to `datetime.timezone.utc` -pub fn timezone_utc_bound(py: Python<'_>) -> Bound<'_, PyTzInfo> { +pub fn timezone_utc(py: Python<'_>) -> Bound<'_, PyTzInfo> { // TODO: this _could_ have a borrowed form `timezone_utc_borrowed`, but that seems // like an edge case optimization and we'd prefer in PyO3 0.21 to use `Bound` as // much as possible @@ -565,6 +684,13 @@ pub fn timezone_utc_bound(py: Python<'_>) -> Bound<'_, PyTzInfo> { } } +/// Deprecated name for [`timezone_utc`]. +#[deprecated(since = "0.23.0", note = "renamed to `timezone_utc`")] +#[inline] +pub fn timezone_utc_bound(py: Python<'_>) -> Bound<'_, PyTzInfo> { + timezone_utc(py) +} + /// Equivalent to `datetime.timezone` constructor /// /// Only used internally @@ -597,7 +723,7 @@ pyobject_native_type!( impl PyDelta { /// Creates a new `timedelta`. - pub fn new_bound( + pub fn new( py: Python<'_>, days: i32, seconds: i32, @@ -617,6 +743,19 @@ impl PyDelta { .downcast_into_unchecked() } } + + /// Deprecated name for [`PyDelta::new`]. + #[deprecated(since = "0.23.0", note = "renamed to `PyDelta::new`")] + #[inline] + pub fn new_bound( + py: Python<'_>, + days: i32, + seconds: i32, + microseconds: i32, + normalize: bool, + ) -> PyResult> { + Self::new(py, days, seconds, microseconds, normalize) + } } impl PyDeltaAccess for Bound<'_, PyDelta> { @@ -653,15 +792,14 @@ mod tests { #[cfg_attr(target_arch = "wasm32", ignore)] // DateTime import fails on wasm for mysterious reasons fn test_datetime_fromtimestamp() { Python::with_gil(|py| { - let dt = PyDateTime::from_timestamp_bound(py, 100.0, None).unwrap(); + let dt = PyDateTime::from_timestamp(py, 100.0, None).unwrap(); py_run!( py, dt, "import datetime; assert dt == datetime.datetime.fromtimestamp(100)" ); - let dt = - PyDateTime::from_timestamp_bound(py, 100.0, Some(&timezone_utc_bound(py))).unwrap(); + let dt = PyDateTime::from_timestamp(py, 100.0, Some(&timezone_utc(py))).unwrap(); py_run!( py, dt, @@ -675,7 +813,7 @@ mod tests { #[cfg_attr(target_arch = "wasm32", ignore)] // DateTime import fails on wasm for mysterious reasons fn test_date_fromtimestamp() { Python::with_gil(|py| { - let dt = PyDate::from_timestamp_bound(py, 100).unwrap(); + let dt = PyDate::from_timestamp(py, 100).unwrap(); py_run!( py, dt, @@ -688,10 +826,8 @@ mod tests { #[cfg_attr(target_arch = "wasm32", ignore)] // DateTime import fails on wasm for mysterious reasons fn test_new_with_fold() { Python::with_gil(|py| { - let a = - PyDateTime::new_bound_with_fold(py, 2021, 1, 23, 20, 32, 40, 341516, None, false); - let b = - PyDateTime::new_bound_with_fold(py, 2021, 1, 23, 20, 32, 40, 341516, None, true); + let a = PyDateTime::new_with_fold(py, 2021, 1, 23, 20, 32, 40, 341516, None, false); + let b = PyDateTime::new_with_fold(py, 2021, 1, 23, 20, 32, 40, 341516, None, true); assert!(!a.unwrap().get_fold()); assert!(b.unwrap().get_fold()); @@ -702,23 +838,23 @@ mod tests { #[cfg_attr(target_arch = "wasm32", ignore)] // DateTime import fails on wasm for mysterious reasons fn test_get_tzinfo() { crate::Python::with_gil(|py| { - let utc = timezone_utc_bound(py); + let utc = timezone_utc(py); - let dt = PyDateTime::new_bound(py, 2018, 1, 1, 0, 0, 0, 0, Some(&utc)).unwrap(); + let dt = PyDateTime::new(py, 2018, 1, 1, 0, 0, 0, 0, Some(&utc)).unwrap(); - assert!(dt.get_tzinfo_bound().unwrap().eq(&utc).unwrap()); + assert!(dt.get_tzinfo().unwrap().eq(&utc).unwrap()); - let dt = PyDateTime::new_bound(py, 2018, 1, 1, 0, 0, 0, 0, None).unwrap(); + let dt = PyDateTime::new(py, 2018, 1, 1, 0, 0, 0, 0, None).unwrap(); - assert!(dt.get_tzinfo_bound().is_none()); + assert!(dt.get_tzinfo().is_none()); - let t = PyTime::new_bound(py, 0, 0, 0, 0, Some(&utc)).unwrap(); + let t = PyTime::new(py, 0, 0, 0, 0, Some(&utc)).unwrap(); - assert!(t.get_tzinfo_bound().unwrap().eq(utc).unwrap()); + assert!(t.get_tzinfo().unwrap().eq(utc).unwrap()); - let t = PyTime::new_bound(py, 0, 0, 0, 0, None).unwrap(); + let t = PyTime::new(py, 0, 0, 0, 0, None).unwrap(); - assert!(t.get_tzinfo_bound().is_none()); + assert!(t.get_tzinfo().is_none()); }); } @@ -728,28 +864,28 @@ mod tests { fn test_timezone_from_offset() { Python::with_gil(|py| { assert!( - timezone_from_offset(&PyDelta::new_bound(py, 0, -3600, 0, true).unwrap()) + timezone_from_offset(&PyDelta::new(py, 0, -3600, 0, true).unwrap()) .unwrap() .call_method1("utcoffset", ((),)) .unwrap() .downcast_into::() .unwrap() - .eq(PyDelta::new_bound(py, 0, -3600, 0, true).unwrap()) + .eq(PyDelta::new(py, 0, -3600, 0, true).unwrap()) .unwrap() ); assert!( - timezone_from_offset(&PyDelta::new_bound(py, 0, 3600, 0, true).unwrap()) + timezone_from_offset(&PyDelta::new(py, 0, 3600, 0, true).unwrap()) .unwrap() .call_method1("utcoffset", ((),)) .unwrap() .downcast_into::() .unwrap() - .eq(PyDelta::new_bound(py, 0, 3600, 0, true).unwrap()) + .eq(PyDelta::new(py, 0, 3600, 0, true).unwrap()) .unwrap() ); - timezone_from_offset(&PyDelta::new_bound(py, 1, 0, 0, true).unwrap()).unwrap_err(); + timezone_from_offset(&PyDelta::new(py, 1, 0, 0, true).unwrap()).unwrap_err(); }) } } diff --git a/src/types/mod.rs b/src/types/mod.rs index 9a54eee9661..d11af8598fe 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -9,9 +9,10 @@ pub use self::capsule::{PyCapsule, PyCapsuleMethods}; pub use self::code::PyCode; pub use self::complex::{PyComplex, PyComplexMethods}; #[cfg(not(Py_LIMITED_API))] +#[allow(deprecated)] pub use self::datetime::{ - timezone_utc_bound, PyDate, PyDateAccess, PyDateTime, PyDelta, PyDeltaAccess, PyTime, - PyTimeAccess, PyTzInfo, PyTzInfoAccess, + timezone_utc, timezone_utc_bound, PyDate, PyDateAccess, PyDateTime, PyDelta, PyDeltaAccess, + PyTime, PyTimeAccess, PyTzInfo, PyTzInfoAccess, }; pub use self::dict::{IntoPyDict, PyDict, PyDictMethods}; #[cfg(not(any(PyPy, GraalPy)))] diff --git a/tests/test_datetime.rs b/tests/test_datetime.rs index 214e1313d94..2278d9ecc73 100644 --- a/tests/test_datetime.rs +++ b/tests/test_datetime.rs @@ -1,6 +1,6 @@ #![cfg(not(Py_LIMITED_API))] -use pyo3::types::{timezone_utc_bound, IntoPyDict, PyDate, PyDateTime, PyTime}; +use pyo3::types::{timezone_utc, IntoPyDict, PyDate, PyDateTime, PyTime}; use pyo3::{ffi, prelude::*}; use pyo3_ffi::PyDateTime_IMPORT; use std::ffi::CString; @@ -131,9 +131,9 @@ fn test_datetime_utc() { use pyo3::types::PyDateTime; Python::with_gil(|py| { - let utc = timezone_utc_bound(py); + let utc = timezone_utc(py); - let dt = PyDateTime::new_bound(py, 2018, 1, 1, 0, 0, 0, 0, Some(&utc)).unwrap(); + let dt = PyDateTime::new(py, 2018, 1, 1, 0, 0, 0, 0, Some(&utc)).unwrap(); let locals = [("dt", dt)].into_py_dict(py); @@ -172,7 +172,7 @@ fn test_pydate_out_of_bounds() { Python::with_gil(|py| { for val in INVALID_DATES { let (year, month, day) = val; - let dt = PyDate::new_bound(py, *year, *month, *day); + let dt = PyDate::new(py, *year, *month, *day); dt.unwrap_err(); } }); @@ -185,7 +185,7 @@ fn test_pytime_out_of_bounds() { Python::with_gil(|py| { for val in INVALID_TIMES { let (hour, minute, second, microsecond) = val; - let dt = PyTime::new_bound(py, *hour, *minute, *second, *microsecond, None); + let dt = PyTime::new(py, *hour, *minute, *second, *microsecond, None); dt.unwrap_err(); } }); @@ -209,7 +209,7 @@ fn test_pydatetime_out_of_bounds() { let (date, time) = val; let (year, month, day) = date; let (hour, minute, second, microsecond) = time; - let dt = PyDateTime::new_bound( + let dt = PyDateTime::new( py, *year, *month, diff --git a/tests/test_datetime_import.rs b/tests/test_datetime_import.rs index 68ef776a37a..fff2ddc6280 100644 --- a/tests/test_datetime_import.rs +++ b/tests/test_datetime_import.rs @@ -21,6 +21,6 @@ fn test_bad_datetime_module_panic() { .unwrap(); // This should panic because the "datetime" module is empty - PyDate::new_bound(py, 2018, 1, 1).unwrap(); + PyDate::new(py, 2018, 1, 1).unwrap(); }); } From 4fb9b8690f9daaabda1fd2e36dc1b5e43b2126ed Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Tue, 20 Aug 2024 16:40:50 -0600 Subject: [PATCH 222/495] fix weakref proxy tests on free-threaded build (#4460) --- src/types/weakref/proxy.rs | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/src/types/weakref/proxy.rs b/src/types/weakref/proxy.rs index 80d254a38cf..013eeb5e81e 100644 --- a/src/types/weakref/proxy.rs +++ b/src/types/weakref/proxy.rs @@ -238,11 +238,12 @@ mod tests { mod python_class { use super::*; use crate::ffi; - use crate::{py_result_ext::PyResultExt, types::PyType}; + use crate::{py_result_ext::PyResultExt, types::PyDict, types::PyType}; fn get_type(py: Python<'_>) -> PyResult> { - py.run(ffi::c_str!("class A:\n pass\n"), None, None)?; - py.eval(ffi::c_str!("A"), None, None) + let globals = PyDict::new(py); + py.run(ffi::c_str!("class A:\n pass\n"), Some(&globals), None)?; + py.eval(ffi::c_str!("A"), Some(&globals), None) .downcast_into::() } @@ -262,10 +263,7 @@ mod tests { format!("", CLASS_NAME) ); - assert_eq!( - reference.getattr("__class__")?.to_string(), - "" - ); + assert_eq!(reference.getattr("__class__")?.to_string(), ""); #[cfg(not(Py_LIMITED_API))] check_repr(&reference, &object, Some("A"))?; @@ -782,15 +780,16 @@ mod tests { mod python_class { use super::*; use crate::ffi; - use crate::{py_result_ext::PyResultExt, types::PyType}; + use crate::{py_result_ext::PyResultExt, types::PyDict, types::PyType}; fn get_type(py: Python<'_>) -> PyResult> { + let globals = PyDict::new(py); py.run( ffi::c_str!("class A:\n def __call__(self):\n return 'This class is callable!'\n"), - None, + Some(&globals), None, )?; - py.eval(ffi::c_str!("A"), None, None) + py.eval(ffi::c_str!("A"), Some(&globals), None) .downcast_into::() } @@ -806,10 +805,7 @@ mod tests { #[cfg(not(Py_LIMITED_API))] assert_eq!(reference.get_type().to_string(), CLASS_NAME); - assert_eq!( - reference.getattr("__class__")?.to_string(), - "" - ); + assert_eq!(reference.getattr("__class__")?.to_string(), ""); #[cfg(not(Py_LIMITED_API))] check_repr(&reference, &object, Some("A"))?; From 43110ef000930bc6b6e112ae1d5ef90cb0262cc1 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Tue, 20 Aug 2024 16:41:16 -0600 Subject: [PATCH 223/495] Delete iter_dict_nosegv (#4459) --- tests/test_dict_iter.rs | 18 ------------------ 1 file changed, 18 deletions(-) delete mode 100644 tests/test_dict_iter.rs diff --git a/tests/test_dict_iter.rs b/tests/test_dict_iter.rs deleted file mode 100644 index 5b3573d10ad..00000000000 --- a/tests/test_dict_iter.rs +++ /dev/null @@ -1,18 +0,0 @@ -use pyo3::prelude::*; -use pyo3::types::IntoPyDict; - -#[test] -#[cfg_attr(target_arch = "wasm32", ignore)] // Not sure why this fails. -#[cfg_attr(Py_GIL_DISABLED, ignore)] // test deadlocks in GIL-disabled build, TODO: fix deadlock -fn iter_dict_nosegv() { - Python::with_gil(|py| { - const LEN: usize = 10_000_000; - let dict = (0..LEN as u64).map(|i| (i, i * 2)).into_py_dict(py); - let mut sum = 0; - for (k, _v) in dict { - let i: u64 = k.extract().unwrap(); - sum += i; - } - assert_eq!(sum, 49_999_995_000_000); - }); -} From eac59bcba1cdd14268622609c1f196d9358422a1 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Wed, 21 Aug 2024 09:23:04 +0100 Subject: [PATCH 224/495] ci: swap .python-version file for explicit versions in actions yml (#4462) * ci: swap .python-version file for explicit versions in actions yml * pin valgrind job to 3.12.4 --- .github/workflows/benches.yml | 2 ++ .github/workflows/changelog.yml | 2 ++ .github/workflows/ci.yml | 24 ++++++++++++++++++++++-- .github/workflows/gh-pages.yml | 2 ++ .python-version | 1 - 5 files changed, 28 insertions(+), 3 deletions(-) delete mode 100644 .python-version diff --git a/.github/workflows/benches.yml b/.github/workflows/benches.yml index 04b362947f5..19a10adaa40 100644 --- a/.github/workflows/benches.yml +++ b/.github/workflows/benches.yml @@ -19,6 +19,8 @@ jobs: steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 + with: + python-version: '3.12' - uses: dtolnay/rust-toolchain@stable with: components: rust-src diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml index 0a782b4010f..b4c3b9892f3 100644 --- a/.github/workflows/changelog.yml +++ b/.github/workflows/changelog.yml @@ -11,5 +11,7 @@ jobs: steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 + with: + python-version: '3.12' - run: python -m pip install --upgrade pip && pip install nox - run: nox -s check-changelog diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2f2e093937d..ede5d7172b5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,6 +22,8 @@ jobs: steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 + with: + python-version: '3.12' - run: python -m pip install --upgrade pip && pip install nox - uses: dtolnay/rust-toolchain@stable with: @@ -38,6 +40,8 @@ jobs: steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 + with: + python-version: '3.12' - name: resolve MSRV id: resolve-msrv run: @@ -50,6 +54,8 @@ jobs: steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 + with: + python-version: '3.12' - uses: obi1kenobi/cargo-semver-checks-action@v2 check-msrv: @@ -60,11 +66,10 @@ jobs: - uses: dtolnay/rust-toolchain@master with: toolchain: ${{ needs.resolve.outputs.MSRV }} - targets: x86_64-unknown-linux-gnu components: rust-src - uses: actions/setup-python@v5 with: - architecture: "x64" + python-version: '3.12' - uses: Swatinem/rust-cache@v2 with: save-if: ${{ github.event_name != 'merge_group' }} @@ -143,6 +148,7 @@ jobs: components: clippy,rust-src - uses: actions/setup-python@v5 with: + python-version: '3.12' architecture: ${{ matrix.platform.python-architecture }} - uses: Swatinem/rust-cache@v2 with: @@ -324,6 +330,10 @@ jobs: steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 + with: + # FIXME valgrind detects an issue with Python 3.12.5, needs investigation + # whether it's a PyO3 issue or upstream CPython. + python-version: '3.12.4' - uses: Swatinem/rust-cache@v2 with: save-if: ${{ github.event_name != 'merge_group' }} @@ -343,6 +353,8 @@ jobs: steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 + with: + python-version: '3.12' - uses: Swatinem/rust-cache@v2 with: save-if: ${{ github.event_name != 'merge_group' }} @@ -363,6 +375,8 @@ jobs: steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 + with: + python-version: '3.12' - uses: Swatinem/rust-cache@v2 with: save-if: ${{ github.event_name != 'merge_group' }} @@ -387,6 +401,8 @@ jobs: if: steps.should-skip.outputs.skip != 'true' - uses: actions/setup-python@v5 if: steps.should-skip.outputs.skip != 'true' + with: + python-version: '3.12' - uses: Swatinem/rust-cache@v2 if: steps.should-skip.outputs.skip != 'true' with: @@ -546,6 +562,8 @@ jobs: steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 + with: + python-version: '3.12' - uses: Swatinem/rust-cache@v2 with: save-if: ${{ github.event_name != 'merge_group' }} @@ -613,6 +631,8 @@ jobs: steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 + with: + python-version: '3.12' - uses: Swatinem/rust-cache@v2 with: workspaces: diff --git a/.github/workflows/gh-pages.yml b/.github/workflows/gh-pages.yml index 7c10a075db4..1050b97f314 100644 --- a/.github/workflows/gh-pages.yml +++ b/.github/workflows/gh-pages.yml @@ -23,6 +23,8 @@ jobs: steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 + with: + python-version: '3.12' - uses: dtolnay/rust-toolchain@nightly - name: Setup mdBook diff --git a/.python-version b/.python-version deleted file mode 100644 index e4fba218358..00000000000 --- a/.python-version +++ /dev/null @@ -1 +0,0 @@ -3.12 From 7879d57df40c241927d0aa3ef46615bddd2d050f Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Wed, 21 Aug 2024 11:09:31 +0200 Subject: [PATCH 225/495] `IntoPyObject` for `#[pyo3(get)]` (#4449) * add `#[pyo3(get)]` specialization for `IntoPyObject` * emit correct diagnostic if nothing is implemented * update migration guide * refactor mutable alias checking into a function * refactor field ptr offset into function * cfg gate `PyO3GetFieldIntoPyObject` * review feedback --- guide/src/migration.md | 3 +- pyo3-macros-backend/src/pymethod.rs | 3 + src/impl_/pyclass.rs | 291 ++++++++++++++++++++++---- tests/ui/invalid_property_args.stderr | 14 +- 4 files changed, 263 insertions(+), 48 deletions(-) diff --git a/guide/src/migration.md b/guide/src/migration.md index e0bd38a5153..8a6ffedc73c 100644 --- a/guide/src/migration.md +++ b/guide/src/migration.md @@ -97,7 +97,8 @@ where Click to expand PyO3 0.23 introduced the new fallible conversion trait `IntoPyObject`. The `#[pyfunction]` and -`#[pymethods]` macros prefer `IntoPyObject` implementations over `IntoPy`. +`#[pymethods]` macros prefer `IntoPyObject` implementations over `IntoPy`. This also +applies to `#[pyo3(get)]`. This change has an effect on functions and methods returning _byte_ collections like - `Vec` diff --git a/pyo3-macros-backend/src/pymethod.rs b/pyo3-macros-backend/src/pymethod.rs index 3fb13e17373..2abc008899c 100644 --- a/pyo3-macros-backend/src/pymethod.rs +++ b/pyo3-macros-backend/src/pymethod.rs @@ -788,6 +788,9 @@ pub fn impl_py_getter_def( Offset, { #pyo3_path::impl_::pyclass::IsPyT::<#ty>::VALUE }, { #pyo3_path::impl_::pyclass::IsToPyObject::<#ty>::VALUE }, + { #pyo3_path::impl_::pyclass::IsIntoPy::<#ty>::VALUE }, + { #pyo3_path::impl_::pyclass::IsIntoPyObjectRef::<#ty>::VALUE }, + { #pyo3_path::impl_::pyclass::IsIntoPyObject::<#ty>::VALUE }, > = unsafe { #pyo3_path::impl_::pyclass::PyClassGetterGenerator::new() }; #pyo3_path::impl_::pyclass::MaybeRuntimePyMethodDef::Runtime( || GENERATOR.generate(#python_name, #doc) diff --git a/src/impl_/pyclass.rs b/src/impl_/pyclass.rs index 1680eca34ac..8d25a7b8920 100644 --- a/src/impl_/pyclass.rs +++ b/src/impl_/pyclass.rs @@ -1,4 +1,5 @@ use crate::{ + conversion::IntoPyObject, exceptions::{PyAttributeError, PyNotImplementedError, PyRuntimeError, PyValueError}, ffi, impl_::{ @@ -6,9 +7,11 @@ use crate::{ pycell::{GetBorrowChecker, PyClassMutability, PyClassObjectLayout}, pymethods::{PyGetterDef, PyMethodDefType}, }, + pycell::PyBorrowError, pyclass_init::PyObjectInit, types::{any::PyAnyMethods, PyBool}, - Borrowed, IntoPy, Py, PyAny, PyClass, PyErr, PyResult, PyTypeInfo, Python, ToPyObject, + Borrowed, BoundObject, IntoPy, Py, PyAny, PyClass, PyErr, PyRef, PyResult, PyTypeInfo, Python, + ToPyObject, }; use std::{ borrow::Cow, @@ -1211,6 +1214,9 @@ pub struct PyClassGetterGenerator< // at compile time const IS_PY_T: bool, const IMPLEMENTS_TOPYOBJECT: bool, + const IMPLEMENTS_INTOPY: bool, + const IMPLEMENTS_INTOPYOBJECT_REF: bool, + const IMPLEMENTS_INTOPYOBJECT: bool, >(PhantomData<(ClassT, FieldT, Offset)>); impl< @@ -1219,7 +1225,20 @@ impl< Offset: OffsetCalculator, const IS_PY_T: bool, const IMPLEMENTS_TOPYOBJECT: bool, - > PyClassGetterGenerator + const IMPLEMENTS_INTOPY: bool, + const IMPLEMENTS_INTOPYOBJECT_REF: bool, + const IMPLEMENTS_INTOPYOBJECT: bool, + > + PyClassGetterGenerator< + ClassT, + FieldT, + Offset, + IS_PY_T, + IMPLEMENTS_TOPYOBJECT, + IMPLEMENTS_INTOPY, + IMPLEMENTS_INTOPYOBJECT_REF, + IMPLEMENTS_INTOPYOBJECT, + > { /// Safety: constructing this type requires that there exists a value of type FieldT /// at the calculated offset within the type ClassT. @@ -1233,7 +1252,20 @@ impl< U, Offset: OffsetCalculator>, const IMPLEMENTS_TOPYOBJECT: bool, - > PyClassGetterGenerator, Offset, true, IMPLEMENTS_TOPYOBJECT> + const IMPLEMENTS_INTOPY: bool, + const IMPLEMENTS_INTOPYOBJECT_REF: bool, + const IMPLEMENTS_INTOPYOBJECT: bool, + > + PyClassGetterGenerator< + ClassT, + Py, + Offset, + true, + IMPLEMENTS_TOPYOBJECT, + IMPLEMENTS_INTOPY, + IMPLEMENTS_INTOPYOBJECT_REF, + IMPLEMENTS_INTOPYOBJECT, + > { /// `Py` fields have a potential optimization to use Python's "struct members" to read /// the field directly from the struct, rather than using a getter function. @@ -1260,9 +1292,14 @@ impl< } } -/// Field is not `Py`; try to use `ToPyObject` to avoid potentially expensive clones of containers like `Vec` -impl> - PyClassGetterGenerator +/// Fallback case; Field is not `Py`; try to use `ToPyObject` to avoid potentially expensive +/// clones of containers like `Vec` +impl< + ClassT: PyClass, + FieldT: ToPyObject, + Offset: OffsetCalculator, + const IMPLEMENTS_INTOPY: bool, + > PyClassGetterGenerator { pub const fn generate(&self, name: &'static CStr, doc: &'static CStr) -> PyMethodDefType { PyMethodDefType::Getter(PyGetterDef { @@ -1273,32 +1310,133 @@ impl`; try to use `IntoPyObject` for `&T` (prefered over `ToPyObject`) to avoid +/// potentially expensive clones of containers like `Vec` +impl< + ClassT, + FieldT, + Offset, + const IMPLEMENTS_TOPYOBJECT: bool, + const IMPLEMENTS_INTOPY: bool, + const IMPLEMENTS_INTOPYOBJECT: bool, + > + PyClassGetterGenerator< + ClassT, + FieldT, + Offset, + false, + IMPLEMENTS_TOPYOBJECT, + IMPLEMENTS_INTOPY, + true, + IMPLEMENTS_INTOPYOBJECT, + > +where + ClassT: PyClass, + for<'a, 'py> &'a FieldT: IntoPyObject<'py>, + Offset: OffsetCalculator, + for<'a, 'py> PyErr: From<<&'a FieldT as IntoPyObject<'py>>::Error>, +{ + pub const fn generate(&self, name: &'static CStr, doc: &'static CStr) -> PyMethodDefType { + PyMethodDefType::Getter(PyGetterDef { + name, + meth: pyo3_get_value_into_pyobject_ref::, + doc, + }) + } +} + +/// Temporary case to prefer `IntoPyObject + Clone` over `IntoPy + Clone`, while still showing the +/// `IntoPyObject` suggestion if neither is implemented; +impl + PyClassGetterGenerator< + ClassT, + FieldT, + Offset, + false, + IMPLEMENTS_TOPYOBJECT, + IMPLEMENTS_INTOPY, + false, + true, + > +where + ClassT: PyClass, + Offset: OffsetCalculator, + for<'py> FieldT: IntoPyObject<'py> + Clone, + for<'py> PyErr: std::convert::From<>::Error>, +{ + pub const fn generate(&self, name: &'static CStr, doc: &'static CStr) -> PyMethodDefType { + PyMethodDefType::Getter(PyGetterDef { + name, + meth: pyo3_get_value_into_pyobject::, + doc, + }) + } +} + +/// IntoPy + Clone fallback case, which was the only behaviour before PyO3 0.22. +impl + PyClassGetterGenerator +where + ClassT: PyClass, + Offset: OffsetCalculator, + FieldT: IntoPy> + Clone, +{ + pub const fn generate(&self, name: &'static CStr, doc: &'static CStr) -> PyMethodDefType { + PyMethodDefType::Getter(PyGetterDef { + name, + meth: pyo3_get_value::, + doc, + }) + } +} + +#[cfg(diagnostic_namespace)] +#[diagnostic::on_unimplemented( + message = "`{Self}` cannot be converted to a Python object", + label = "required by `#[pyo3(get)]` to create a readable property from a field of type `{Self}`", + note = "implement `IntoPyObject` for `&{Self}` or `IntoPyObject + Clone` for `{Self}` to define the conversion" )] -pub trait PyO3GetField: IntoPy> + Clone {} -impl> + Clone> PyO3GetField for T {} +pub trait PyO3GetField<'py>: IntoPyObject<'py, Error: Into> + Clone {} -/// Base case attempts to use IntoPy + Clone, which was the only behaviour before PyO3 0.22. +#[cfg(diagnostic_namespace)] +impl<'py, T> PyO3GetField<'py> for T +where + T: IntoPyObject<'py> + Clone, + PyErr: std::convert::From<>::Error>, +{ +} + +/// Base case attempts to use IntoPyObject + Clone impl> - PyClassGetterGenerator + PyClassGetterGenerator { - pub const fn generate(&self, name: &'static CStr, doc: &'static CStr) -> PyMethodDefType + #[cfg(not(diagnostic_namespace))] + pub const fn generate(&self, _name: &'static CStr, _doc: &'static CStr) -> PyMethodDefType // The bound goes here rather than on the block so that this impl is always available // if no specialization is used instead where - FieldT: PyO3GetField, + for<'py> FieldT: IntoPyObject<'py> + Clone, + for<'py> PyErr: std::convert::From<>::Error>, { - PyMethodDefType::Getter(PyGetterDef { - name, - meth: pyo3_get_value::, - doc, - }) + // unreachable not allowed in const + panic!( + "exists purely to emit diagnostics on unimplemented traits. When `ToPyObject` \ + and `IntoPy` are fully removed this will be replaced by the temporary `IntoPyObject` case above." + ) + } + + #[cfg(diagnostic_namespace)] + pub const fn generate(&self, _name: &'static CStr, _doc: &'static CStr) -> PyMethodDefType + // The bound goes here rather than on the block so that this impl is always available + // if no specialization is used instead + where + for<'py> FieldT: PyO3GetField<'py>, + { + // unreachable not allowed in const + panic!( + "exists purely to emit diagnostics on unimplemented traits. When `ToPyObject` \ + and `IntoPy` are fully removed this will be replaced by the temporary `IntoPyObject` case above." + ) } } @@ -1334,6 +1472,51 @@ impl IsToPyObject { pub const VALUE: bool = true; } +probe!(IsIntoPy); + +impl> IsIntoPy { + pub const VALUE: bool = true; +} + +probe!(IsIntoPyObjectRef); + +impl<'a, 'py, T: 'a> IsIntoPyObjectRef +where + &'a T: IntoPyObject<'py>, +{ + pub const VALUE: bool = true; +} + +probe!(IsIntoPyObject); + +impl<'py, T> IsIntoPyObject +where + T: IntoPyObject<'py>, +{ + pub const VALUE: bool = true; +} + +/// ensures `obj` is not mutably aliased +#[inline] +unsafe fn ensure_no_mutable_alias<'py, ClassT: PyClass>( + py: Python<'py>, + obj: &*mut ffi::PyObject, +) -> Result, PyBorrowError> { + BoundRef::ref_from_ptr(py, obj) + .downcast_unchecked::() + .try_borrow() +} + +/// calculates the field pointer from an PyObject pointer +#[inline] +fn field_from_object(obj: *mut ffi::PyObject) -> *mut FieldT +where + ClassT: PyClass, + Offset: OffsetCalculator, +{ + unsafe { obj.cast::().add(Offset::offset()).cast::() } +} + fn pyo3_get_value_topyobject< ClassT: PyClass, FieldT: ToPyObject, @@ -1342,20 +1525,54 @@ fn pyo3_get_value_topyobject< py: Python<'_>, obj: *mut ffi::PyObject, ) -> PyResult<*mut ffi::PyObject> { - // Check for mutable aliasing - let _holder = unsafe { - BoundRef::ref_from_ptr(py, &obj) - .downcast_unchecked::() - .try_borrow()? - }; - - let value = unsafe { obj.cast::().add(Offset::offset()).cast::() }; + let _holder = unsafe { ensure_no_mutable_alias::(py, &obj)? }; + let value = field_from_object::(obj); // SAFETY: Offset is known to describe the location of the value, and // _holder is preventing mutable aliasing Ok((unsafe { &*value }).to_object(py).into_ptr()) } +fn pyo3_get_value_into_pyobject_ref( + py: Python<'_>, + obj: *mut ffi::PyObject, +) -> PyResult<*mut ffi::PyObject> +where + ClassT: PyClass, + for<'a, 'py> &'a FieldT: IntoPyObject<'py>, + Offset: OffsetCalculator, + for<'a, 'py> PyErr: From<<&'a FieldT as IntoPyObject<'py>>::Error>, +{ + let _holder = unsafe { ensure_no_mutable_alias::(py, &obj)? }; + let value = field_from_object::(obj); + + // SAFETY: Offset is known to describe the location of the value, and + // _holder is preventing mutable aliasing + Ok((unsafe { &*value }).into_pyobject(py)?.into_ptr()) +} + +fn pyo3_get_value_into_pyobject( + py: Python<'_>, + obj: *mut ffi::PyObject, +) -> PyResult<*mut ffi::PyObject> +where + ClassT: PyClass, + for<'py> FieldT: IntoPyObject<'py> + Clone, + Offset: OffsetCalculator, + for<'py> >::Error: Into, +{ + let _holder = unsafe { ensure_no_mutable_alias::(py, &obj)? }; + let value = field_from_object::(obj); + + // SAFETY: Offset is known to describe the location of the value, and + // _holder is preventing mutable aliasing + Ok((unsafe { &*value }) + .clone() + .into_pyobject(py) + .map_err(Into::into)? + .into_ptr()) +} + fn pyo3_get_value< ClassT: PyClass, FieldT: IntoPy> + Clone, @@ -1364,14 +1581,8 @@ fn pyo3_get_value< py: Python<'_>, obj: *mut ffi::PyObject, ) -> PyResult<*mut ffi::PyObject> { - // Check for mutable aliasing - let _holder = unsafe { - BoundRef::ref_from_ptr(py, &obj) - .downcast_unchecked::() - .try_borrow()? - }; - - let value = unsafe { obj.cast::().add(Offset::offset()).cast::() }; + let _holder = unsafe { ensure_no_mutable_alias::(py, &obj)? }; + let value = field_from_object::(obj); // SAFETY: Offset is known to describe the location of the value, and // _holder is preventing mutable aliasing diff --git a/tests/ui/invalid_property_args.stderr b/tests/ui/invalid_property_args.stderr index 0ee00cc6430..0a03969fda8 100644 --- a/tests/ui/invalid_property_args.stderr +++ b/tests/ui/invalid_property_args.stderr @@ -52,14 +52,14 @@ error[E0277]: `PhantomData` cannot be converted to a Python object 45 | value: ::std::marker::PhantomData, | ^ required by `#[pyo3(get)]` to create a readable property from a field of type `PhantomData` | - = help: the trait `IntoPy>` is not implemented for `PhantomData`, which is required by `PhantomData: PyO3GetField` - = note: implement `ToPyObject` or `IntoPy + Clone` for `PhantomData` to define the conversion - = note: required for `PhantomData` to implement `PyO3GetField` -note: required by a bound in `PyClassGetterGenerator::::generate` + = help: the trait `IntoPyObject<'_>` is not implemented for `PhantomData`, which is required by `for<'py> PhantomData: PyO3GetField<'py>` + = note: implement `IntoPyObject` for `&PhantomData` or `IntoPyObject + Clone` for `PhantomData` to define the conversion + = note: required for `PhantomData` to implement `for<'py> PyO3GetField<'py>` +note: required by a bound in `PyClassGetterGenerator::::generate` --> src/impl_/pyclass.rs | - | pub const fn generate(&self, name: &'static CStr, doc: &'static CStr) -> PyMethodDefType + | pub const fn generate(&self, _name: &'static CStr, _doc: &'static CStr) -> PyMethodDefType | -------- required by a bound in this associated function ... - | FieldT: PyO3GetField, - | ^^^^^^^^^^^^ required by this bound in `PyClassGetterGenerator::::generate` + | for<'py> FieldT: PyO3GetField<'py>, + | ^^^^^^^^^^^^^^^^^ required by this bound in `PyClassGetterGenerator::::generate` From 84138909a9393f7aeae83c71b75471492202024a Mon Sep 17 00:00:00 2001 From: Chayim Refael Friedman Date: Wed, 21 Aug 2024 17:17:53 +0300 Subject: [PATCH 226/495] Fix a soundness bug with `PyClassInitializer` (#4454) From now you cannot initialize a `PyClassInitializer` with `PyClassInitializer::from(Py).add_subclass(SubClass)`. This was out of bounds write. Now it panics. See details at https://github.com/PyO3/pyo3/issues/4452. --- newsfragments/4454.fixed.md | 1 + src/pyclass_init.rs | 49 +++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 newsfragments/4454.fixed.md diff --git a/newsfragments/4454.fixed.md b/newsfragments/4454.fixed.md new file mode 100644 index 00000000000..e7faf3e690d --- /dev/null +++ b/newsfragments/4454.fixed.md @@ -0,0 +1 @@ +Fix a soundness bug with `PyClassInitializer`: from now you cannot initialize a `PyClassInitializer` with `PyClassInitializer::from(Py).add_subclass(SubClass)`. \ No newline at end of file diff --git a/src/pyclass_init.rs b/src/pyclass_init.rs index 01983c79b13..2e391f38d16 100644 --- a/src/pyclass_init.rs +++ b/src/pyclass_init.rs @@ -27,6 +27,10 @@ pub trait PyObjectInit: Sized { py: Python<'_>, subtype: *mut PyTypeObject, ) -> PyResult<*mut ffi::PyObject>; + + #[doc(hidden)] + fn can_be_subclassed(&self) -> bool; + private_decl! {} } @@ -81,6 +85,11 @@ impl PyObjectInit for PyNativeTypeInitializer { inner(py, type_object, subtype) } + #[inline] + fn can_be_subclassed(&self) -> bool { + true + } + private_impl! {} } @@ -147,7 +156,14 @@ impl PyClassInitializer { /// Constructs a new initializer from value `T` and base class' initializer. /// /// It is recommended to use `add_subclass` instead of this method for most usage. + #[track_caller] + #[inline] pub fn new(init: T, super_init: ::Initializer) -> Self { + // This is unsound; see https://github.com/PyO3/pyo3/issues/4452. + assert!( + super_init.can_be_subclassed(), + "you cannot add a subclass to an existing value", + ); Self(PyClassInitializerImpl::New { init, super_init }) } @@ -197,6 +213,8 @@ impl PyClassInitializer { /// }) /// } /// ``` + #[track_caller] + #[inline] pub fn add_subclass(self, subclass_value: S) -> PyClassInitializer where S: PyClass, @@ -268,6 +286,11 @@ impl PyObjectInit for PyClassInitializer { .map(Bound::into_ptr) } + #[inline] + fn can_be_subclassed(&self) -> bool { + !matches!(self.0, PyClassInitializerImpl::Existing(..)) + } + private_impl! {} } @@ -288,6 +311,8 @@ where B: PyClass, B::BaseType: PyClassBaseType>, { + #[track_caller] + #[inline] fn from(sub_and_base: (S, B)) -> PyClassInitializer { let (sub, base) = sub_and_base; PyClassInitializer::from(base).add_subclass(sub) @@ -320,3 +345,27 @@ where Ok(self.into()) } } + +#[cfg(all(test, feature = "macros"))] +mod tests { + //! See https://github.com/PyO3/pyo3/issues/4452. + + use crate::prelude::*; + + #[pyclass(crate = "crate", subclass)] + struct BaseClass {} + + #[pyclass(crate = "crate", extends=BaseClass)] + struct SubClass { + _data: i32, + } + + #[test] + #[should_panic] + fn add_subclass_to_py_is_unsound() { + Python::with_gil(|py| { + let base = Py::new(py, BaseClass {}).unwrap(); + let _subclass = PyClassInitializer::from(base).add_subclass(SubClass { _data: 42 }); + }); + } +} From 5cc23d9f1e7e42a198539d70b43dae4f735585f5 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Wed, 21 Aug 2024 18:16:51 +0100 Subject: [PATCH 227/495] ffi: add compat functions for no-argument calls (#4461) --- newsfragments/4461.added.md | 1 + pyo3-ffi/src/compat/mod.rs | 2 ++ pyo3-ffi/src/compat/py_3_9.rs | 21 +++++++++++++++++++++ src/types/any.rs | 32 ++++++-------------------------- 4 files changed, 30 insertions(+), 26 deletions(-) create mode 100644 newsfragments/4461.added.md create mode 100644 pyo3-ffi/src/compat/py_3_9.rs diff --git a/newsfragments/4461.added.md b/newsfragments/4461.added.md new file mode 100644 index 00000000000..c151664c843 --- /dev/null +++ b/newsfragments/4461.added.md @@ -0,0 +1 @@ +Add FFI definitions `compat::PyObject_CallNoArgs` and `compat::PyObject_CallMethodNoArgs`. diff --git a/pyo3-ffi/src/compat/mod.rs b/pyo3-ffi/src/compat/mod.rs index db41c834b3d..11f2912848e 100644 --- a/pyo3-ffi/src/compat/mod.rs +++ b/pyo3-ffi/src/compat/mod.rs @@ -52,6 +52,8 @@ macro_rules! compat_function { mod py_3_10; mod py_3_13; +mod py_3_9; pub use self::py_3_10::*; pub use self::py_3_13::*; +pub use self::py_3_9::*; diff --git a/pyo3-ffi/src/compat/py_3_9.rs b/pyo3-ffi/src/compat/py_3_9.rs new file mode 100644 index 00000000000..285f2b2ae7e --- /dev/null +++ b/pyo3-ffi/src/compat/py_3_9.rs @@ -0,0 +1,21 @@ +compat_function!( + originally_defined_for(all( + not(PyPy), + not(GraalPy), + any(Py_3_10, all(not(Py_LIMITED_API), Py_3_9)) // Added to python in 3.9 but to limited API in 3.10 + )); + + #[inline] + pub unsafe fn PyObject_CallNoArgs(obj: *mut crate::PyObject) -> *mut crate::PyObject { + crate::PyObject_CallObject(obj, std::ptr::null_mut()) + } +); + +compat_function!( + originally_defined_for(all(Py_3_9, not(any(Py_LIMITED_API, PyPy, GraalPy)))); + + #[inline] + pub unsafe fn PyObject_CallMethodNoArgs(obj: *mut crate::PyObject, name: *mut crate::PyObject) -> *mut crate::PyObject { + crate::PyObject_CallMethodObjArgs(obj, name, std::ptr::null_mut::()) + } +); diff --git a/src/types/any.rs b/src/types/any.rs index e7c7c578e3e..6dff9a1264d 100644 --- a/src/types/any.rs +++ b/src/types/any.rs @@ -1180,20 +1180,7 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { } fn call0(&self) -> PyResult> { - cfg_if::cfg_if! { - if #[cfg(all( - not(PyPy), - not(GraalPy), - any(Py_3_10, all(not(Py_LIMITED_API), Py_3_9)) // PyObject_CallNoArgs was added to python in 3.9 but to limited API in 3.10 - ))] { - // Optimized path on python 3.9+ - unsafe { - ffi::PyObject_CallNoArgs(self.as_ptr()).assume_owned_or_err(self.py()) - } - } else { - self.call((), None) - } - } + unsafe { ffi::compat::PyObject_CallNoArgs(self.as_ptr()).assume_owned_or_err(self.py()) } } fn call1(&self, args: impl IntoPy>) -> PyResult> { @@ -1218,18 +1205,11 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { where N: IntoPy>, { - cfg_if::cfg_if! { - if #[cfg(all(Py_3_9, not(any(Py_LIMITED_API, PyPy, GraalPy))))] { - let py = self.py(); - - // Optimized path on python 3.9+ - unsafe { - let name = name.into_py(py).into_bound(py); - ffi::PyObject_CallMethodNoArgs(self.as_ptr(), name.as_ptr()).assume_owned_or_err(py) - } - } else { - self.call_method(name, (), None) - } + let py = self.py(); + let name = name.into_py(py).into_bound(py); + unsafe { + ffi::compat::PyObject_CallMethodNoArgs(self.as_ptr(), name.as_ptr()) + .assume_owned_or_err(py) } } From 2f5b45efa10e52a44bb3185dfeb716c2e815a4f8 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Wed, 21 Aug 2024 20:20:42 +0100 Subject: [PATCH 228/495] ffi: update object.rs for 3.13 (#4447) * ffi: update `object.rs` for 3.13 * add newsfragment * fixup `_Py_IsImmortal` cfgs * also update `cpython/object.rs` * fix `Py_Is` on PyPy 3.10 * `Py_GetConstant` & `Py_GetConstantBorrowed` are pub * [review] mejrs feedback * correct typo Co-authored-by: Alex Gaynor * fix freethreaded build --------- Co-authored-by: Alex Gaynor --- Architecture.md | 7 +- newsfragments/4447.added.md | 1 + newsfragments/4447.fixed.md | 1 + newsfragments/4447.removed.md | 1 + pyo3-ffi/src/abstract_.rs | 14 +- pyo3-ffi/src/boolobject.rs | 6 - pyo3-ffi/src/cpython/object.rs | 131 +++++++++-------- pyo3-ffi/src/impl_/mod.rs | 22 +++ pyo3-ffi/src/lib.rs | 1 + pyo3-ffi/src/longobject.rs | 6 - pyo3-ffi/src/object.rs | 254 +++++++++++++++++++++++---------- 11 files changed, 279 insertions(+), 165 deletions(-) create mode 100644 newsfragments/4447.added.md create mode 100644 newsfragments/4447.fixed.md create mode 100644 newsfragments/4447.removed.md create mode 100644 pyo3-ffi/src/impl_/mod.rs diff --git a/Architecture.md b/Architecture.md index a4218a7f71f..be9d2b53d82 100644 --- a/Architecture.md +++ b/Architecture.md @@ -37,12 +37,9 @@ automated tooling because: - it gives us best control about how to adapt C conventions to Rust, and - there are many Python interpreter versions we support in a single set of files. -We aim to provide straight-forward Rust wrappers resembling the file structure of -[`cpython/Include`](https://github.com/python/cpython/tree/v3.9.2/Include). +We aim to provide straight-forward Rust wrappers resembling the file structure of [`cpython/Include`](https://github.com/python/cpython/tree/3.13/Include). -However, we still lack some APIs and are continuously updating the module to match -the file contents upstream in CPython. -The tracking issue is [#1289](https://github.com/PyO3/pyo3/issues/1289), and contribution is welcome. +We are continuously updating the module to match the latest CPython version which PyO3 supports (i.e. as of time of writing Python 3.13). The tracking issue is [#1289](https://github.com/PyO3/pyo3/issues/1289), and contribution is welcome. In the [`pyo3-ffi`] crate, there is lots of conditional compilation such as `#[cfg(Py_LIMITED_API)]`, `#[cfg(Py_3_7)]`, and `#[cfg(PyPy)]`. diff --git a/newsfragments/4447.added.md b/newsfragments/4447.added.md new file mode 100644 index 00000000000..a1e6b5eaa39 --- /dev/null +++ b/newsfragments/4447.added.md @@ -0,0 +1 @@ +Add Python 3.13 FFI definitions `PyObject_GetOptionalAttr`, `PyObject_GetOptionalAttrString`, `PyObject_HasAttrWithError`, `PyObject_HasAttrStringWithError`, `Py_CONSTANT_*` constants, `Py_GetConstant`, `Py_GetConstantBorrowed`, and `PyType_GetModuleByDef`. diff --git a/newsfragments/4447.fixed.md b/newsfragments/4447.fixed.md new file mode 100644 index 00000000000..bfc92ae1d26 --- /dev/null +++ b/newsfragments/4447.fixed.md @@ -0,0 +1 @@ +Fix FFI definition `Py_Is` for PyPy on 3.10 to call the function defined by PyPy. diff --git a/newsfragments/4447.removed.md b/newsfragments/4447.removed.md new file mode 100644 index 00000000000..c7451866d83 --- /dev/null +++ b/newsfragments/4447.removed.md @@ -0,0 +1 @@ +Remove private FFI definitions `_Py_IMMORTAL_REFCNT`, `_Py_IsImmortal`, `_Py_TPFLAGS_STATIC_BUILTIN`, `_Py_Dealloc`, `_Py_IncRef`, `_Py_DecRef`. diff --git a/pyo3-ffi/src/abstract_.rs b/pyo3-ffi/src/abstract_.rs index 175f9af734f..1899545011a 100644 --- a/pyo3-ffi/src/abstract_.rs +++ b/pyo3-ffi/src/abstract_.rs @@ -1,23 +1,17 @@ use crate::object::*; use crate::pyport::Py_ssize_t; use std::os::raw::{c_char, c_int}; -use std::ptr; - -extern "C" { - #[cfg(PyPy)] - #[link_name = "PyPyObject_DelAttrString"] - pub fn PyObject_DelAttrString(o: *mut PyObject, attr_name: *const c_char) -> c_int; -} #[inline] -#[cfg(not(PyPy))] +#[cfg(all(not(Py_3_13), not(PyPy)))] // CPython exposed as a function in 3.13, in object.h pub unsafe fn PyObject_DelAttrString(o: *mut PyObject, attr_name: *const c_char) -> c_int { - PyObject_SetAttrString(o, attr_name, ptr::null_mut()) + PyObject_SetAttrString(o, attr_name, std::ptr::null_mut()) } #[inline] +#[cfg(all(not(Py_3_13), not(PyPy)))] // CPython exposed as a function in 3.13, in object.h pub unsafe fn PyObject_DelAttr(o: *mut PyObject, attr_name: *mut PyObject) -> c_int { - PyObject_SetAttr(o, attr_name, ptr::null_mut()) + PyObject_SetAttr(o, attr_name, std::ptr::null_mut()) } extern "C" { diff --git a/pyo3-ffi/src/boolobject.rs b/pyo3-ffi/src/boolobject.rs index 10b5969fa4f..eec9da707a1 100644 --- a/pyo3-ffi/src/boolobject.rs +++ b/pyo3-ffi/src/boolobject.rs @@ -4,12 +4,6 @@ use crate::object::*; use std::os::raw::{c_int, c_long}; use std::ptr::addr_of_mut; -#[cfg_attr(windows, link(name = "pythonXY"))] -extern "C" { - #[cfg_attr(PyPy, link_name = "PyPyBool_Type")] - pub static mut PyBool_Type: PyTypeObject; -} - #[inline] pub unsafe fn PyBool_Check(op: *mut PyObject) -> c_int { (Py_TYPE(op) == addr_of_mut!(PyBool_Type)) as c_int diff --git a/pyo3-ffi/src/cpython/object.rs b/pyo3-ffi/src/cpython/object.rs index 871f3b883d9..23d7f94081e 100644 --- a/pyo3-ffi/src/cpython/object.rs +++ b/pyo3-ffi/src/cpython/object.rs @@ -1,20 +1,23 @@ #[cfg(Py_3_8)] use crate::vectorcallfunc; -#[cfg(Py_3_11)] -use crate::PyModuleDef; use crate::{object, PyGetSetDef, PyMemberDef, PyMethodDef, PyObject, Py_ssize_t}; use std::mem; -use std::os::raw::{c_char, c_int, c_uint, c_ulong, c_void}; +use std::os::raw::{c_char, c_int, c_uint, c_void}; + +// skipped private _Py_NewReference +// skipped private _Py_NewReferenceNoTotal +// skipped private _Py_ResurrectReference -// skipped _Py_NewReference -// skipped _Py_ForgetReference -// skipped _Py_GetRefTotal +// skipped private _Py_GetGlobalRefTotal +// skipped private _Py_GetRefTotal +// skipped private _Py_GetLegacyRefTotal +// skipped private _PyInterpreterState_GetRefTotal -// skipped _Py_Identifier +// skipped private _Py_Identifier -// skipped _Py_static_string_init -// skipped _Py_static_string -// skipped _Py_IDENTIFIER +// skipped private _Py_static_string_init +// skipped private _Py_static_string +// skipped private _Py_IDENTIFIER #[cfg(not(Py_3_11))] // moved to src/buffer.rs from Python mod bufferinfo { @@ -240,7 +243,10 @@ pub struct PyTypeObject { pub tp_getattro: Option, pub tp_setattro: Option, pub tp_as_buffer: *mut PyBufferProcs, - pub tp_flags: c_ulong, + #[cfg(not(Py_GIL_DISABLED))] + pub tp_flags: std::os::raw::c_ulong, + #[cfg(Py_GIL_DISABLED)] + pub tp_flags: crate::impl_::AtomicCULong, pub tp_doc: *const c_char, pub tp_traverse: Option, pub tp_clear: Option, @@ -292,12 +298,12 @@ pub struct PyTypeObject { #[cfg(Py_3_11)] #[repr(C)] #[derive(Clone)] -pub struct _specialization_cache { - pub getitem: *mut PyObject, +struct _specialization_cache { + getitem: *mut PyObject, #[cfg(Py_3_12)] - pub getitem_version: u32, + getitem_version: u32, #[cfg(Py_3_13)] - pub init: *mut PyObject, + init: *mut PyObject, } #[repr(C)] @@ -316,9 +322,9 @@ pub struct PyHeapTypeObject { #[cfg(Py_3_9)] pub ht_module: *mut object::PyObject, #[cfg(Py_3_11)] - pub _ht_tpname: *mut c_char, + _ht_tpname: *mut c_char, #[cfg(Py_3_11)] - pub _spec_cache: _specialization_cache, + _spec_cache: _specialization_cache, } impl Default for PyHeapTypeObject { @@ -329,82 +335,75 @@ impl Default for PyHeapTypeObject { } #[inline] +#[cfg(not(Py_3_11))] pub unsafe fn PyHeapType_GET_MEMBERS(etype: *mut PyHeapTypeObject) -> *mut PyMemberDef { let py_type = object::Py_TYPE(etype as *mut object::PyObject); let ptr = etype.offset((*py_type).tp_basicsize); ptr as *mut PyMemberDef } -// skipped _PyType_Name -// skipped _PyType_Lookup -// skipped _PyType_LookupId -// skipped _PyObject_LookupSpecial -// skipped _PyType_CalculateMetaclass -// skipped _PyType_GetDocFromInternalDoc -// skipped _PyType_GetTextSignatureFromInternalDoc +// skipped private _PyType_Name +// skipped private _PyType_Lookup +// skipped private _PyType_LookupRef extern "C" { - #[cfg(Py_3_11)] - #[cfg_attr(PyPy, link_name = "PyPyType_GetModuleByDef")] - pub fn PyType_GetModuleByDef(ty: *mut PyTypeObject, def: *mut PyModuleDef) -> *mut PyObject; - #[cfg(Py_3_12)] pub fn PyType_GetDict(o: *mut PyTypeObject) -> *mut PyObject; #[cfg_attr(PyPy, link_name = "PyPyObject_Print")] pub fn PyObject_Print(o: *mut PyObject, fp: *mut ::libc::FILE, flags: c_int) -> c_int; - // skipped _Py_BreakPoint - // skipped _PyObject_Dump - // skipped _PyObject_IsFreed - // skipped _PyObject_IsAbstract + // skipped private _Py_BreakPoint + // skipped private _PyObject_Dump + // skipped _PyObject_GetAttrId - // skipped _PyObject_SetAttrId - // skipped _PyObject_LookupAttr - // skipped _PyObject_LookupAttrId - // skipped _PyObject_GetMethod - #[cfg(not(PyPy))] - pub fn _PyObject_GetDictPtr(obj: *mut PyObject) -> *mut *mut PyObject; - #[cfg(not(PyPy))] - pub fn _PyObject_NextNotImplemented(arg1: *mut PyObject) -> *mut PyObject; + // skipped private _PyObject_GetDictPtr pub fn PyObject_CallFinalizer(arg1: *mut PyObject); #[cfg_attr(PyPy, link_name = "PyPyObject_CallFinalizerFromDealloc")] pub fn PyObject_CallFinalizerFromDealloc(arg1: *mut PyObject) -> c_int; - // skipped _PyObject_GenericGetAttrWithDict - // skipped _PyObject_GenericSetAttrWithDict - // skipped _PyObject_FunctionStr + // skipped private _PyObject_GenericGetAttrWithDict + // skipped private _PyObject_GenericSetAttrWithDict + // skipped private _PyObject_FunctionStr } // skipped Py_SETREF // skipped Py_XSETREF -#[cfg_attr(windows, link(name = "pythonXY"))] -extern "C" { - pub static mut _PyNone_Type: PyTypeObject; - pub static mut _PyNotImplemented_Type: PyTypeObject; -} - -// skipped _Py_SwappedOp +// skipped private _PyObject_ASSERT_FROM +// skipped private _PyObject_ASSERT_WITH_MSG +// skipped private _PyObject_ASSERT +// skipped private _PyObject_ASSERT_FAILED_MSG +// skipped private _PyObject_AssertFailed -// skipped _PyDebugAllocatorStats -// skipped _PyObject_DebugTypeStats -// skipped _PyObject_ASSERT_FROM -// skipped _PyObject_ASSERT_WITH_MSG -// skipped _PyObject_ASSERT -// skipped _PyObject_ASSERT_FAILED_MSG -// skipped _PyObject_AssertFailed -// skipped _PyObject_CheckConsistency +// skipped private _PyTrash_begin +// skipped private _PyTrash_end // skipped _PyTrash_thread_deposit_object // skipped _PyTrash_thread_destroy_chain -// skipped _PyTrash_begin -// skipped _PyTrash_end -// skipped _PyTrash_cond -// skipped PyTrash_UNWIND_LEVEL -// skipped Py_TRASHCAN_BEGIN_CONDITION -// skipped Py_TRASHCAN_END + // skipped Py_TRASHCAN_BEGIN -// skipped Py_TRASHCAN_SAFE_BEGIN -// skipped Py_TRASHCAN_SAFE_END +// skipped Py_TRASHCAN_END + +// skipped PyObject_GetItemData + +// skipped PyObject_VisitManagedDict +// skipped _PyObject_SetManagedDict +// skipped PyObject_ClearManagedDict + +// skipped TYPE_MAX_WATCHERS + +// skipped PyType_WatchCallback +// skipped PyType_AddWatcher +// skipped PyType_ClearWatcher +// skipped PyType_Watch +// skipped PyType_Unwatch + +// skipped PyUnstable_Type_AssignVersionTag + +// skipped PyRefTracerEvent + +// skipped PyRefTracer +// skipped PyRefTracer_SetTracer +// skipped PyRefTracer_GetTracer diff --git a/pyo3-ffi/src/impl_/mod.rs b/pyo3-ffi/src/impl_/mod.rs new file mode 100644 index 00000000000..3058e852e6f --- /dev/null +++ b/pyo3-ffi/src/impl_/mod.rs @@ -0,0 +1,22 @@ +#[cfg(Py_GIL_DISABLED)] +mod atomic_c_ulong { + pub struct GetAtomicCULong(); + + pub trait AtomicCULongType { + type Type; + } + impl AtomicCULongType for GetAtomicCULong<32> { + type Type = std::sync::atomic::AtomicU32; + } + impl AtomicCULongType for GetAtomicCULong<64> { + type Type = std::sync::atomic::AtomicU64; + } + + pub type TYPE = + () * 8 }> as AtomicCULongType>::Type; +} + +/// Typedef for an atomic integer to match the platform-dependent c_ulong type. +#[cfg(Py_GIL_DISABLED)] +#[doc(hidden)] +pub type AtomicCULong = atomic_c_ulong::TYPE; diff --git a/pyo3-ffi/src/lib.rs b/pyo3-ffi/src/lib.rs index 4b5b3e390ad..1e3f804b1f4 100644 --- a/pyo3-ffi/src/lib.rs +++ b/pyo3-ffi/src/lib.rs @@ -292,6 +292,7 @@ pub const fn _cstr_from_utf8_with_nul_checked(s: &str) -> &CStr { use std::ffi::CStr; pub mod compat; +mod impl_; pub use self::abstract_::*; pub use self::bltinmodule::*; diff --git a/pyo3-ffi/src/longobject.rs b/pyo3-ffi/src/longobject.rs index 25fa12dbbfc..68b4ecba540 100644 --- a/pyo3-ffi/src/longobject.rs +++ b/pyo3-ffi/src/longobject.rs @@ -6,12 +6,6 @@ use std::ptr::addr_of_mut; opaque_struct!(PyLongObject); -#[cfg_attr(windows, link(name = "pythonXY"))] -extern "C" { - #[cfg_attr(PyPy, link_name = "PyPyLong_Type")] - pub static mut PyLong_Type: PyTypeObject; -} - #[inline] pub unsafe fn PyLong_Check(op: *mut PyObject) -> c_int { PyType_FastSubclass(Py_TYPE(op), Py_TPFLAGS_LONG_SUBCLASS) diff --git a/pyo3-ffi/src/object.rs b/pyo3-ffi/src/object.rs index 5e8b6af44a1..deb67caf768 100644 --- a/pyo3-ffi/src/object.rs +++ b/pyo3-ffi/src/object.rs @@ -15,11 +15,8 @@ opaque_struct!(PyTypeObject); #[cfg(not(Py_LIMITED_API))] pub use crate::cpython::object::PyTypeObject; -// _PyObject_HEAD_EXTRA: conditionally defined in PyObject_HEAD_INIT -// _PyObject_EXTRA_INIT: conditionally defined in PyObject_HEAD_INIT - #[cfg(Py_3_12)] -pub const _Py_IMMORTAL_REFCNT: Py_ssize_t = { +const _Py_IMMORTAL_REFCNT: Py_ssize_t = { if cfg!(target_pointer_width = "64") { c_uint::MAX as Py_ssize_t } else { @@ -29,9 +26,7 @@ pub const _Py_IMMORTAL_REFCNT: Py_ssize_t = { }; #[cfg(Py_GIL_DISABLED)] -pub const _Py_IMMORTAL_REFCNT_LOCAL: u32 = u32::MAX; -#[cfg(Py_GIL_DISABLED)] -pub const _Py_REF_SHARED_SHIFT: isize = 2; +const _Py_IMMORTAL_REFCNT_LOCAL: u32 = u32::MAX; #[allow(clippy::declare_interior_mutable_const)] pub const PyObject_HEAD_INIT: PyObject = PyObject { @@ -66,9 +61,22 @@ pub const PyObject_HEAD_INIT: PyObject = PyObject { // skipped PyObject_VAR_HEAD // skipped Py_INVALID_SIZE +// skipped private _Py_UNOWNED_TID + +#[cfg(Py_GIL_DISABLED)] +const _Py_REF_SHARED_SHIFT: isize = 2; +// skipped private _Py_REF_SHARED_FLAG_MASK + +// skipped private _Py_REF_SHARED_INIT +// skipped private _Py_REF_MAYBE_WEAKREF +// skipped private _Py_REF_QUEUED +// skipped private _Py_REF_MERGED + +// skipped private _Py_REF_SHARED + #[repr(C)] #[derive(Copy, Clone)] -#[cfg(Py_3_12)] +#[cfg(all(Py_3_12, not(Py_GIL_DISABLED)))] /// This union is anonymous in CPython, so the name was given by PyO3 because /// Rust unions need a name. pub union PyObjectObRefcnt { @@ -77,14 +85,14 @@ pub union PyObjectObRefcnt { pub ob_refcnt_split: [crate::PY_UINT32_T; 2], } -#[cfg(Py_3_12)] +#[cfg(all(Py_3_12, not(Py_GIL_DISABLED)))] impl std::fmt::Debug for PyObjectObRefcnt { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", unsafe { self.ob_refcnt }) } } -#[cfg(not(Py_3_12))] +#[cfg(all(not(Py_3_12), not(Py_GIL_DISABLED)))] pub type PyObjectObRefcnt = Py_ssize_t; #[repr(C)] @@ -113,7 +121,7 @@ pub struct PyObject { pub ob_type: *mut PyTypeObject, } -// skipped _PyObject_CAST +// skipped private _PyObject_CAST #[repr(C)] #[derive(Debug)] @@ -123,38 +131,54 @@ pub struct PyVarObject { pub ob_size: Py_ssize_t, } -// skipped _PyVarObject_CAST +// skipped private _PyVarObject_CAST #[inline] +#[cfg(not(all(PyPy, Py_3_10)))] +#[cfg_attr(docsrs, doc(cfg(all())))] pub unsafe fn Py_Is(x: *mut PyObject, y: *mut PyObject) -> c_int { (x == y).into() } -#[inline] -#[cfg(Py_GIL_DISABLED)] -pub unsafe fn Py_REFCNT(ob: *mut PyObject) -> Py_ssize_t { - let local = (*ob).ob_ref_local.load(Relaxed); - if local == _Py_IMMORTAL_REFCNT_LOCAL { - return _Py_IMMORTAL_REFCNT; - } - let shared = (*ob).ob_ref_shared.load(Relaxed); - local as Py_ssize_t + Py_ssize_t::from(shared >> _Py_REF_SHARED_SHIFT) +#[cfg(all(PyPy, Py_3_10))] +#[cfg_attr(docsrs, doc(cfg(all())))] +extern "C" { + #[cfg_attr(PyPy, link_name = "PyPy_Is")] + pub fn Py_Is(x: *mut PyObject, y: *mut PyObject) -> c_int; } -#[inline] -#[cfg(not(Py_GIL_DISABLED))] -#[cfg(Py_3_12)] -pub unsafe fn Py_REFCNT(ob: *mut PyObject) -> Py_ssize_t { - (*ob).ob_refcnt.ob_refcnt -} +// skipped private _Py_GetThreadLocal_Addr + +// skipped private _Py_ThreadId + +// skipped private _Py_IsOwnedByCurrentThread #[inline] -#[cfg(not(Py_3_12))] pub unsafe fn Py_REFCNT(ob: *mut PyObject) -> Py_ssize_t { - #[cfg(not(GraalPy))] - return (*ob).ob_refcnt; - #[cfg(GraalPy)] - return _Py_REFCNT(ob); + #[cfg(Py_GIL_DISABLED)] + { + let local = (*ob).ob_ref_local.load(Relaxed); + if local == _Py_IMMORTAL_REFCNT_LOCAL { + return _Py_IMMORTAL_REFCNT; + } + let shared = (*ob).ob_ref_shared.load(Relaxed); + local as Py_ssize_t + Py_ssize_t::from(shared >> _Py_REF_SHARED_SHIFT) + } + + #[cfg(all(not(Py_GIL_DISABLED), Py_3_12))] + { + (*ob).ob_refcnt.ob_refcnt + } + + #[cfg(all(not(Py_GIL_DISABLED), not(Py_3_12), not(GraalPy)))] + { + (*ob).ob_refcnt + } + + #[cfg(all(not(Py_GIL_DISABLED), not(Py_3_12), GraalPy))] + { + _Py_REFCNT(ob) + } } #[inline] @@ -165,8 +189,13 @@ pub unsafe fn Py_TYPE(ob: *mut PyObject) -> *mut PyTypeObject { return _Py_TYPE(ob); } -// PyLong_Type defined in longobject.rs -// PyBool_Type defined in boolobject.rs +#[cfg_attr(windows, link(name = "pythonXY"))] +extern "C" { + #[cfg_attr(PyPy, link_name = "PyPyLong_Type")] + pub static mut PyLong_Type: PyTypeObject; + #[cfg_attr(PyPy, link_name = "PyPyBool_Type")] + pub static mut PyBool_Type: PyTypeObject; +} #[inline] pub unsafe fn Py_SIZE(ob: *mut PyObject) -> Py_ssize_t { @@ -180,28 +209,31 @@ pub unsafe fn Py_SIZE(ob: *mut PyObject) -> Py_ssize_t { _Py_SIZE(ob) } +#[inline(always)] +#[cfg(all(Py_3_12, not(Py_GIL_DISABLED)))] +unsafe fn _Py_IsImmortal(op: *mut PyObject) -> c_int { + #[cfg(target_pointer_width = "64")] + { + (((*op).ob_refcnt.ob_refcnt as crate::PY_INT32_T) < 0) as c_int + } + + #[cfg(target_pointer_width = "32")] + { + ((*op).ob_refcnt.ob_refcnt == _Py_IMMORTAL_REFCNT) as c_int + } +} + #[inline] pub unsafe fn Py_IS_TYPE(ob: *mut PyObject, tp: *mut PyTypeObject) -> c_int { (Py_TYPE(ob) == tp) as c_int } -#[inline(always)] -#[cfg(all(not(Py_GIL_DISABLED), Py_3_12, target_pointer_width = "64"))] -pub unsafe fn _Py_IsImmortal(op: *mut PyObject) -> c_int { - (((*op).ob_refcnt.ob_refcnt as crate::PY_INT32_T) < 0) as c_int -} - -#[inline(always)] -#[cfg(all(Py_3_12, target_pointer_width = "32"))] -pub unsafe fn _Py_IsImmortal(op: *mut PyObject) -> c_int { - ((*op).ob_refcnt.ob_refcnt == _Py_IMMORTAL_REFCNT) as c_int -} +// skipped _Py_SetRefCnt -// skipped _Py_SET_REFCNT // skipped Py_SET_REFCNT -// skipped _Py_SET_TYPE + // skipped Py_SET_TYPE -// skipped _Py_SET_SIZE + // skipped Py_SET_SIZE pub type unaryfunc = unsafe extern "C" fn(*mut PyObject) -> *mut PyObject; @@ -344,7 +376,7 @@ extern "C" { #[inline] pub unsafe fn PyObject_TypeCheck(ob: *mut PyObject, tp: *mut PyTypeObject) -> c_int { - (Py_TYPE(ob) == tp || PyType_IsSubtype(Py_TYPE(ob), tp) != 0) as c_int + (Py_IS_TYPE(ob, tp) != 0 || PyType_IsSubtype(Py_TYPE(ob), tp) != 0) as c_int } #[cfg_attr(windows, link(name = "pythonXY"))] @@ -401,18 +433,43 @@ extern "C" { arg2: *const c_char, arg3: *mut PyObject, ) -> c_int; + #[cfg(any(Py_3_13, PyPy))] // CPython defined in 3.12 as an inline function in abstract.h + #[cfg_attr(PyPy, link_name = "PyPyObject_DelAttrString")] + pub fn PyObject_DelAttrString(arg1: *mut PyObject, arg2: *const c_char) -> c_int; #[cfg_attr(PyPy, link_name = "PyPyObject_HasAttrString")] pub fn PyObject_HasAttrString(arg1: *mut PyObject, arg2: *const c_char) -> c_int; #[cfg_attr(PyPy, link_name = "PyPyObject_GetAttr")] pub fn PyObject_GetAttr(arg1: *mut PyObject, arg2: *mut PyObject) -> *mut PyObject; + #[cfg(Py_3_13)] + #[cfg_attr(PyPy, link_name = "PyPyObject_GetOptionalAttr")] + pub fn PyObject_GetOptionalAttr( + arg1: *mut PyObject, + arg2: *mut PyObject, + arg3: *mut *mut PyObject, + ) -> c_int; + #[cfg(Py_3_13)] + #[cfg_attr(PyPy, link_name = "PyPyObject_GetOptionalAttrString")] + pub fn PyObject_GetOptionalAttrString( + arg1: *mut PyObject, + arg2: *const c_char, + arg3: *mut *mut PyObject, + ) -> c_int; #[cfg_attr(PyPy, link_name = "PyPyObject_SetAttr")] pub fn PyObject_SetAttr(arg1: *mut PyObject, arg2: *mut PyObject, arg3: *mut PyObject) -> c_int; + #[cfg(any(Py_3_13, PyPy))] // CPython defined in 3.12 as an inline function in abstract.h + #[cfg_attr(PyPy, link_name = "PyPyObject_DelAttr")] + pub fn PyObject_DelAttr(arg1: *mut PyObject, arg2: *mut PyObject) -> c_int; #[cfg_attr(PyPy, link_name = "PyPyObject_HasAttr")] pub fn PyObject_HasAttr(arg1: *mut PyObject, arg2: *mut PyObject) -> c_int; + #[cfg(Py_3_13)] + #[cfg_attr(PyPy, link_name = "PyPyObject_HasAttrWithError")] + pub fn PyObject_HasAttrWithError(arg1: *mut PyObject, arg2: *mut PyObject) -> c_int; + #[cfg(Py_3_13)] + #[cfg_attr(PyPy, link_name = "PyPyObject_HasAttrStringWithError")] + pub fn PyObject_HasAttrStringWithError(arg1: *mut PyObject, arg2: *const c_char) -> c_int; #[cfg_attr(PyPy, link_name = "PyPyObject_SelfIter")] pub fn PyObject_SelfIter(arg1: *mut PyObject) -> *mut PyObject; - #[cfg_attr(PyPy, link_name = "PyPyObject_GenericGetAttr")] pub fn PyObject_GenericGetAttr(arg1: *mut PyObject, arg2: *mut PyObject) -> *mut PyObject; #[cfg_attr(PyPy, link_name = "PyPyObject_GenericSetAttr")] @@ -422,7 +479,9 @@ extern "C" { arg3: *mut PyObject, ) -> c_int; #[cfg(not(all(Py_LIMITED_API, not(Py_3_10))))] + #[cfg_attr(PyPy, link_name = "PyPyObject_GenericGetDict")] pub fn PyObject_GenericGetDict(arg1: *mut PyObject, arg2: *mut c_void) -> *mut PyObject; + #[cfg_attr(PyPy, link_name = "PyPyObject_GenericSetDict")] pub fn PyObject_GenericSetDict( arg1: *mut PyObject, arg2: *mut PyObject, @@ -450,8 +509,8 @@ extern "C" { // Flag bits for printing: pub const Py_PRINT_RAW: c_int = 1; // No string quotes etc. -#[cfg(all(Py_3_12, not(Py_LIMITED_API)))] -pub const _Py_TPFLAGS_STATIC_BUILTIN: c_ulong = 1 << 1; +// skipped because is a private API +// const _Py_TPFLAGS_STATIC_BUILTIN: c_ulong = 1 << 1; #[cfg(all(Py_3_12, not(Py_LIMITED_API)))] pub const Py_TPFLAGS_MANAGED_WEAKREF: c_ulong = 1 << 3; @@ -480,7 +539,7 @@ pub const Py_TPFLAGS_BASETYPE: c_ulong = 1 << 10; /// Set if the type implements the vectorcall protocol (PEP 590) #[cfg(any(Py_3_12, all(Py_3_8, not(Py_LIMITED_API))))] pub const Py_TPFLAGS_HAVE_VECTORCALL: c_ulong = 1 << 11; -// skipped non-limited _Py_TPFLAGS_HAVE_VECTORCALL +// skipped backwards-compatibility alias _Py_TPFLAGS_HAVE_VECTORCALL /// Set if the type is 'ready' -- fully initialized pub const Py_TPFLAGS_READY: c_ulong = 1 << 12; @@ -526,14 +585,14 @@ pub const Py_TPFLAGS_HAVE_VERSION_TAG: c_ulong = 1 << 18; extern "C" { #[cfg(all(py_sys_config = "Py_REF_DEBUG", not(Py_LIMITED_API)))] - pub fn _Py_NegativeRefcount(filename: *const c_char, lineno: c_int, op: *mut PyObject); + fn _Py_NegativeRefcount(filename: *const c_char, lineno: c_int, op: *mut PyObject); #[cfg(all(Py_3_12, py_sys_config = "Py_REF_DEBUG", not(Py_LIMITED_API)))] fn _Py_INCREF_IncRefTotal(); #[cfg(all(Py_3_12, py_sys_config = "Py_REF_DEBUG", not(Py_LIMITED_API)))] fn _Py_DECREF_DecRefTotal(); #[cfg_attr(PyPy, link_name = "_PyPy_Dealloc")] - pub fn _Py_Dealloc(arg1: *mut PyObject); + fn _Py_Dealloc(arg1: *mut PyObject); #[cfg_attr(PyPy, link_name = "PyPy_IncRef")] #[cfg_attr(GraalPy, link_name = "_Py_IncRef")] @@ -543,18 +602,18 @@ extern "C" { pub fn Py_DecRef(o: *mut PyObject); #[cfg(all(Py_3_10, not(PyPy)))] - pub fn _Py_IncRef(o: *mut PyObject); + fn _Py_IncRef(o: *mut PyObject); #[cfg(all(Py_3_10, not(PyPy)))] - pub fn _Py_DecRef(o: *mut PyObject); + fn _Py_DecRef(o: *mut PyObject); #[cfg(GraalPy)] - pub fn _Py_REFCNT(arg1: *const PyObject) -> Py_ssize_t; + fn _Py_REFCNT(arg1: *const PyObject) -> Py_ssize_t; #[cfg(GraalPy)] - pub fn _Py_TYPE(arg1: *const PyObject) -> *mut PyTypeObject; + fn _Py_TYPE(arg1: *const PyObject) -> *mut PyTypeObject; #[cfg(GraalPy)] - pub fn _Py_SIZE(arg1: *const PyObject) -> Py_ssize_t; + fn _Py_SIZE(arg1: *const PyObject) -> Py_ssize_t; } #[inline(always)] @@ -740,9 +799,39 @@ pub unsafe fn Py_XNewRef(obj: *mut PyObject) -> *mut PyObject { obj } +#[cfg(Py_3_13)] +pub const Py_CONSTANT_NONE: c_uint = 0; +#[cfg(Py_3_13)] +pub const Py_CONSTANT_FALSE: c_uint = 1; +#[cfg(Py_3_13)] +pub const Py_CONSTANT_TRUE: c_uint = 2; +#[cfg(Py_3_13)] +pub const Py_CONSTANT_ELLIPSIS: c_uint = 3; +#[cfg(Py_3_13)] +pub const Py_CONSTANT_NOT_IMPLEMENTED: c_uint = 4; +#[cfg(Py_3_13)] +pub const Py_CONSTANT_ZERO: c_uint = 5; +#[cfg(Py_3_13)] +pub const Py_CONSTANT_ONE: c_uint = 6; +#[cfg(Py_3_13)] +pub const Py_CONSTANT_EMPTY_STR: c_uint = 7; +#[cfg(Py_3_13)] +pub const Py_CONSTANT_EMPTY_BYTES: c_uint = 8; +#[cfg(Py_3_13)] +pub const Py_CONSTANT_EMPTY_TUPLE: c_uint = 9; + +extern "C" { + #[cfg(Py_3_13)] + #[cfg_attr(PyPy, link_name = "PyPy_GetConstant")] + pub fn Py_GetConstant(constant_id: c_uint) -> *mut PyObject; + #[cfg(Py_3_13)] + #[cfg_attr(PyPy, link_name = "PyPy_GetConstantBorrowed")] + pub fn Py_GetConstantBorrowed(constant_id: c_uint) -> *mut PyObject; +} + #[cfg_attr(windows, link(name = "pythonXY"))] extern "C" { - #[cfg(not(GraalPy))] + #[cfg(all(not(GraalPy), not(all(Py_3_13, Py_LIMITED_API))))] #[cfg_attr(PyPy, link_name = "_PyPy_NoneStruct")] static mut _Py_NoneStruct: PyObject; @@ -752,8 +841,12 @@ extern "C" { #[inline] pub unsafe fn Py_None() -> *mut PyObject { - #[cfg(not(GraalPy))] + #[cfg(all(not(GraalPy), all(Py_3_13, Py_LIMITED_API)))] + return Py_GetConstantBorrowed(Py_CONSTANT_NONE); + + #[cfg(all(not(GraalPy), not(all(Py_3_13, Py_LIMITED_API))))] return ptr::addr_of_mut!(_Py_NoneStruct); + #[cfg(GraalPy)] return _Py_NoneStructReference; } @@ -767,7 +860,7 @@ pub unsafe fn Py_IsNone(x: *mut PyObject) -> c_int { #[cfg_attr(windows, link(name = "pythonXY"))] extern "C" { - #[cfg(not(GraalPy))] + #[cfg(all(not(GraalPy), not(all(Py_3_13, Py_LIMITED_API))))] #[cfg_attr(PyPy, link_name = "_PyPy_NotImplementedStruct")] static mut _Py_NotImplementedStruct: PyObject; @@ -777,8 +870,12 @@ extern "C" { #[inline] pub unsafe fn Py_NotImplemented() -> *mut PyObject { - #[cfg(not(GraalPy))] + #[cfg(all(not(GraalPy), all(Py_3_13, Py_LIMITED_API)))] + return Py_GetConstantBorrowed(Py_CONSTANT_NONE); + + #[cfg(all(not(GraalPy), not(all(Py_3_13, Py_LIMITED_API))))] return ptr::addr_of_mut!(_Py_NotImplementedStruct); + #[cfg(GraalPy)] return _Py_NotImplementedStructReference; } @@ -805,15 +902,17 @@ pub enum PySendResult { // skipped Py_RETURN_RICHCOMPARE #[inline] -#[cfg(Py_LIMITED_API)] -pub unsafe fn PyType_HasFeature(t: *mut PyTypeObject, f: c_ulong) -> c_int { - ((PyType_GetFlags(t) & f) != 0) as c_int -} +pub unsafe fn PyType_HasFeature(ty: *mut PyTypeObject, feature: c_ulong) -> c_int { + #[cfg(Py_LIMITED_API)] + let flags = PyType_GetFlags(ty); -#[inline] -#[cfg(not(Py_LIMITED_API))] -pub unsafe fn PyType_HasFeature(t: *mut PyTypeObject, f: c_ulong) -> c_int { - (((*t).tp_flags & f) != 0) as c_int + #[cfg(all(not(Py_LIMITED_API), Py_GIL_DISABLED))] + let flags = (*ty).tp_flags.load(std::sync::atomic::Ordering::Relaxed); + + #[cfg(all(not(Py_LIMITED_API), not(Py_GIL_DISABLED)))] + let flags = (*ty).tp_flags; + + ((flags & feature) != 0) as c_int } #[inline] @@ -826,7 +925,18 @@ pub unsafe fn PyType_Check(op: *mut PyObject) -> c_int { PyType_FastSubclass(Py_TYPE(op), Py_TPFLAGS_TYPE_SUBCLASS) } +// skipped _PyType_CAST + #[inline] pub unsafe fn PyType_CheckExact(op: *mut PyObject) -> c_int { Py_IS_TYPE(op, ptr::addr_of_mut!(PyType_Type)) } + +extern "C" { + #[cfg(any(Py_3_13, all(Py_3_11, not(Py_LIMITED_API))))] + #[cfg_attr(PyPy, link_name = "PyPyType_GetModuleByDef")] + pub fn PyType_GetModuleByDef( + arg1: *mut crate::PyTypeObject, + arg2: *mut crate::PyModuleDef, + ) -> *mut PyObject; +} From 03659ae7c6b044d70389f8be04a97a5036cb239c Mon Sep 17 00:00:00 2001 From: Calum Lynch <89159592+Tlunch@users.noreply.github.com> Date: Fri, 23 Aug 2024 11:21:36 +0100 Subject: [PATCH 229/495] Update multiple-python-versions.md (#4471) Missing word. --- guide/src/building-and-distribution/multiple-python-versions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/guide/src/building-and-distribution/multiple-python-versions.md b/guide/src/building-and-distribution/multiple-python-versions.md index b328d236c51..a7879941d0c 100644 --- a/guide/src/building-and-distribution/multiple-python-versions.md +++ b/guide/src/building-and-distribution/multiple-python-versions.md @@ -1,6 +1,6 @@ # Supporting multiple Python versions -PyO3 supports all actively-supported Python 3 and PyPy versions. As much as possible, this is done internally to PyO3 so that your crate's code does not need to adapt to the differences between each version. However, as Python features grow and change between versions, PyO3 cannot a completely identical API for every Python version. This may require you to add conditional compilation to your crate or runtime checks for the Python version. +PyO3 supports all actively-supported Python 3 and PyPy versions. As much as possible, this is done internally to PyO3 so that your crate's code does not need to adapt to the differences between each version. However, as Python features grow and change between versions, PyO3 cannot offer a completely identical API for every Python version. This may require you to add conditional compilation to your crate or runtime checks for the Python version. This section of the guide first introduces the `pyo3-build-config` crate, which you can use as a `build-dependency` to add additional `#[cfg]` flags which allow you to support multiple Python versions at compile-time. From ea6a0aac3993978539e65d39a9ae78913c5a8586 Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Fri, 23 Aug 2024 13:23:56 +0200 Subject: [PATCH 230/495] impl `IntoPyObject` for tuples (#4468) --- src/types/tuple.rs | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/types/tuple.rs b/src/types/tuple.rs index 711922ce3fa..44a9d15e836 100644 --- a/src/types/tuple.rs +++ b/src/types/tuple.rs @@ -1,5 +1,6 @@ use std::iter::FusedIterator; +use crate::conversion::IntoPyObject; use crate::ffi::{self, Py_ssize_t}; use crate::ffi_ptr_ext::FfiPtrExt; #[cfg(feature = "experimental-inspect")] @@ -8,8 +9,8 @@ use crate::instance::Borrowed; use crate::internal_tricks::get_ssize_index; use crate::types::{any::PyAnyMethods, sequence::PySequenceMethods, PyList, PySequence}; use crate::{ - exceptions, Bound, FromPyObject, IntoPy, Py, PyAny, PyErr, PyObject, PyResult, Python, - ToPyObject, + exceptions, Bound, BoundObject, FromPyObject, IntoPy, Py, PyAny, PyErr, PyObject, PyResult, + Python, ToPyObject, }; #[inline] @@ -526,6 +527,20 @@ fn type_output() -> TypeInfo { } } + impl <'py, $($T),+> IntoPyObject<'py> for ($($T,)+) + where + $($T: IntoPyObject<'py>,)+ + PyErr: $(From<$T::Error> + )+ + { + type Target = PyTuple; + type Output = Bound<'py, Self::Target>; + type Error = PyErr; + + fn into_pyobject(self, py: Python<'py>) -> Result { + Ok(array_into_tuple(py, [$(self.$n.into_pyobject(py)?.into_any().unbind()),+]).into_bound(py)) + } + } + impl <$($T: IntoPy),+> IntoPy> for ($($T,)+) { fn into_py(self, py: Python<'_>) -> Py { array_into_tuple(py, [$(self.$n.into_py(py)),+]) From a8970eae1bf554bc58cb71ad2148f97c1f87367c Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Fri, 23 Aug 2024 17:38:25 +0100 Subject: [PATCH 231/495] only show free-threaded warning on free-threaded build (#4473) --- pyo3-ffi/build.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyo3-ffi/build.rs b/pyo3-ffi/build.rs index 8b3a98bf9f7..b0f1c28d448 100644 --- a/pyo3-ffi/build.rs +++ b/pyo3-ffi/build.rs @@ -135,7 +135,7 @@ fn ensure_gil_enabled(interpreter_config: &InterpreterConfig) -> Result<()> { = help: set UNSAFE_PYO3_BUILD_FREE_THREADED=1 to suppress this check and build anyway for free-threaded Python", std::env::var("CARGO_PKG_VERSION").unwrap() ); - if interpreter_config.abi3 { + if !gil_enabled && interpreter_config.abi3 { warn!( "The free-threaded build of CPython does not yet support abi3 so the build artifacts will be version-specific." ) From ec0853dbe6b19a2f90d013149c77b872f40688ec Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Fri, 23 Aug 2024 13:45:04 -0600 Subject: [PATCH 232/495] Fix tests for cargo stress on free-threaded build (#4469) * make it possible to run test_bad_datetime_module_panic under 'cargo stress' * weaken assertions in tests of the pending decrefs pool on free-threaded builds * use the tempfile crate to create a temporary directory --- Cargo.toml | 1 + src/gil.rs | 14 ++++++++++++-- tests/test_datetime_import.rs | 14 ++++++++------ 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 5aac73fced4..d276dd80350 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -61,6 +61,7 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0.61" rayon = "1.6.1" futures = "0.3.28" +tempfile = "3.12.0" [build-dependencies] pyo3-build-config = { path = "pyo3-build-config", version = "=0.23.0-dev", features = ["resolve-config"] } diff --git a/src/gil.rs b/src/gil.rs index ed3b80eac32..d7e6daae918 100644 --- a/src/gil.rs +++ b/src/gil.rs @@ -447,7 +447,9 @@ mod tests { .contains(&unsafe { NonNull::new_unchecked(obj.as_ptr()) }) } - #[cfg(not(pyo3_disable_reference_pool))] + // with no GIL, threads can empty the POOL at any time, so this + // function does not test anything meaningful + #[cfg(not(any(pyo3_disable_reference_pool, Py_GIL_DISABLED)))] fn pool_dec_refs_contains(obj: &PyObject) -> bool { POOL.pending_decrefs .lock() @@ -471,7 +473,7 @@ mod tests { drop(reference); assert_eq!(obj.get_refcnt(py), 1); - #[cfg(not(pyo3_disable_reference_pool))] + #[cfg(not(any(pyo3_disable_reference_pool)))] assert!(pool_dec_refs_does_not_contain(&obj)); }); } @@ -493,12 +495,18 @@ mod tests { // The reference count should not have changed (the GIL has always // been held by this thread), it is remembered to release later. assert_eq!(obj.get_refcnt(py), 2); + #[cfg(not(Py_GIL_DISABLED))] assert!(pool_dec_refs_contains(&obj)); obj }); // Next time the GIL is acquired, the reference is released + #[allow(unused)] Python::with_gil(|py| { + // with no GIL, another thread could still be processing + // DECREFs after releasing the lock on the POOL, so the + // refcnt could still be 2 when this assert happens + #[cfg(not(Py_GIL_DISABLED))] assert_eq!(obj.get_refcnt(py), 1); assert!(pool_dec_refs_does_not_contain(&obj)); }); @@ -641,6 +649,7 @@ mod tests { // For GILGuard::acquire POOL.register_decref(NonNull::new(obj.clone_ref(py).into_ptr()).unwrap()); + #[cfg(not(Py_GIL_DISABLED))] assert!(pool_dec_refs_contains(&obj)); let _guard = GILGuard::acquire(); assert!(pool_dec_refs_does_not_contain(&obj)); @@ -648,6 +657,7 @@ mod tests { // For GILGuard::assume POOL.register_decref(NonNull::new(obj.clone_ref(py).into_ptr()).unwrap()); + #[cfg(not(Py_GIL_DISABLED))] assert!(pool_dec_refs_contains(&obj)); let _guard2 = unsafe { GILGuard::assume() }; assert!(pool_dec_refs_does_not_contain(&obj)); diff --git a/tests/test_datetime_import.rs b/tests/test_datetime_import.rs index fff2ddc6280..cac4908bba6 100644 --- a/tests/test_datetime_import.rs +++ b/tests/test_datetime_import.rs @@ -1,26 +1,28 @@ #![cfg(not(Py_LIMITED_API))] use pyo3::{prelude::*, types::PyDate}; +use tempfile::Builder; #[test] #[should_panic(expected = "module 'datetime' has no attribute 'datetime_CAPI'")] fn test_bad_datetime_module_panic() { // Create an empty temporary directory // with an empty "datetime" module which we'll put on the sys.path - let tmpdir = std::env::temp_dir(); - let tmpdir = tmpdir.join("pyo3_test_date_check"); - let _ = std::fs::remove_dir_all(&tmpdir); - std::fs::create_dir(&tmpdir).unwrap(); - std::fs::File::create(tmpdir.join("datetime.py")).unwrap(); + let tmpdir = Builder::new() + .prefix("pyo3_test_data_check") + .tempdir() + .unwrap(); + std::fs::File::create(tmpdir.path().join("datetime.py")).unwrap(); Python::with_gil(|py: Python<'_>| { let sys = py.import("sys").unwrap(); sys.getattr("path") .unwrap() - .call_method1("insert", (0, tmpdir)) + .call_method1("insert", (0, tmpdir.path())) .unwrap(); // This should panic because the "datetime" module is empty PyDate::new(py, 2018, 1, 1).unwrap(); }); + tmpdir.close().unwrap(); } From e068a307b5bc99913846745440c2a811f21624a3 Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Fri, 23 Aug 2024 23:36:30 +0200 Subject: [PATCH 233/495] fix wrong trait bound `IntoPyObject` impls for `Hash` collections (#4478) --- src/conversions/hashbrown.rs | 2 +- src/conversions/std/map.rs | 2 +- src/conversions/std/set.rs | 2 +- src/types/mapping.rs | 39 ++++++++++++++++++------------------ 4 files changed, 23 insertions(+), 22 deletions(-) diff --git a/src/conversions/hashbrown.rs b/src/conversions/hashbrown.rs index 7104fe35679..4189ee52a2e 100644 --- a/src/conversions/hashbrown.rs +++ b/src/conversions/hashbrown.rs @@ -165,7 +165,7 @@ where impl<'a, 'py, K, H> IntoPyObject<'py> for &'a hashbrown::HashSet where &'a K: IntoPyObject<'py> + cmp::Eq + hash::Hash, - &'a H: hash::BuildHasher, + H: hash::BuildHasher, PyErr: From<<&'a K as IntoPyObject<'py>>::Error>, { type Target = PySet; diff --git a/src/conversions/std/map.rs b/src/conversions/std/map.rs index f19b5671d7a..e1638150518 100644 --- a/src/conversions/std/map.rs +++ b/src/conversions/std/map.rs @@ -76,7 +76,7 @@ impl<'a, 'py, K, V, H> IntoPyObject<'py> for &'a collections::HashMap where &'a K: IntoPyObject<'py> + cmp::Eq + hash::Hash, &'a V: IntoPyObject<'py>, - &'a H: hash::BuildHasher, + H: hash::BuildHasher, PyErr: From<<&'a K as IntoPyObject<'py>>::Error> + From<<&'a V as IntoPyObject<'py>>::Error>, { type Target = PyDict; diff --git a/src/conversions/std/set.rs b/src/conversions/std/set.rs index b15013b81ba..7108787370b 100644 --- a/src/conversions/std/set.rs +++ b/src/conversions/std/set.rs @@ -80,7 +80,7 @@ where impl<'a, 'py, K, H> IntoPyObject<'py> for &'a collections::HashSet where &'a K: IntoPyObject<'py> + Eq + hash::Hash, - &'a H: hash::BuildHasher, + H: hash::BuildHasher, PyErr: From<<&'a K as IntoPyObject<'py>>::Error>, { type Target = PySet; diff --git a/src/types/mapping.rs b/src/types/mapping.rs index 537959719e0..67a7b9b4d3d 100644 --- a/src/types/mapping.rs +++ b/src/types/mapping.rs @@ -189,19 +189,20 @@ mod tests { use crate::{exceptions::PyKeyError, types::PyTuple}; use super::*; + use crate::conversion::IntoPyObject; #[test] fn test_len() { Python::with_gil(|py| { - let mut v = HashMap::new(); - let ob = v.to_object(py); - let mapping = ob.downcast_bound::(py).unwrap(); + let mut v = HashMap::::new(); + let ob = (&v).into_pyobject(py).unwrap(); + let mapping = ob.downcast::().unwrap(); assert_eq!(0, mapping.len().unwrap()); assert!(mapping.is_empty().unwrap()); v.insert(7, 32); - let ob = v.to_object(py); - let mapping2 = ob.downcast_bound::(py).unwrap(); + let ob = v.into_pyobject(py).unwrap(); + let mapping2 = ob.downcast::().unwrap(); assert_eq!(1, mapping2.len().unwrap()); assert!(!mapping2.is_empty().unwrap()); }); @@ -212,8 +213,8 @@ mod tests { Python::with_gil(|py| { let mut v = HashMap::new(); v.insert("key0", 1234); - let ob = v.to_object(py); - let mapping = ob.downcast_bound::(py).unwrap(); + let ob = v.into_pyobject(py).unwrap(); + let mapping = ob.downcast::().unwrap(); mapping.set_item("key1", "foo").unwrap(); assert!(mapping.contains("key0").unwrap()); @@ -227,8 +228,8 @@ mod tests { Python::with_gil(|py| { let mut v = HashMap::new(); v.insert(7, 32); - let ob = v.to_object(py); - let mapping = ob.downcast_bound::(py).unwrap(); + let ob = v.into_pyobject(py).unwrap(); + let mapping = ob.downcast::().unwrap(); assert_eq!( 32, mapping.get_item(7i32).unwrap().extract::().unwrap() @@ -245,8 +246,8 @@ mod tests { Python::with_gil(|py| { let mut v = HashMap::new(); v.insert(7, 32); - let ob = v.to_object(py); - let mapping = ob.downcast_bound::(py).unwrap(); + let ob = v.into_pyobject(py).unwrap(); + let mapping = ob.downcast::().unwrap(); assert!(mapping.set_item(7i32, 42i32).is_ok()); // change assert!(mapping.set_item(8i32, 123i32).is_ok()); // insert assert_eq!( @@ -265,8 +266,8 @@ mod tests { Python::with_gil(|py| { let mut v = HashMap::new(); v.insert(7, 32); - let ob = v.to_object(py); - let mapping = ob.downcast_bound::(py).unwrap(); + let ob = v.into_pyobject(py).unwrap(); + let mapping = ob.downcast::().unwrap(); assert!(mapping.del_item(7i32).is_ok()); assert_eq!(0, mapping.len().unwrap()); assert!(mapping @@ -283,8 +284,8 @@ mod tests { v.insert(7, 32); v.insert(8, 42); v.insert(9, 123); - let ob = v.to_object(py); - let mapping = ob.downcast_bound::(py).unwrap(); + let ob = v.into_pyobject(py).unwrap(); + let mapping = ob.downcast::().unwrap(); // Can't just compare against a vector of tuples since we don't have a guaranteed ordering. let mut key_sum = 0; let mut value_sum = 0; @@ -305,8 +306,8 @@ mod tests { v.insert(7, 32); v.insert(8, 42); v.insert(9, 123); - let ob = v.to_object(py); - let mapping = ob.downcast_bound::(py).unwrap(); + let ob = v.into_pyobject(py).unwrap(); + let mapping = ob.downcast::().unwrap(); // Can't just compare against a vector of tuples since we don't have a guaranteed ordering. let mut key_sum = 0; for el in mapping.keys().unwrap().iter().unwrap() { @@ -323,8 +324,8 @@ mod tests { v.insert(7, 32); v.insert(8, 42); v.insert(9, 123); - let ob = v.to_object(py); - let mapping = ob.downcast_bound::(py).unwrap(); + let ob = v.into_pyobject(py).unwrap(); + let mapping = ob.downcast::().unwrap(); // Can't just compare against a vector of tuples since we don't have a guaranteed ordering. let mut values_sum = 0; for el in mapping.values().unwrap().iter().unwrap() { From 18bca6ec76e593c2b0a7a9229373ebd453675cbc Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Sat, 24 Aug 2024 00:58:50 +0200 Subject: [PATCH 234/495] reintroduce `PyErr` constructors and methods (#4475) * reintroduce `PyErr` constructors and methods * fixup Co-authored-by: Lily Foote --------- Co-authored-by: Lily Foote --- .../python-from-rust/calling-existing-code.md | 6 +- src/conversions/anyhow.rs | 4 +- src/conversions/chrono.rs | 18 +- src/conversions/eyre.rs | 4 +- src/coroutine.rs | 2 +- src/err/impls.rs | 4 +- src/err/mod.rs | 252 ++++++++++++------ src/exceptions.rs | 23 +- src/impl_/extract_argument.rs | 9 +- src/impl_/pyclass.rs | 2 +- src/impl_/trampoline.rs | 2 +- src/instance.rs | 2 +- src/tests/common.rs | 2 +- src/types/dict.rs | 2 +- src/types/mapping.rs | 2 +- src/types/sequence.rs | 2 +- src/types/string.rs | 12 +- src/types/traceback.rs | 12 +- src/types/weakref/proxy.rs | 12 +- tests/test_coroutine.rs | 2 +- tests/test_exceptions.rs | 4 +- 21 files changed, 232 insertions(+), 146 deletions(-) diff --git a/guide/src/python-from-rust/calling-existing-code.md b/guide/src/python-from-rust/calling-existing-code.md index 9ffe8d77c70..a372ece2808 100644 --- a/guide/src/python-from-rust/calling-existing-code.md +++ b/guide/src/python-from-rust/calling-existing-code.md @@ -368,9 +368,9 @@ class House(object): .call_method1( "__exit__", ( - e.get_type_bound(py), - e.value_bound(py), - e.traceback_bound(py), + e.get_type(py), + e.value(py), + e.traceback(py), ), ) .unwrap(); diff --git a/src/conversions/anyhow.rs b/src/conversions/anyhow.rs index 00a966d4f21..de2c5c9dc90 100644 --- a/src/conversions/anyhow.rs +++ b/src/conversions/anyhow.rs @@ -150,7 +150,7 @@ mod test_anyhow { let pyerr = py .run(ffi::c_str!("raise err"), None, Some(&locals)) .unwrap_err(); - assert_eq!(pyerr.value_bound(py).to_string(), expected_contents); + assert_eq!(pyerr.value(py).to_string(), expected_contents); }) } @@ -169,7 +169,7 @@ mod test_anyhow { let pyerr = py .run(ffi::c_str!("raise err"), None, Some(&locals)) .unwrap_err(); - assert_eq!(pyerr.value_bound(py).to_string(), expected_contents); + assert_eq!(pyerr.value(py).to_string(), expected_contents); }) } diff --git a/src/conversions/chrono.rs b/src/conversions/chrono.rs index 48246d827db..5e61716975e 100644 --- a/src/conversions/chrono.rs +++ b/src/conversions/chrono.rs @@ -52,9 +52,11 @@ use crate::types::{ timezone_utc, PyDate, PyDateAccess, PyDateTime, PyDelta, PyDeltaAccess, PyTime, PyTimeAccess, PyTzInfo, PyTzInfoAccess, }; +use crate::{ + ffi, Bound, FromPyObject, IntoPy, PyAny, PyErr, PyObject, PyResult, Python, ToPyObject, +}; #[cfg(Py_LIMITED_API)] use crate::{intern, DowncastError}; -use crate::{Bound, FromPyObject, IntoPy, PyAny, PyErr, PyObject, PyResult, Python, ToPyObject}; use chrono::offset::{FixedOffset, Utc}; use chrono::{ DateTime, Datelike, Duration, NaiveDate, NaiveDateTime, NaiveTime, Offset, TimeZone, Timelike, @@ -701,13 +703,13 @@ fn naive_datetime_to_py_datetime( fn warn_truncated_leap_second(obj: &Bound<'_, PyAny>) { let py = obj.py(); - if let Err(e) = PyErr::warn_bound( + if let Err(e) = PyErr::warn( py, &py.get_type::(), - "ignored leap-second, `datetime` does not support leap-seconds", + ffi::c_str!("ignored leap-second, `datetime` does not support leap-seconds"), 0, ) { - e.write_unraisable_bound(py, Some(&obj.as_borrowed())) + e.write_unraisable(py, Some(&obj.as_borrowed())) }; } @@ -835,7 +837,7 @@ mod tests { assert!(result.is_err()); let res = result.err().unwrap(); // Also check the error message is what we expect - let msg = res.value_bound(py).repr().unwrap().to_string(); + let msg = res.value(py).repr().unwrap().to_string(); assert_eq!(msg, "TypeError(\"zoneinfo.ZoneInfo(key='Europe/London') is not a fixed offset timezone\")"); }); } @@ -850,7 +852,7 @@ mod tests { // Now test that converting a PyDateTime with tzinfo to a NaiveDateTime fails let res: PyResult = py_datetime.extract(); assert_eq!( - res.unwrap_err().value_bound(py).repr().unwrap().to_string(), + res.unwrap_err().value(py).repr().unwrap().to_string(), "TypeError('expected a datetime without tzinfo')" ); }); @@ -865,14 +867,14 @@ mod tests { // Now test that converting a PyDateTime with tzinfo to a NaiveDateTime fails let res: PyResult> = py_datetime.extract(); assert_eq!( - res.unwrap_err().value_bound(py).repr().unwrap().to_string(), + res.unwrap_err().value(py).repr().unwrap().to_string(), "TypeError('expected a datetime with non-None tzinfo')" ); // Now test that converting a PyDateTime with tzinfo to a NaiveDateTime fails let res: PyResult> = py_datetime.extract(); assert_eq!( - res.unwrap_err().value_bound(py).repr().unwrap().to_string(), + res.unwrap_err().value(py).repr().unwrap().to_string(), "TypeError('expected a datetime with non-None tzinfo')" ); }); diff --git a/src/conversions/eyre.rs b/src/conversions/eyre.rs index dadf6039f9a..5bfaddcbdc6 100644 --- a/src/conversions/eyre.rs +++ b/src/conversions/eyre.rs @@ -155,7 +155,7 @@ mod tests { let pyerr = py .run(ffi::c_str!("raise err"), None, Some(&locals)) .unwrap_err(); - assert_eq!(pyerr.value_bound(py).to_string(), expected_contents); + assert_eq!(pyerr.value(py).to_string(), expected_contents); }) } @@ -174,7 +174,7 @@ mod tests { let pyerr = py .run(ffi::c_str!("raise err"), None, Some(&locals)) .unwrap_err(); - assert_eq!(pyerr.value_bound(py).to_string(), expected_contents); + assert_eq!(pyerr.value(py).to_string(), expected_contents); }) } diff --git a/src/coroutine.rs b/src/coroutine.rs index 169d28d635f..22aad47f5c6 100644 --- a/src/coroutine.rs +++ b/src/coroutine.rs @@ -78,7 +78,7 @@ impl Coroutine { (Some(exc), Some(cb)) => cb.throw(exc), (Some(exc), None) => { self.close(); - return Err(PyErr::from_value_bound(exc.into_bound(py))); + return Err(PyErr::from_value(exc.into_bound(py))); } (None, _) => {} } diff --git a/src/err/impls.rs b/src/err/impls.rs index f14a839b471..81d0b2ae81b 100644 --- a/src/err/impls.rs +++ b/src/err/impls.rs @@ -141,8 +141,8 @@ mod tests { let py_err_recovered_from_rust_err: PyErr = rust_err_from_py_err.into(); assert!(py_err_recovered_from_rust_err - .value_bound(py) - .is(py_error_clone.value_bound(py))); // It should be the same exception + .value(py) + .is(py_error_clone.value(py))); // It should be the same exception }) }; diff --git a/src/err/mod.rs b/src/err/mod.rs index 68d207c8fb8..7d142350976 100644 --- a/src/err/mod.rs +++ b/src/err/mod.rs @@ -10,7 +10,7 @@ use crate::{ use crate::{Borrowed, IntoPy, Py, PyAny, PyObject, Python, ToPyObject}; use std::borrow::Cow; use std::cell::UnsafeCell; -use std::ffi::CString; +use std::ffi::{CStr, CString}; mod err_state; mod impls; @@ -176,13 +176,23 @@ impl PyErr { /// If `ty` does not inherit from `BaseException`, then a `TypeError` will be returned. /// /// If calling `ty` with `args` raises an exception, that exception will be returned. - pub fn from_type_bound(ty: Bound<'_, PyType>, args: A) -> PyErr + pub fn from_type(ty: Bound<'_, PyType>, args: A) -> PyErr where A: PyErrArguments + Send + Sync + 'static, { PyErr::from_state(PyErrState::lazy(ty.unbind().into_any(), args)) } + /// Deprecated name for [`PyErr::from_type`]. + #[deprecated(since = "0.23.0", note = "renamed to `PyErr::from_type`")] + #[inline] + pub fn from_type_bound(ty: Bound<'_, PyType>, args: A) -> PyErr + where + A: PyErrArguments + Send + Sync + 'static, + { + Self::from_type(ty, args) + } + /// Creates a new PyErr. /// /// If `obj` is a Python exception object, the PyErr will contain that object. @@ -200,23 +210,23 @@ impl PyErr { /// /// Python::with_gil(|py| { /// // Case #1: Exception object - /// let err = PyErr::from_value_bound(PyTypeError::new_err("some type error") - /// .value_bound(py).clone().into_any()); + /// let err = PyErr::from_value(PyTypeError::new_err("some type error") + /// .value(py).clone().into_any()); /// assert_eq!(err.to_string(), "TypeError: some type error"); /// /// // Case #2: Exception type - /// let err = PyErr::from_value_bound(PyTypeError::type_object_bound(py).into_any()); + /// let err = PyErr::from_value(PyTypeError::type_object_bound(py).into_any()); /// assert_eq!(err.to_string(), "TypeError: "); /// /// // Case #3: Invalid exception value - /// let err = PyErr::from_value_bound(PyString::new(py, "foo").into_any()); + /// let err = PyErr::from_value(PyString::new(py, "foo").into_any()); /// assert_eq!( /// err.to_string(), /// "TypeError: exceptions must derive from BaseException" /// ); /// }); /// ``` - pub fn from_value_bound(obj: Bound<'_, PyAny>) -> PyErr { + pub fn from_value(obj: Bound<'_, PyAny>) -> PyErr { let state = match obj.downcast_into::() { Ok(obj) => PyErrState::normalized(obj), Err(err) => { @@ -231,6 +241,13 @@ impl PyErr { PyErr::from_state(state) } + /// Deprecated name for [`PyErr::from_value`]. + #[deprecated(since = "0.23.0", note = "renamed to `PyErr::from_value`")] + #[inline] + pub fn from_value_bound(obj: Bound<'_, PyAny>) -> PyErr { + Self::from_value(obj) + } + /// Returns the type of this exception. /// /// # Examples @@ -239,13 +256,20 @@ impl PyErr { /// /// Python::with_gil(|py| { /// let err: PyErr = PyTypeError::new_err(("some type error",)); - /// assert!(err.get_type_bound(py).is(&PyType::new::(py))); + /// assert!(err.get_type(py).is(&PyType::new::(py))); /// }); /// ``` - pub fn get_type_bound<'py>(&self, py: Python<'py>) -> Bound<'py, PyType> { + pub fn get_type<'py>(&self, py: Python<'py>) -> Bound<'py, PyType> { self.normalized(py).ptype(py) } + /// Deprecated name for [`PyErr::get_type`]. + #[deprecated(since = "0.23.0", note = "renamed to `PyErr::get_type`")] + #[inline] + pub fn get_type_bound<'py>(&self, py: Python<'py>) -> Bound<'py, PyType> { + self.get_type(py) + } + /// Returns the value of this exception. /// /// # Examples @@ -256,13 +280,20 @@ impl PyErr { /// Python::with_gil(|py| { /// let err: PyErr = PyTypeError::new_err(("some type error",)); /// assert!(err.is_instance_of::(py)); - /// assert_eq!(err.value_bound(py).to_string(), "some type error"); + /// assert_eq!(err.value(py).to_string(), "some type error"); /// }); /// ``` - pub fn value_bound<'py>(&self, py: Python<'py>) -> &Bound<'py, PyBaseException> { + pub fn value<'py>(&self, py: Python<'py>) -> &Bound<'py, PyBaseException> { self.normalized(py).pvalue.bind(py) } + /// Deprecated name for [`PyErr::value`]. + #[deprecated(since = "0.23.0", note = "renamed to `PyErr::value`")] + #[inline] + pub fn value_bound<'py>(&self, py: Python<'py>) -> &Bound<'py, PyBaseException> { + self.value(py) + } + /// Consumes self to take ownership of the exception value contained in this error. pub fn into_value(self, py: Python<'_>) -> Py { // NB technically this causes one reference count increase and decrease in quick succession @@ -286,13 +317,20 @@ impl PyErr { /// /// Python::with_gil(|py| { /// let err = PyTypeError::new_err(("some type error",)); - /// assert!(err.traceback_bound(py).is_none()); + /// assert!(err.traceback(py).is_none()); /// }); /// ``` - pub fn traceback_bound<'py>(&self, py: Python<'py>) -> Option> { + pub fn traceback<'py>(&self, py: Python<'py>) -> Option> { self.normalized(py).ptraceback(py) } + /// Deprecated name for [`PyErr::traceback`]. + #[deprecated(since = "0.23.0", note = "renamed to `PyErr::traceback`")] + #[inline] + pub fn traceback_bound<'py>(&self, py: Python<'py>) -> Option> { + self.traceback(py) + } + /// Gets whether an error is present in the Python interpreter's global state. #[inline] pub fn occurred(_: Python<'_>) -> bool { @@ -429,14 +467,10 @@ impl PyErr { /// # Errors /// /// This function returns an error if `name` is not of the form `.`. - /// - /// # Panics - /// - /// This function will panic if `name` or `doc` cannot be converted to [`CString`]s. - pub fn new_type_bound<'py>( + pub fn new_type<'py>( py: Python<'py>, - name: &str, - doc: Option<&str>, + name: &CStr, + doc: Option<&CStr>, base: Option<&Bound<'py, PyType>>, dict: Option, ) -> PyResult> { @@ -450,34 +484,44 @@ impl PyErr { Some(obj) => obj.as_ptr(), }; - let null_terminated_name = - CString::new(name).expect("Failed to initialize nul terminated exception name"); - - let null_terminated_doc = - doc.map(|d| CString::new(d).expect("Failed to initialize nul terminated docstring")); - - let null_terminated_doc_ptr = match null_terminated_doc.as_ref() { + let doc_ptr = match doc.as_ref() { Some(c) => c.as_ptr(), None => std::ptr::null(), }; - let ptr = unsafe { - ffi::PyErr_NewExceptionWithDoc( - null_terminated_name.as_ptr(), - null_terminated_doc_ptr, - base, - dict, - ) - }; + let ptr = unsafe { ffi::PyErr_NewExceptionWithDoc(name.as_ptr(), doc_ptr, base, dict) }; unsafe { Py::from_owned_ptr_or_err(py, ptr) } } + /// Deprecated name for [`PyErr::new_type`]. + #[deprecated(since = "0.23.0", note = "renamed to `PyErr::new_type`")] + #[inline] + pub fn new_type_bound<'py>( + py: Python<'py>, + name: &str, + doc: Option<&str>, + base: Option<&Bound<'py, PyType>>, + dict: Option, + ) -> PyResult> { + let null_terminated_name = + CString::new(name).expect("Failed to initialize nul terminated exception name"); + let null_terminated_doc = + doc.map(|d| CString::new(d).expect("Failed to initialize nul terminated docstring")); + Self::new_type( + py, + &null_terminated_name, + null_terminated_doc.as_deref(), + base, + dict, + ) + } + /// Prints a standard traceback to `sys.stderr`. pub fn display(&self, py: Python<'_>) { #[cfg(Py_3_12)] unsafe { - ffi::PyErr_DisplayException(self.value_bound(py).as_ptr()) + ffi::PyErr_DisplayException(self.value(py).as_ptr()) } #[cfg(not(Py_3_12))] @@ -486,11 +530,11 @@ impl PyErr { // PyErr_Display. if we inline this, the `Bound` will be dropped // after the argument got evaluated, leading to call with a dangling // pointer. - let traceback = self.traceback_bound(py); - let type_bound = self.get_type_bound(py); + let traceback = self.traceback(py); + let type_bound = self.get_type(py); ffi::PyErr_Display( type_bound.as_ptr(), - self.value_bound(py).as_ptr(), + self.value(py).as_ptr(), traceback .as_ref() .map_or(std::ptr::null_mut(), |traceback| traceback.as_ptr()), @@ -520,23 +564,30 @@ impl PyErr { where T: ToPyObject, { - self.is_instance_bound(py, exc.to_object(py).bind(py)) + self.is_instance(py, exc.to_object(py).bind(py)) } /// Returns true if the current exception is instance of `T`. #[inline] - pub fn is_instance_bound(&self, py: Python<'_>, ty: &Bound<'_, PyAny>) -> bool { - let type_bound = self.get_type_bound(py); + pub fn is_instance(&self, py: Python<'_>, ty: &Bound<'_, PyAny>) -> bool { + let type_bound = self.get_type(py); (unsafe { ffi::PyErr_GivenExceptionMatches(type_bound.as_ptr(), ty.as_ptr()) }) != 0 } + /// Deprecated name for [`PyErr::is_instance`]. + #[deprecated(since = "0.23.0", note = "renamed to `PyErr::is_instance`")] + #[inline] + pub fn is_instance_bound(&self, py: Python<'_>, ty: &Bound<'_, PyAny>) -> bool { + self.is_instance(py, ty) + } + /// Returns true if the current exception is instance of `T`. #[inline] pub fn is_instance_of(&self, py: Python<'_>) -> bool where T: PyTypeInfo, { - self.is_instance_bound(py, &T::type_object_bound(py)) + self.is_instance(py, &T::type_object_bound(py)) } /// Writes the error back to the Python interpreter's global state. @@ -571,18 +622,25 @@ impl PyErr { /// # fn main() -> PyResult<()> { /// Python::with_gil(|py| { /// match failing_function() { - /// Err(pyerr) => pyerr.write_unraisable_bound(py, None), + /// Err(pyerr) => pyerr.write_unraisable(py, None), /// Ok(..) => { /* do something here */ } /// } /// Ok(()) /// }) /// # } #[inline] - pub fn write_unraisable_bound(self, py: Python<'_>, obj: Option<&Bound<'_, PyAny>>) { + pub fn write_unraisable(self, py: Python<'_>, obj: Option<&Bound<'_, PyAny>>) { self.restore(py); unsafe { ffi::PyErr_WriteUnraisable(obj.map_or(std::ptr::null_mut(), Bound::as_ptr)) } } + /// Deprecated name for [`PyErr::write_unraisable`]. + #[deprecated(since = "0.23.0", note = "renamed to `PyErr::write_unraisable`")] + #[inline] + pub fn write_unraisable_bound(self, py: Python<'_>, obj: Option<&Bound<'_, PyAny>>) { + self.write_unraisable(py, obj) + } + /// Issues a warning message. /// /// May return an `Err(PyErr)` if warnings-as-errors is enabled. @@ -596,21 +654,21 @@ impl PyErr { /// Example: /// ```rust /// # use pyo3::prelude::*; + /// # use pyo3::ffi::c_str; /// # fn main() -> PyResult<()> { /// Python::with_gil(|py| { /// let user_warning = py.get_type::(); - /// PyErr::warn_bound(py, &user_warning, "I am warning you", 0)?; + /// PyErr::warn(py, &user_warning, c_str!("I am warning you"), 0)?; /// Ok(()) /// }) /// # } /// ``` - pub fn warn_bound<'py>( + pub fn warn<'py>( py: Python<'py>, category: &Bound<'py, PyAny>, - message: &str, + message: &CStr, stacklevel: i32, ) -> PyResult<()> { - let message = CString::new(message)?; error_on_minusone(py, unsafe { ffi::PyErr_WarnEx( category.as_ptr(), @@ -620,6 +678,19 @@ impl PyErr { }) } + /// Deprecated name for [`PyErr::warn`]. + #[deprecated(since = "0.23.0", note = "renamed to `PyErr::warn`")] + #[inline] + pub fn warn_bound<'py>( + py: Python<'py>, + category: &Bound<'py, PyAny>, + message: &str, + stacklevel: i32, + ) -> PyResult<()> { + let message = CString::new(message)?; + Self::warn(py, category, &message, stacklevel) + } + /// Issues a warning message, with more control over the warning attributes. /// /// May return a `PyErr` if warnings-as-errors is enabled. @@ -628,18 +699,15 @@ impl PyErr { /// /// The `category` should be one of the `Warning` classes available in /// [`pyo3::exceptions`](crate::exceptions), or a subclass. - pub fn warn_explicit_bound<'py>( + pub fn warn_explicit<'py>( py: Python<'py>, category: &Bound<'py, PyAny>, - message: &str, - filename: &str, + message: &CStr, + filename: &CStr, lineno: i32, - module: Option<&str>, + module: Option<&CStr>, registry: Option<&Bound<'py, PyAny>>, ) -> PyResult<()> { - let message = CString::new(message)?; - let filename = CString::new(filename)?; - let module = module.map(CString::new).transpose()?; let module_ptr = match module { None => std::ptr::null_mut(), Some(s) => s.as_ptr(), @@ -660,6 +728,32 @@ impl PyErr { }) } + /// Deprecated name for [`PyErr::warn_explicit`]. + #[deprecated(since = "0.23.0", note = "renamed to `PyErr::warn`")] + #[inline] + pub fn warn_explicit_bound<'py>( + py: Python<'py>, + category: &Bound<'py, PyAny>, + message: &str, + filename: &str, + lineno: i32, + module: Option<&str>, + registry: Option<&Bound<'py, PyAny>>, + ) -> PyResult<()> { + let message = CString::new(message)?; + let filename = CString::new(filename)?; + let module = module.map(CString::new).transpose()?; + Self::warn_explicit( + py, + category, + &message, + &filename, + lineno, + module.as_deref(), + registry, + ) + } + /// Clone the PyErr. This requires the GIL, which is why PyErr does not implement Clone. /// /// # Examples @@ -668,11 +762,11 @@ impl PyErr { /// Python::with_gil(|py| { /// let err: PyErr = PyTypeError::new_err(("some type error",)); /// let err_clone = err.clone_ref(py); - /// assert!(err.get_type_bound(py).is(&err_clone.get_type_bound(py))); - /// assert!(err.value_bound(py).is(err_clone.value_bound(py))); - /// match err.traceback_bound(py) { - /// None => assert!(err_clone.traceback_bound(py).is_none()), - /// Some(tb) => assert!(err_clone.traceback_bound(py).unwrap().is(&tb)), + /// assert!(err.get_type(py).is(&err_clone.get_type(py))); + /// assert!(err.value(py).is(err_clone.value(py))); + /// match err.traceback(py) { + /// None => assert!(err_clone.traceback(py).is_none()), + /// Some(tb) => assert!(err_clone.traceback(py).unwrap().is(&tb)), /// } /// }); /// ``` @@ -685,9 +779,8 @@ impl PyErr { /// associated with the exception, as accessible from Python through `__cause__`. pub fn cause(&self, py: Python<'_>) -> Option { use crate::ffi_ptr_ext::FfiPtrExt; - let obj = unsafe { - ffi::PyException_GetCause(self.value_bound(py).as_ptr()).assume_owned_or_opt(py) - }; + let obj = + unsafe { ffi::PyException_GetCause(self.value(py).as_ptr()).assume_owned_or_opt(py) }; // PyException_GetCause is documented as potentially returning PyNone, but only GraalPy seems to actually do that #[cfg(GraalPy)] if let Some(cause) = &obj { @@ -695,12 +788,12 @@ impl PyErr { return None; } } - obj.map(Self::from_value_bound) + obj.map(Self::from_value) } /// Set the cause associated with the exception, pass `None` to clear it. pub fn set_cause(&self, py: Python<'_>, cause: Option) { - let value = self.value_bound(py); + let value = self.value(py); let cause = cause.map(|err| err.into_value(py)); unsafe { // PyException_SetCause _steals_ a reference to cause, so must use .into_ptr() @@ -759,9 +852,9 @@ impl std::fmt::Debug for PyErr { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { Python::with_gil(|py| { f.debug_struct("PyErr") - .field("type", &self.get_type_bound(py)) - .field("value", self.value_bound(py)) - .field("traceback", &self.traceback_bound(py)) + .field("type", &self.get_type(py)) + .field("value", self.value(py)) + .field("traceback", &self.traceback(py)) .finish() }) } @@ -770,7 +863,7 @@ impl std::fmt::Debug for PyErr { impl std::fmt::Display for PyErr { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { Python::with_gil(|py| { - let value = self.value_bound(py); + let value = self.value(py); let type_name = value.get_type().qualname().map_err(|_| std::fmt::Error)?; write!(f, "{}", type_name)?; if let Ok(s) = value.str() { @@ -858,7 +951,7 @@ where { #[inline] fn from(err: Bound<'py, T>) -> PyErr { - PyErr::from_value_bound(err.into_any()) + PyErr::from_value(err.into_any()) } } @@ -1107,8 +1200,7 @@ mod tests { assert!(!err.matches(py, PyTypeError::type_object_bound(py))); // String is not a valid exception class, so we should get a TypeError - let err: PyErr = - PyErr::from_type_bound(crate::types::PyString::type_object_bound(py), "foo"); + let err: PyErr = PyErr::from_type(crate::types::PyString::type_object_bound(py), "foo"); assert!(err.matches(py, PyTypeError::type_object_bound(py))); }) } @@ -1161,7 +1253,7 @@ mod tests { // First, test the warning is emitted assert_warnings!( py, - { PyErr::warn_bound(py, &cls, "I am warning you", 0).unwrap() }, + { PyErr::warn(py, &cls, ffi::c_str!("I am warning you"), 0).unwrap() }, [(exceptions::PyUserWarning, "I am warning you")] ); @@ -1169,7 +1261,7 @@ mod tests { warnings .call_method1("simplefilter", ("error", &cls)) .unwrap(); - PyErr::warn_bound(py, &cls, "I am warning you", 0).unwrap_err(); + PyErr::warn(py, &cls, ffi::c_str!("I am warning you"), 0).unwrap_err(); // Test with error for an explicit module warnings.call_method0("resetwarnings").unwrap(); @@ -1180,22 +1272,22 @@ mod tests { // This has the wrong module and will not raise, just be emitted assert_warnings!( py, - { PyErr::warn_bound(py, &cls, "I am warning you", 0).unwrap() }, + { PyErr::warn(py, &cls, ffi::c_str!("I am warning you"), 0).unwrap() }, [(exceptions::PyUserWarning, "I am warning you")] ); - let err = PyErr::warn_explicit_bound( + let err = PyErr::warn_explicit( py, &cls, - "I am warning you", - "pyo3test.py", + ffi::c_str!("I am warning you"), + ffi::c_str!("pyo3test.py"), 427, None, None, ) .unwrap_err(); assert!(err - .value_bound(py) + .value(py) .getattr("args") .unwrap() .get_item(0) diff --git a/src/exceptions.rs b/src/exceptions.rs index fbd2eb077b5..958ad58110c 100644 --- a/src/exceptions.rs +++ b/src/exceptions.rs @@ -215,7 +215,7 @@ macro_rules! create_exception { $crate::impl_exception_boilerplate!($name); - $crate::create_exception_type_object!($module, $name, $base, ::std::option::Option::None); + $crate::create_exception_type_object!($module, $name, $base, None); }; ($module: expr, $name: ident, $base: ty, $doc: expr) => { #[repr(transparent)] @@ -225,12 +225,7 @@ macro_rules! create_exception { $crate::impl_exception_boilerplate!($name); - $crate::create_exception_type_object!( - $module, - $name, - $base, - ::std::option::Option::Some($doc) - ); + $crate::create_exception_type_object!($module, $name, $base, Some($doc)); }; } @@ -239,6 +234,12 @@ macro_rules! create_exception { #[doc(hidden)] #[macro_export] macro_rules! create_exception_type_object { + ($module: expr, $name: ident, $base: ty, None) => { + $crate::create_exception_type_object!($module, $name, $base, ::std::option::Option::None); + }; + ($module: expr, $name: ident, $base: ty, Some($doc: expr)) => { + $crate::create_exception_type_object!($module, $name, $base, ::std::option::Option::Some($crate::ffi::c_str!($doc))); + }; ($module: expr, $name: ident, $base: ty, $doc: expr) => { $crate::pyobject_native_type_core!( $name, @@ -254,9 +255,9 @@ macro_rules! create_exception_type_object { TYPE_OBJECT .get_or_init(py, || - $crate::PyErr::new_type_bound( + $crate::PyErr::new_type( py, - concat!(stringify!($module), ".", stringify!($name)), + $crate::ffi::c_str!(concat!(stringify!($module), ".", stringify!($name))), $doc, ::std::option::Option::Some(&py.get_type::<$base>()), ::std::option::Option::None, @@ -758,7 +759,7 @@ macro_rules! test_exception { assert!(err.is_instance_of::<$exc_ty>(py)); - let value = err.value_bound(py).as_any().downcast::<$exc_ty>().unwrap(); + let value = err.value(py).as_any().downcast::<$exc_ty>().unwrap(); assert!($crate::PyErr::from(value.clone()).is_instance_of::<$exc_ty>(py)); }) @@ -1072,7 +1073,7 @@ mod tests { let invalid_utf8 = b"fo\xd8o"; #[cfg_attr(invalid_from_utf8_lint, allow(invalid_from_utf8))] let err = std::str::from_utf8(invalid_utf8).expect_err("should be invalid utf8"); - PyErr::from_value_bound( + PyErr::from_value( PyUnicodeDecodeError::new_utf8_bound(py, invalid_utf8, err) .unwrap() .into_any(), diff --git a/src/impl_/extract_argument.rs b/src/impl_/extract_argument.rs index 5402f33e6fa..bb31f96d8b7 100644 --- a/src/impl_/extract_argument.rs +++ b/src/impl_/extract_argument.rs @@ -200,12 +200,9 @@ pub fn from_py_with_with_default<'a, 'py, T>( #[doc(hidden)] #[cold] pub fn argument_extraction_error(py: Python<'_>, arg_name: &str, error: PyErr) -> PyErr { - if error.get_type_bound(py).is(&py.get_type::()) { - let remapped_error = PyTypeError::new_err(format!( - "argument '{}': {}", - arg_name, - error.value_bound(py) - )); + if error.get_type(py).is(&py.get_type::()) { + let remapped_error = + PyTypeError::new_err(format!("argument '{}': {}", arg_name, error.value(py))); remapped_error.set_cause(py, error.cause(py)); remapped_error } else { diff --git a/src/impl_/pyclass.rs b/src/impl_/pyclass.rs index 8d25a7b8920..86b6d4daf5b 100644 --- a/src/impl_/pyclass.rs +++ b/src/impl_/pyclass.rs @@ -1087,7 +1087,7 @@ impl ThreadCheckerImpl { "{} is unsendable, but is being dropped on another thread", type_name )) - .write_unraisable_bound(py, None); + .write_unraisable(py, None); return false; } diff --git a/src/impl_/trampoline.rs b/src/impl_/trampoline.rs index 86dc0067c8b..2892095e736 100644 --- a/src/impl_/trampoline.rs +++ b/src/impl_/trampoline.rs @@ -235,7 +235,7 @@ where if let Err(py_err) = panic::catch_unwind(move || body(py)) .unwrap_or_else(|payload| Err(PanicException::from_panic_payload(payload))) { - py_err.write_unraisable_bound(py, ctx.assume_borrowed_or_opt(py).as_deref()); + py_err.write_unraisable(py, ctx.assume_borrowed_or_opt(py).as_deref()); } trap.disarm(); } diff --git a/src/instance.rs b/src/instance.rs index 6e4e9ab23e7..cb537027203 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -464,7 +464,7 @@ fn python_format( ) -> Result<(), std::fmt::Error> { match format_result { Result::Ok(s) => return f.write_str(&s.to_string_lossy()), - Result::Err(err) => err.write_unraisable_bound(any.py(), Some(any)), + Result::Err(err) => err.write_unraisable(any.py(), Some(any)), } match any.get_type().name() { diff --git a/src/tests/common.rs b/src/tests/common.rs index 83c2f911672..870e30abde0 100644 --- a/src/tests/common.rs +++ b/src/tests/common.rs @@ -74,7 +74,7 @@ mod inner { #[pymethods(crate = "pyo3")] impl UnraisableCapture { pub fn hook(&mut self, unraisable: Bound<'_, PyAny>) { - let err = PyErr::from_value_bound(unraisable.getattr("exc_value").unwrap()); + let err = PyErr::from_value(unraisable.getattr("exc_value").unwrap()); let instance = unraisable.getattr("object").unwrap(); self.capture = Some((err, instance.into())); } diff --git a/src/types/dict.rs b/src/types/dict.rs index 50a9e139355..e589b1cb884 100644 --- a/src/types/dict.rs +++ b/src/types/dict.rs @@ -757,7 +757,7 @@ mod tests { } Err(err) => { assert!(err.is_instance_of::(py)); - assert_eq!(err.value_bound(py).to_string(), "Error from __hash__") + assert_eq!(err.value(py).to_string(), "Error from __hash__") } } }) diff --git a/src/types/mapping.rs b/src/types/mapping.rs index 67a7b9b4d3d..f9946f5b82e 100644 --- a/src/types/mapping.rs +++ b/src/types/mapping.rs @@ -176,7 +176,7 @@ impl PyTypeCheck for PyMapping { || get_mapping_abc(object.py()) .and_then(|abc| object.is_instance(abc)) .unwrap_or_else(|err| { - err.write_unraisable_bound(object.py(), Some(&object.as_borrowed())); + err.write_unraisable(object.py(), Some(&object.as_borrowed())); false }) } diff --git a/src/types/sequence.rs b/src/types/sequence.rs index e413e578cc9..9e70dd8220a 100644 --- a/src/types/sequence.rs +++ b/src/types/sequence.rs @@ -367,7 +367,7 @@ impl PyTypeCheck for PySequence { || get_sequence_abc(object.py()) .and_then(|abc| object.is_instance(abc)) .unwrap_or_else(|err| { - err.write_unraisable_bound(object.py(), Some(&object.as_borrowed())); + err.write_unraisable(object.py(), Some(&object.as_borrowed())); false }) } diff --git a/src/types/string.rs b/src/types/string.rs index eac0f26eff0..ff1e33c74f8 100644 --- a/src/types/string.rs +++ b/src/types/string.rs @@ -696,9 +696,7 @@ mod tests { let data = unsafe { s.data().unwrap() }; assert_eq!(data, PyStringData::Ucs1(b"f\xfe")); let err = data.to_string(py).unwrap_err(); - assert!(err - .get_type_bound(py) - .is(&py.get_type::())); + assert!(err.get_type(py).is(&py.get_type::())); assert!(err .to_string() .contains("'utf-8' codec can't decode byte 0xfe in position 1")); @@ -740,9 +738,7 @@ mod tests { let data = unsafe { s.data().unwrap() }; assert_eq!(data, PyStringData::Ucs2(&[0xff22, 0xd800])); let err = data.to_string(py).unwrap_err(); - assert!(err - .get_type_bound(py) - .is(&py.get_type::())); + assert!(err.get_type(py).is(&py.get_type::())); assert!(err .to_string() .contains("'utf-16' codec can't decode bytes in position 0-3")); @@ -781,9 +777,7 @@ mod tests { let data = unsafe { s.data().unwrap() }; assert_eq!(data, PyStringData::Ucs4(&[0x20000, 0xd800])); let err = data.to_string(py).unwrap_err(); - assert!(err - .get_type_bound(py) - .is(&py.get_type::())); + assert!(err.get_type(py).is(&py.get_type::())); assert!(err .to_string() .contains("'utf-32' codec can't decode bytes in position 0-7")); diff --git a/src/types/traceback.rs b/src/types/traceback.rs index 89b26d8df72..6c9909e7113 100644 --- a/src/types/traceback.rs +++ b/src/types/traceback.rs @@ -42,7 +42,7 @@ pub trait PyTracebackMethods<'py>: crate::sealed::Sealed { /// .run(c_str!("raise Exception('banana')"), None, None) /// .expect_err("raise will create a Python error"); /// - /// let traceback = err.traceback_bound(py).expect("raised exception will have a traceback"); + /// let traceback = err.traceback(py).expect("raised exception will have a traceback"); /// assert_eq!( /// format!("{}{}", traceback.format()?, err), /// "\ @@ -94,7 +94,7 @@ mod tests { .expect_err("raising should have given us an error"); assert_eq!( - err.traceback_bound(py).unwrap().format().unwrap(), + err.traceback(py).unwrap().format().unwrap(), "Traceback (most recent call last):\n File \"\", line 1, in \n" ); }) @@ -118,9 +118,9 @@ except Exception as e: Some(&locals), ) .unwrap(); - let err = PyErr::from_value_bound(locals.get_item("err").unwrap().unwrap()); - let traceback = err.value_bound(py).getattr("__traceback__").unwrap(); - assert!(err.traceback_bound(py).unwrap().is(&traceback)); + let err = PyErr::from_value(locals.get_item("err").unwrap().unwrap()); + let traceback = err.value(py).getattr("__traceback__").unwrap(); + assert!(err.traceback(py).unwrap().is(&traceback)); }) } @@ -142,7 +142,7 @@ def f(): .unwrap(); let f = locals.get_item("f").unwrap().unwrap(); let err = f.call0().unwrap_err(); - let traceback = err.traceback_bound(py).unwrap(); + let traceback = err.traceback(py).unwrap(); let err_object = err.clone_ref(py).into_py(py).into_bound(py); assert!(err_object.getattr("__traceback__").unwrap().is(&traceback)); diff --git a/src/types/weakref/proxy.rs b/src/types/weakref/proxy.rs index 013eeb5e81e..870399fbef4 100644 --- a/src/types/weakref/proxy.rs +++ b/src/types/weakref/proxy.rs @@ -276,7 +276,7 @@ mod tests { let result = err.is_instance_of::(py); #[cfg(not(Py_LIMITED_API))] let result = result - & (err.value_bound(py).to_string() + & (err.value(py).to_string() == format!("{} object is not callable", CLASS_NAME)); result })); @@ -300,7 +300,7 @@ mod tests { let result = err.is_instance_of::(py); #[cfg(not(Py_LIMITED_API))] let result = result - & (err.value_bound(py).to_string() + & (err.value(py).to_string() == format!("{} object is not callable", CLASS_NAME)); result })); @@ -550,7 +550,7 @@ mod tests { let result = err.is_instance_of::(py); #[cfg(not(Py_LIMITED_API))] let result = result - & (err.value_bound(py).to_string() + & (err.value(py).to_string() == format!("{} object is not callable", CLASS_NAME)); result })); @@ -574,7 +574,7 @@ mod tests { let result = err.is_instance_of::(py); #[cfg(not(Py_LIMITED_API))] let result = result - & (err.value_bound(py).to_string() + & (err.value(py).to_string() == format!("{} object is not callable", CLASS_NAME)); result })); @@ -835,7 +835,7 @@ mod tests { .call0() .err() .map_or(false, |err| err.is_instance_of::(py) - & (err.value_bound(py).to_string() + & (err.value(py).to_string() == "weakly-referenced object no longer exists"))); Ok(()) @@ -1104,7 +1104,7 @@ mod tests { .call0() .err() .map_or(false, |err| err.is_instance_of::(py) - & (err.value_bound(py).to_string() + & (err.value(py).to_string() == "weakly-referenced object no longer exists"))); Ok(()) diff --git a/tests/test_coroutine.rs b/tests/test_coroutine.rs index e098e21e89c..bb6889f4c0d 100644 --- a/tests/test_coroutine.rs +++ b/tests/test_coroutine.rs @@ -158,7 +158,7 @@ fn cancelled_coroutine() { ) .unwrap_err(); assert_eq!( - err.value_bound(gil).get_type().qualname().unwrap(), + err.value(gil).get_type().qualname().unwrap(), "CancelledError" ); }) diff --git a/tests/test_exceptions.rs b/tests/test_exceptions.rs index cc823136997..34772be5719 100644 --- a/tests/test_exceptions.rs +++ b/tests/test_exceptions.rs @@ -110,14 +110,14 @@ fn test_write_unraisable() { assert!(capture.borrow(py).capture.is_none()); let err = PyRuntimeError::new_err("foo"); - err.write_unraisable_bound(py, None); + err.write_unraisable(py, None); let (err, object) = capture.borrow_mut(py).capture.take().unwrap(); assert_eq!(err.to_string(), "RuntimeError: foo"); assert!(object.is_none(py)); let err = PyRuntimeError::new_err("bar"); - err.write_unraisable_bound(py, Some(&PyNotImplemented::get(py))); + err.write_unraisable(py, Some(&PyNotImplemented::get(py))); let (err, object) = capture.borrow_mut(py).capture.take().unwrap(); assert_eq!(err.to_string(), "RuntimeError: bar"); From 03e85c2fdacb3d9c3e6bf270e4cdaf75462e4c2f Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Sat, 24 Aug 2024 00:57:31 -0600 Subject: [PATCH 235/495] add bindings for critical section API (#4477) * add bindings for critical section API * fix build on python < 3.13 * add release note * fix clippy on gil-enabled 3.13 build --- newsfragments/4477.added.md | 1 + pyo3-ffi/src/cpython/critical_section.rs | 30 ++++++++++++++++++++++++ pyo3-ffi/src/cpython/mod.rs | 4 ++++ 3 files changed, 35 insertions(+) create mode 100644 newsfragments/4477.added.md create mode 100644 pyo3-ffi/src/cpython/critical_section.rs diff --git a/newsfragments/4477.added.md b/newsfragments/4477.added.md new file mode 100644 index 00000000000..d0f43f909d5 --- /dev/null +++ b/newsfragments/4477.added.md @@ -0,0 +1 @@ +* Added bindings for the Python critical section API available on Python 3.13 and newer. diff --git a/pyo3-ffi/src/cpython/critical_section.rs b/pyo3-ffi/src/cpython/critical_section.rs new file mode 100644 index 00000000000..97b2f5e0559 --- /dev/null +++ b/pyo3-ffi/src/cpython/critical_section.rs @@ -0,0 +1,30 @@ +#[cfg(Py_GIL_DISABLED)] +use crate::PyMutex; +use crate::PyObject; + +#[repr(C)] +#[cfg(Py_GIL_DISABLED)] +pub struct PyCriticalSection { + _cs_prev: usize, + _cs_mutex: *mut PyMutex, +} + +#[repr(C)] +#[cfg(Py_GIL_DISABLED)] +pub struct PyCriticalSection2 { + _cs_base: PyCriticalSection, + _cs_mutex2: *mut PyMutex, +} + +#[cfg(not(Py_GIL_DISABLED))] +opaque_struct!(PyCriticalSection); + +#[cfg(not(Py_GIL_DISABLED))] +opaque_struct!(PyCriticalSection2); + +extern "C" { + pub fn PyCriticalSection_Begin(c: *mut PyCriticalSection, op: *mut PyObject); + pub fn PyCriticalSection_End(c: *mut PyCriticalSection); + pub fn PyCriticalSection2_Begin(c: *mut PyCriticalSection2, a: *mut PyObject, b: *mut PyObject); + pub fn PyCriticalSection2_End(c: *mut PyCriticalSection2); +} diff --git a/pyo3-ffi/src/cpython/mod.rs b/pyo3-ffi/src/cpython/mod.rs index 8f850104def..fad9cd72f66 100644 --- a/pyo3-ffi/src/cpython/mod.rs +++ b/pyo3-ffi/src/cpython/mod.rs @@ -5,6 +5,8 @@ pub(crate) mod bytesobject; pub(crate) mod ceval; pub(crate) mod code; pub(crate) mod compile; +#[cfg(Py_3_13)] +pub(crate) mod critical_section; pub(crate) mod descrobject; #[cfg(not(PyPy))] pub(crate) mod dictobject; @@ -45,6 +47,8 @@ pub use self::bytesobject::*; pub use self::ceval::*; pub use self::code::*; pub use self::compile::*; +#[cfg(Py_3_13)] +pub use self::critical_section::*; pub use self::descrobject::*; #[cfg(not(PyPy))] pub use self::dictobject::*; From 44c16ec848e1c9040fd81768a4ed833de5678c26 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Sat, 24 Aug 2024 08:24:37 +0100 Subject: [PATCH 236/495] ci: run coverage jobs in `pull_request_target` context (#4483) --- .github/workflows/ci.yml | 41 -------------------------- .github/workflows/coverage.yml | 54 ++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 41 deletions(-) create mode 100644 .github/workflows/coverage.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ede5d7172b5..6aa3d6fa385 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -385,46 +385,6 @@ jobs: components: rust-src - run: cargo rustdoc --lib --no-default-features --features full -Zunstable-options --config "build.rustdocflags=[\"--cfg\", \"docsrs\"]" - coverage: - needs: [fmt] - name: coverage ${{ matrix.os }} - strategy: - matrix: - os: ["windows-latest", "macos-14", "ubuntu-latest"] # first available arm macos runner - runs-on: ${{ matrix.os }} - steps: - - if: ${{ github.event_name == 'pull_request' && matrix.os != 'ubuntu-latest' }} - id: should-skip - shell: bash - run: echo 'skip=true' >> $GITHUB_OUTPUT - - uses: actions/checkout@v4 - if: steps.should-skip.outputs.skip != 'true' - - uses: actions/setup-python@v5 - if: steps.should-skip.outputs.skip != 'true' - with: - python-version: '3.12' - - uses: Swatinem/rust-cache@v2 - if: steps.should-skip.outputs.skip != 'true' - with: - save-if: ${{ github.event_name != 'merge_group' }} - - uses: dtolnay/rust-toolchain@stable - if: steps.should-skip.outputs.skip != 'true' - with: - 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 - - run: python -m pip install --upgrade pip && pip install nox - if: steps.should-skip.outputs.skip != 'true' - - run: nox -s coverage - if: steps.should-skip.outputs.skip != 'true' - - uses: codecov/codecov-action@v4 - if: steps.should-skip.outputs.skip != 'true' - with: - file: coverage.json - name: ${{ matrix.os }} - token: ${{ secrets.CODECOV_TOKEN }} - emscripten: name: emscripten if: ${{ contains(github.event.pull_request.labels.*.name, 'CI-build-full') || github.event_name != 'pull_request' }} @@ -673,7 +633,6 @@ jobs: - valgrind - careful - docsrs - - coverage - emscripten - test-debug - test-free-threaded diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml new file mode 100644 index 00000000000..94a0a23c42d --- /dev/null +++ b/.github/workflows/coverage.yml @@ -0,0 +1,54 @@ +name: Coverage + +on: + push: + branches: + - main + pull_request_target: + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref_name }}-coverage + cancel-in-progress: true + +env: + CARGO_TERM_COLOR: always + +jobs: + coverage: + name: coverage ${{ matrix.os }} + strategy: + matrix: + os: ["windows-latest", "macos-14", "ubuntu-latest"] # first available arm macos runner + runs-on: ${{ matrix.os }} + steps: + - if: ${{ github.event_name == 'pull_request' && matrix.os != 'ubuntu-latest' }} + id: should-skip + shell: bash + run: echo 'skip=true' >> $GITHUB_OUTPUT + - uses: actions/checkout@v4 + if: steps.should-skip.outputs.skip != 'true' + - uses: actions/setup-python@v5 + if: steps.should-skip.outputs.skip != 'true' + with: + python-version: '3.12' + - uses: Swatinem/rust-cache@v2 + if: steps.should-skip.outputs.skip != 'true' + with: + save-if: ${{ github.event_name != 'merge_group' }} + - uses: dtolnay/rust-toolchain@stable + if: steps.should-skip.outputs.skip != 'true' + with: + 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 + - run: python -m pip install --upgrade pip && pip install nox + if: steps.should-skip.outputs.skip != 'true' + - run: nox -s coverage + if: steps.should-skip.outputs.skip != 'true' + - uses: codecov/codecov-action@v4 + if: steps.should-skip.outputs.skip != 'true' + with: + file: coverage.json + name: ${{ matrix.os }} + token: ${{ secrets.CODECOV_TOKEN }} From ff1b5c414c868cab4afa42873c74df7340a763d0 Mon Sep 17 00:00:00 2001 From: Georg Brandl Date: Sat, 24 Aug 2024 12:57:18 +0200 Subject: [PATCH 237/495] mention type name in "no constructor defined" message (#4481) fixes #4470 --- newsfragments/4481.changed.md | 1 + pytests/tests/test_pyclasses.py | 12 +++++++----- src/pyclass/create_type_object.rs | 15 ++++++++++----- 3 files changed, 18 insertions(+), 10 deletions(-) create mode 100644 newsfragments/4481.changed.md diff --git a/newsfragments/4481.changed.md b/newsfragments/4481.changed.md new file mode 100644 index 00000000000..9ce455c923c --- /dev/null +++ b/newsfragments/4481.changed.md @@ -0,0 +1 @@ +Mention the type name in the exception message when trying to instantiate a class with no constructor defined. diff --git a/pytests/tests/test_pyclasses.py b/pytests/tests/test_pyclasses.py index 0c336ecf2e7..e91e75fa58a 100644 --- a/pytests/tests/test_pyclasses.py +++ b/pytests/tests/test_pyclasses.py @@ -65,13 +65,13 @@ def test_new_classmethod(): _ = AssertingSubClass(expected_type=str) -class ClassWithoutConstructorPy: +class ClassWithoutConstructor: def __new__(cls): - raise TypeError("No constructor defined") + raise TypeError("No constructor defined for ClassWithoutConstructor") @pytest.mark.parametrize( - "cls", [pyclasses.ClassWithoutConstructor, ClassWithoutConstructorPy] + "cls", [pyclasses.ClassWithoutConstructor, ClassWithoutConstructor] ) def test_no_constructor_defined_propagates_cause(cls: Type): original_error = ValueError("Original message") @@ -79,10 +79,12 @@ def test_no_constructor_defined_propagates_cause(cls: Type): try: raise original_error except Exception: - cls() # should raise TypeError("No constructor defined") + cls() # should raise TypeError("No constructor defined for ...") assert exc_info.type is TypeError - assert exc_info.value.args == ("No constructor defined",) + assert exc_info.value.args == ( + "No constructor defined for ClassWithoutConstructor", + ) assert exc_info.value.__context__ is original_error diff --git a/src/pyclass/create_type_object.rs b/src/pyclass/create_type_object.rs index b05d164e2b0..ea601c41dfa 100644 --- a/src/pyclass/create_type_object.rs +++ b/src/pyclass/create_type_object.rs @@ -524,14 +524,19 @@ fn bpo_45315_workaround(py: Python<'_>, class_name: CString) { /// Default new implementation unsafe extern "C" fn no_constructor_defined( - _subtype: *mut ffi::PyTypeObject, + subtype: *mut ffi::PyTypeObject, _args: *mut ffi::PyObject, _kwds: *mut ffi::PyObject, ) -> *mut ffi::PyObject { - trampoline(|_| { - Err(crate::exceptions::PyTypeError::new_err( - "No constructor defined", - )) + trampoline(|py| { + let tpobj = PyType::from_borrowed_type_ptr(py, subtype); + let name = tpobj + .name() + .map_or_else(|_| "".into(), |name| name.to_string()); + Err(crate::exceptions::PyTypeError::new_err(format!( + "No constructor defined for {}", + name + ))) }) } From 7aa2101fda66ce0b1cfaf2d1d8e08197afe464ed Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Sat, 24 Aug 2024 13:42:57 +0100 Subject: [PATCH 238/495] Revert "ci: run coverage jobs in `pull_request_target` context (#4483)" (#4485) This reverts commit 44c16ec848e1c9040fd81768a4ed833de5678c26. --- .github/workflows/ci.yml | 41 ++++++++++++++++++++++++++ .github/workflows/coverage.yml | 54 ---------------------------------- 2 files changed, 41 insertions(+), 54 deletions(-) delete mode 100644 .github/workflows/coverage.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6aa3d6fa385..ede5d7172b5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -385,6 +385,46 @@ jobs: components: rust-src - run: cargo rustdoc --lib --no-default-features --features full -Zunstable-options --config "build.rustdocflags=[\"--cfg\", \"docsrs\"]" + coverage: + needs: [fmt] + name: coverage ${{ matrix.os }} + strategy: + matrix: + os: ["windows-latest", "macos-14", "ubuntu-latest"] # first available arm macos runner + runs-on: ${{ matrix.os }} + steps: + - if: ${{ github.event_name == 'pull_request' && matrix.os != 'ubuntu-latest' }} + id: should-skip + shell: bash + run: echo 'skip=true' >> $GITHUB_OUTPUT + - uses: actions/checkout@v4 + if: steps.should-skip.outputs.skip != 'true' + - uses: actions/setup-python@v5 + if: steps.should-skip.outputs.skip != 'true' + with: + python-version: '3.12' + - uses: Swatinem/rust-cache@v2 + if: steps.should-skip.outputs.skip != 'true' + with: + save-if: ${{ github.event_name != 'merge_group' }} + - uses: dtolnay/rust-toolchain@stable + if: steps.should-skip.outputs.skip != 'true' + with: + 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 + - run: python -m pip install --upgrade pip && pip install nox + if: steps.should-skip.outputs.skip != 'true' + - run: nox -s coverage + if: steps.should-skip.outputs.skip != 'true' + - uses: codecov/codecov-action@v4 + if: steps.should-skip.outputs.skip != 'true' + with: + file: coverage.json + name: ${{ matrix.os }} + token: ${{ secrets.CODECOV_TOKEN }} + emscripten: name: emscripten if: ${{ contains(github.event.pull_request.labels.*.name, 'CI-build-full') || github.event_name != 'pull_request' }} @@ -633,6 +673,7 @@ jobs: - valgrind - careful - docsrs + - coverage - emscripten - test-debug - test-free-threaded diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml deleted file mode 100644 index 94a0a23c42d..00000000000 --- a/.github/workflows/coverage.yml +++ /dev/null @@ -1,54 +0,0 @@ -name: Coverage - -on: - push: - branches: - - main - pull_request_target: - -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref_name }}-coverage - cancel-in-progress: true - -env: - CARGO_TERM_COLOR: always - -jobs: - coverage: - name: coverage ${{ matrix.os }} - strategy: - matrix: - os: ["windows-latest", "macos-14", "ubuntu-latest"] # first available arm macos runner - runs-on: ${{ matrix.os }} - steps: - - if: ${{ github.event_name == 'pull_request' && matrix.os != 'ubuntu-latest' }} - id: should-skip - shell: bash - run: echo 'skip=true' >> $GITHUB_OUTPUT - - uses: actions/checkout@v4 - if: steps.should-skip.outputs.skip != 'true' - - uses: actions/setup-python@v5 - if: steps.should-skip.outputs.skip != 'true' - with: - python-version: '3.12' - - uses: Swatinem/rust-cache@v2 - if: steps.should-skip.outputs.skip != 'true' - with: - save-if: ${{ github.event_name != 'merge_group' }} - - uses: dtolnay/rust-toolchain@stable - if: steps.should-skip.outputs.skip != 'true' - with: - 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 - - run: python -m pip install --upgrade pip && pip install nox - if: steps.should-skip.outputs.skip != 'true' - - run: nox -s coverage - if: steps.should-skip.outputs.skip != 'true' - - uses: codecov/codecov-action@v4 - if: steps.should-skip.outputs.skip != 'true' - with: - file: coverage.json - name: ${{ matrix.os }} - token: ${{ secrets.CODECOV_TOKEN }} From 07da500f687fc1bbe9b71afe56571dadf21cc667 Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Sat, 24 Aug 2024 15:09:35 +0200 Subject: [PATCH 239/495] reintroduce `PyTypeInfo` methods (#4484) --- guide/src/class/numeric.md | 2 +- src/err/mod.rs | 19 ++++++++----------- src/impl_/pymodule.rs | 2 +- src/marker.rs | 2 +- src/tests/common.rs | 2 +- src/type_object.rs | 29 +++++++++++++++++++++++++---- src/types/any.rs | 6 +++--- src/types/code.rs | 2 +- src/types/ellipsis.rs | 8 ++++---- src/types/mapping.rs | 4 ++-- src/types/none.rs | 10 ++++------ src/types/notimplemented.rs | 8 ++++---- src/types/pysuper.rs | 10 ++++------ src/types/sequence.rs | 6 +++--- src/types/typeobject.rs | 4 ++-- 15 files changed, 64 insertions(+), 50 deletions(-) diff --git a/guide/src/class/numeric.md b/guide/src/class/numeric.md index 4144f760def..a441eba4e13 100644 --- a/guide/src/class/numeric.md +++ b/guide/src/class/numeric.md @@ -387,7 +387,7 @@ fn my_module(m: &Bound<'_, PyModule>) -> PyResult<()> { # fn main() -> PyResult<()> { # Python::with_gil(|py| -> PyResult<()> { # let globals = PyModule::import(py, "__main__")?.dict(); -# globals.set_item("Number", Number::type_object_bound(py))?; +# globals.set_item("Number", Number::type_object(py))?; # # py.run(SCRIPT, Some(&globals), None)?; # Ok(()) diff --git a/src/err/mod.rs b/src/err/mod.rs index 7d142350976..c23abcd5dad 100644 --- a/src/err/mod.rs +++ b/src/err/mod.rs @@ -160,7 +160,7 @@ impl PyErr { { PyErr::from_state(PyErrState::Lazy(Box::new(move |py| { PyErrStateLazyFnOutput { - ptype: T::type_object_bound(py).into(), + ptype: T::type_object(py).into(), pvalue: args.arguments(py), } }))) @@ -215,7 +215,7 @@ impl PyErr { /// assert_eq!(err.to_string(), "TypeError: some type error"); /// /// // Case #2: Exception type - /// let err = PyErr::from_value(PyTypeError::type_object_bound(py).into_any()); + /// let err = PyErr::from_value(PyTypeError::type_object(py).into_any()); /// assert_eq!(err.to_string(), "TypeError: "); /// /// // Case #3: Invalid exception value @@ -587,7 +587,7 @@ impl PyErr { where T: PyTypeInfo, { - self.is_instance(py, &T::type_object_bound(py)) + self.is_instance(py, &T::type_object(py)) } /// Writes the error back to the Python interpreter's global state. @@ -1187,21 +1187,18 @@ mod tests { fn test_pyerr_matches() { Python::with_gil(|py| { let err = PyErr::new::("foo"); - assert!(err.matches(py, PyValueError::type_object_bound(py))); + assert!(err.matches(py, PyValueError::type_object(py))); assert!(err.matches( py, - ( - PyValueError::type_object_bound(py), - PyTypeError::type_object_bound(py) - ) + (PyValueError::type_object(py), PyTypeError::type_object(py)) )); - assert!(!err.matches(py, PyTypeError::type_object_bound(py))); + assert!(!err.matches(py, PyTypeError::type_object(py))); // String is not a valid exception class, so we should get a TypeError - let err: PyErr = PyErr::from_type(crate::types::PyString::type_object_bound(py), "foo"); - assert!(err.matches(py, PyTypeError::type_object_bound(py))); + let err: PyErr = PyErr::from_type(crate::types::PyString::type_object(py), "foo"); + assert!(err.matches(py, PyTypeError::type_object(py))); }) } diff --git a/src/impl_/pymodule.rs b/src/impl_/pymodule.rs index 8ce169fffa2..270d32f15a9 100644 --- a/src/impl_/pymodule.rs +++ b/src/impl_/pymodule.rs @@ -176,7 +176,7 @@ impl AddTypeToModule { impl PyAddToModule for AddTypeToModule { fn add_to_module(&'static self, module: &Bound<'_, PyModule>) -> PyResult<()> { - module.add(T::NAME, T::type_object_bound(module.py())) + module.add(T::NAME, T::type_object(module.py())) } } diff --git a/src/marker.rs b/src/marker.rs index e8c978ffe27..0df9b69f8ae 100644 --- a/src/marker.rs +++ b/src/marker.rs @@ -685,7 +685,7 @@ impl<'py> Python<'py> { where T: PyTypeInfo, { - T::type_object_bound(self) + T::type_object(self) } /// Deprecated name for [`Python::get_type`]. diff --git a/src/tests/common.rs b/src/tests/common.rs index 870e30abde0..5ba6ed19401 100644 --- a/src/tests/common.rs +++ b/src/tests/common.rs @@ -144,7 +144,7 @@ mod inner { $crate::tests::common::CatchWarnings::enter($py, |w| { use $crate::types::{PyListMethods, PyStringMethods}; $body; - let expected_warnings = [$((<$category as $crate::type_object::PyTypeInfo>::type_object_bound($py), $message)),+]; + let expected_warnings = [$((<$category as $crate::type_object::PyTypeInfo>::type_object($py), $message)),+]; assert_eq!(w.len(), expected_warnings.len()); for (warning, (category, message)) in w.iter().zip(expected_warnings) { diff --git a/src/type_object.rs b/src/type_object.rs index ed3cbf52de4..72a87b2805b 100644 --- a/src/type_object.rs +++ b/src/type_object.rs @@ -46,7 +46,7 @@ pub unsafe trait PyTypeInfo: Sized { /// Returns the safe abstraction over the type object. #[inline] - fn type_object_bound(py: Python<'_>) -> Bound<'_, PyType> { + fn type_object(py: Python<'_>) -> Bound<'_, PyType> { // Making the borrowed object `Bound` is necessary for soundness reasons. It's an extreme // edge case, but arbitrary Python code _could_ change the __class__ of an object and cause // the type object to be freed. @@ -61,17 +61,38 @@ pub unsafe trait PyTypeInfo: Sized { } } + /// Deprecated name for [`PyTypeInfo::type_object`]. + #[deprecated(since = "0.23.0", note = "renamed to `PyTypeInfo::type_object`")] + #[inline] + fn type_object_bound(py: Python<'_>) -> Bound<'_, PyType> { + Self::type_object(py) + } + /// Checks if `object` is an instance of this type or a subclass of this type. #[inline] - fn is_type_of_bound(object: &Bound<'_, PyAny>) -> bool { + fn is_type_of(object: &Bound<'_, PyAny>) -> bool { unsafe { ffi::PyObject_TypeCheck(object.as_ptr(), Self::type_object_raw(object.py())) != 0 } } + /// Deprecated name for [`PyTypeInfo::is_type_of`]. + #[deprecated(since = "0.23.0", note = "renamed to `PyTypeInfo::is_type_of`")] + #[inline] + fn is_type_of_bound(object: &Bound<'_, PyAny>) -> bool { + Self::is_type_of(object) + } + /// Checks if `object` is an instance of this type. #[inline] - fn is_exact_type_of_bound(object: &Bound<'_, PyAny>) -> bool { + fn is_exact_type_of(object: &Bound<'_, PyAny>) -> bool { unsafe { ffi::Py_TYPE(object.as_ptr()) == Self::type_object_raw(object.py()) } } + + /// Deprecated name for [`PyTypeInfo::is_exact_type_of`]. + #[deprecated(since = "0.23.0", note = "renamed to `PyTypeInfo::is_exact_type_of`")] + #[inline] + fn is_exact_type_of_bound(object: &Bound<'_, PyAny>) -> bool { + Self::is_exact_type_of(object) + } } /// Implemented by types which can be used as a concrete Python type inside `Py` smart pointers. @@ -93,7 +114,7 @@ where #[inline] fn type_check(object: &Bound<'_, PyAny>) -> bool { - T::is_type_of_bound(object) + T::is_type_of(object) } } diff --git a/src/types/any.rs b/src/types/any.rs index 6dff9a1264d..c3eb163b064 100644 --- a/src/types/any.rs +++ b/src/types/any.rs @@ -1430,12 +1430,12 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { #[inline] fn is_instance_of(&self) -> bool { - T::is_type_of_bound(self) + T::is_type_of(self) } #[inline] fn is_exact_instance_of(&self) -> bool { - T::is_exact_type_of_bound(self) + T::is_exact_type_of(self) } fn contains(&self, value: V) -> PyResult @@ -1917,7 +1917,7 @@ class SimpleClass: #[test] fn test_is_callable() { Python::with_gil(|py| { - assert!(PyList::type_object_bound(py).is_callable()); + assert!(PyList::type_object(py).is_callable()); let not_callable = 5.to_object(py).into_bound(py); assert!(!not_callable.is_callable()); diff --git a/src/types/code.rs b/src/types/code.rs index 04e1efb9fe7..0c1683c75be 100644 --- a/src/types/code.rs +++ b/src/types/code.rs @@ -23,7 +23,7 @@ mod tests { #[test] fn test_type_object() { Python::with_gil(|py| { - assert_eq!(PyCode::type_object_bound(py).name().unwrap(), "code"); + assert_eq!(PyCode::type_object(py).name().unwrap(), "code"); }) } } diff --git a/src/types/ellipsis.rs b/src/types/ellipsis.rs index 4507ff6253b..ee5898c9013 100644 --- a/src/types/ellipsis.rs +++ b/src/types/ellipsis.rs @@ -37,13 +37,13 @@ unsafe impl PyTypeInfo for PyEllipsis { } #[inline] - fn is_type_of_bound(object: &Bound<'_, PyAny>) -> bool { + fn is_type_of(object: &Bound<'_, PyAny>) -> bool { // ellipsis is not usable as a base type - Self::is_exact_type_of_bound(object) + Self::is_exact_type_of(object) } #[inline] - fn is_exact_type_of_bound(object: &Bound<'_, PyAny>) -> bool { + fn is_exact_type_of(object: &Bound<'_, PyAny>) -> bool { object.is(&**Self::get(object.py())) } } @@ -67,7 +67,7 @@ mod tests { Python::with_gil(|py| { assert!(PyEllipsis::get(py) .get_type() - .is(&PyEllipsis::type_object_bound(py))); + .is(&PyEllipsis::type_object(py))); }) } diff --git a/src/types/mapping.rs b/src/types/mapping.rs index f9946f5b82e..4cacc04f184 100644 --- a/src/types/mapping.rs +++ b/src/types/mapping.rs @@ -24,7 +24,7 @@ impl PyMapping { /// library). This is equivalent to `collections.abc.Mapping.register(T)` in Python. /// This registration is required for a pyclass to be downcastable from `PyAny` to `PyMapping`. pub fn register(py: Python<'_>) -> PyResult<()> { - let ty = T::type_object_bound(py); + let ty = T::type_object(py); get_mapping_abc(py)?.call_method1("register", (ty,))?; Ok(()) } @@ -172,7 +172,7 @@ impl PyTypeCheck for PyMapping { fn type_check(object: &Bound<'_, PyAny>) -> bool { // Using `is_instance` for `collections.abc.Mapping` is slow, so provide // optimized case dict as a well-known mapping - PyDict::is_type_of_bound(object) + PyDict::is_type_of(object) || get_mapping_abc(object.py()) .and_then(|abc| object.is_instance(abc)) .unwrap_or_else(|err| { diff --git a/src/types/none.rs b/src/types/none.rs index 2a1c54535d7..47a1ac25848 100644 --- a/src/types/none.rs +++ b/src/types/none.rs @@ -38,13 +38,13 @@ unsafe impl PyTypeInfo for PyNone { } #[inline] - fn is_type_of_bound(object: &Bound<'_, PyAny>) -> bool { + fn is_type_of(object: &Bound<'_, PyAny>) -> bool { // NoneType is not usable as a base type - Self::is_exact_type_of_bound(object) + Self::is_exact_type_of(object) } #[inline] - fn is_exact_type_of_bound(object: &Bound<'_, PyAny>) -> bool { + fn is_exact_type_of(object: &Bound<'_, PyAny>) -> bool { object.is(&**Self::get(object.py())) } } @@ -79,9 +79,7 @@ mod tests { #[test] fn test_none_type_object_consistent() { Python::with_gil(|py| { - assert!(PyNone::get(py) - .get_type() - .is(&PyNone::type_object_bound(py))); + assert!(PyNone::get(py).get_type().is(&PyNone::type_object(py))); }) } diff --git a/src/types/notimplemented.rs b/src/types/notimplemented.rs index c1c3d1c393a..d93ab466d2d 100644 --- a/src/types/notimplemented.rs +++ b/src/types/notimplemented.rs @@ -40,13 +40,13 @@ unsafe impl PyTypeInfo for PyNotImplemented { } #[inline] - fn is_type_of_bound(object: &Bound<'_, PyAny>) -> bool { + fn is_type_of(object: &Bound<'_, PyAny>) -> bool { // NotImplementedType is not usable as a base type - Self::is_exact_type_of_bound(object) + Self::is_exact_type_of(object) } #[inline] - fn is_exact_type_of_bound(object: &Bound<'_, PyAny>) -> bool { + fn is_exact_type_of(object: &Bound<'_, PyAny>) -> bool { object.is(&**Self::get(object.py())) } } @@ -70,7 +70,7 @@ mod tests { Python::with_gil(|py| { assert!(PyNotImplemented::get(py) .get_type() - .is(&PyNotImplemented::type_object_bound(py))); + .is(&PyNotImplemented::type_object(py))); }) } diff --git a/src/types/pysuper.rs b/src/types/pysuper.rs index c4e15baca7c..81db8cea869 100644 --- a/src/types/pysuper.rs +++ b/src/types/pysuper.rs @@ -61,12 +61,10 @@ impl PySuper { ty: &Bound<'py, PyType>, obj: &Bound<'py, PyAny>, ) -> PyResult> { - PySuper::type_object_bound(ty.py()) - .call1((ty, obj)) - .map(|any| { - // Safety: super() always returns instance of super - unsafe { any.downcast_into_unchecked() } - }) + PySuper::type_object(ty.py()).call1((ty, obj)).map(|any| { + // Safety: super() always returns instance of super + unsafe { any.downcast_into_unchecked() } + }) } /// Deprecated name for [`PySuper::new`]. diff --git a/src/types/sequence.rs b/src/types/sequence.rs index 9e70dd8220a..4fa72f1219f 100644 --- a/src/types/sequence.rs +++ b/src/types/sequence.rs @@ -27,7 +27,7 @@ impl PySequence { /// library). This is equivalent to `collections.abc.Sequence.register(T)` in Python. /// This registration is required for a pyclass to be downcastable from `PyAny` to `PySequence`. pub fn register(py: Python<'_>) -> PyResult<()> { - let ty = T::type_object_bound(py); + let ty = T::type_object(py); get_sequence_abc(py)?.call_method1("register", (ty,))?; Ok(()) } @@ -362,8 +362,8 @@ impl PyTypeCheck for PySequence { fn type_check(object: &Bound<'_, PyAny>) -> bool { // Using `is_instance` for `collections.abc.Sequence` is slow, so provide // optimized cases for list and tuples as common well-known sequences - PyList::is_type_of_bound(object) - || PyTuple::is_type_of_bound(object) + PyList::is_type_of(object) + || PyTuple::is_type_of(object) || get_sequence_abc(object.py()) .and_then(|abc| object.is_instance(abc)) .unwrap_or_else(|err| { diff --git a/src/types/typeobject.rs b/src/types/typeobject.rs index 1d05015b591..152da881ea9 100644 --- a/src/types/typeobject.rs +++ b/src/types/typeobject.rs @@ -24,7 +24,7 @@ impl PyType { /// Creates a new type object. #[inline] pub fn new(py: Python<'_>) -> Bound<'_, PyType> { - T::type_object_bound(py) + T::type_object(py) } /// Deprecated name for [`PyType::new`]. @@ -202,7 +202,7 @@ impl<'py> PyTypeMethods<'py> for Bound<'py, PyType> { where T: PyTypeInfo, { - self.is_subclass(&T::type_object_bound(self.py())) + self.is_subclass(&T::type_object(self.py())) } fn mro(&self) -> Bound<'py, PyTuple> { From 1c9885857db4632470735d6179305dff8f78379f Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Sat, 24 Aug 2024 11:59:27 -0600 Subject: [PATCH 240/495] skip tests using thread-unsafe constructs on free-threaded build (#4476) * skip tests using thread-unsafe constructs on free-threaded build * fix clippy with features=full --- src/conversions/chrono.rs | 5 ++++- src/err/mod.rs | 2 ++ src/tests/common.rs | 12 +++++++++--- src/tests/mod.rs | 1 + tests/test_buffer_protocol.rs | 2 +- tests/test_class_basics.rs | 2 +- tests/test_exceptions.rs | 2 +- 7 files changed, 19 insertions(+), 7 deletions(-) diff --git a/src/conversions/chrono.rs b/src/conversions/chrono.rs index 5e61716975e..5a0f9c0231e 100644 --- a/src/conversions/chrono.rs +++ b/src/conversions/chrono.rs @@ -1098,6 +1098,7 @@ mod tests { check_utc("regular", 2014, 5, 6, 7, 8, 9, 999_999, 999_999); + #[cfg(not(Py_GIL_DISABLED))] assert_warnings!( py, check_utc("leap second", 2014, 5, 6, 7, 8, 59, 1_999_999, 999_999), @@ -1140,6 +1141,7 @@ mod tests { check_fixed_offset("regular", 2014, 5, 6, 7, 8, 9, 999_999, 999_999); + #[cfg(not(Py_GIL_DISABLED))] assert_warnings!( py, check_fixed_offset("leap second", 2014, 5, 6, 7, 8, 59, 1_999_999, 999_999), @@ -1295,6 +1297,7 @@ mod tests { check_time("regular", 3, 5, 7, 999_999, 999_999); + #[cfg(not(Py_GIL_DISABLED))] assert_warnings!( py, check_time("leap second", 3, 5, 59, 1_999_999, 999_999), @@ -1342,7 +1345,7 @@ mod tests { .unwrap() } - #[cfg(not(target_arch = "wasm32"))] + #[cfg(not(any(target_arch = "wasm32", Py_GIL_DISABLED)))] mod proptests { use super::*; use crate::tests::common::CatchWarnings; diff --git a/src/err/mod.rs b/src/err/mod.rs index c23abcd5dad..2ef9e531768 100644 --- a/src/err/mod.rs +++ b/src/err/mod.rs @@ -1248,6 +1248,7 @@ mod tests { warnings.call_method0("resetwarnings").unwrap(); // First, test the warning is emitted + #[cfg(not(Py_GIL_DISABLED))] assert_warnings!( py, { PyErr::warn(py, &cls, ffi::c_str!("I am warning you"), 0).unwrap() }, @@ -1267,6 +1268,7 @@ mod tests { .unwrap(); // This has the wrong module and will not raise, just be emitted + #[cfg(not(Py_GIL_DISABLED))] assert_warnings!( py, { PyErr::warn(py, &cls, ffi::c_str!("I am warning you"), 0).unwrap() }, diff --git a/src/tests/common.rs b/src/tests/common.rs index 5ba6ed19401..8af20a2c2fa 100644 --- a/src/tests/common.rs +++ b/src/tests/common.rs @@ -9,8 +9,10 @@ mod inner { #[allow(unused_imports)] // pulls in `use crate as pyo3` in `test_utils.rs` use super::*; + #[cfg(not(Py_GIL_DISABLED))] use pyo3::prelude::*; + #[cfg(not(Py_GIL_DISABLED))] use pyo3::types::{IntoPyDict, PyList}; #[macro_export] @@ -63,14 +65,14 @@ mod inner { } // sys.unraisablehook not available until Python 3.8 - #[cfg(all(feature = "macros", Py_3_8))] + #[cfg(all(feature = "macros", Py_3_8, not(Py_GIL_DISABLED)))] #[pyclass(crate = "pyo3")] pub struct UnraisableCapture { pub capture: Option<(PyErr, PyObject)>, old_hook: Option, } - #[cfg(all(feature = "macros", Py_3_8))] + #[cfg(all(feature = "macros", Py_3_8, not(Py_GIL_DISABLED)))] #[pymethods(crate = "pyo3")] impl UnraisableCapture { pub fn hook(&mut self, unraisable: Bound<'_, PyAny>) { @@ -80,7 +82,7 @@ mod inner { } } - #[cfg(all(feature = "macros", Py_3_8))] + #[cfg(all(feature = "macros", Py_3_8, not(Py_GIL_DISABLED)))] impl UnraisableCapture { pub fn install(py: Python<'_>) -> Py { let sys = py.import("sys").unwrap(); @@ -109,10 +111,12 @@ mod inner { } } + #[cfg(not(Py_GIL_DISABLED))] pub struct CatchWarnings<'py> { catch_warnings: Bound<'py, PyAny>, } + #[cfg(not(Py_GIL_DISABLED))] impl<'py> CatchWarnings<'py> { pub fn enter( py: Python<'py>, @@ -129,6 +133,7 @@ mod inner { } } + #[cfg(not(Py_GIL_DISABLED))] impl Drop for CatchWarnings<'_> { fn drop(&mut self) { let py = self.catch_warnings.py(); @@ -138,6 +143,7 @@ mod inner { } } + #[cfg(not(Py_GIL_DISABLED))] #[macro_export] macro_rules! assert_warnings { ($py:expr, $body:expr, [$(($category:ty, $message:literal)),+] $(,)? ) => {{ diff --git a/src/tests/mod.rs b/src/tests/mod.rs index 7e78b6f19bd..136236d7db0 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -1,5 +1,6 @@ #[macro_use] pub(crate) mod common { + #[cfg(not(Py_GIL_DISABLED))] use crate as pyo3; include!("./common.rs"); } diff --git a/tests/test_buffer_protocol.rs b/tests/test_buffer_protocol.rs index 6ee7b0db328..4d3efec3e4f 100644 --- a/tests/test_buffer_protocol.rs +++ b/tests/test_buffer_protocol.rs @@ -94,7 +94,7 @@ fn test_buffer_referenced() { } #[test] -#[cfg(Py_3_8)] // sys.unraisablehook not available until Python 3.8 +#[cfg(all(Py_3_8, not(Py_GIL_DISABLED)))] // sys.unraisablehook not available until Python 3.8 fn test_releasebuffer_unraisable_error() { use common::UnraisableCapture; use pyo3::exceptions::PyValueError; diff --git a/tests/test_class_basics.rs b/tests/test_class_basics.rs index 5b91ca9e695..a6fb89831cc 100644 --- a/tests/test_class_basics.rs +++ b/tests/test_class_basics.rs @@ -615,7 +615,7 @@ fn access_frozen_class_without_gil() { } #[test] -#[cfg(Py_3_8)] // sys.unraisablehook not available until Python 3.8 +#[cfg(all(Py_3_8, not(Py_GIL_DISABLED)))] // sys.unraisablehook not available until Python 3.8 #[cfg_attr(target_arch = "wasm32", ignore)] fn drop_unsendable_elsewhere() { use common::UnraisableCapture; diff --git a/tests/test_exceptions.rs b/tests/test_exceptions.rs index 34772be5719..777c2aa22d4 100644 --- a/tests/test_exceptions.rs +++ b/tests/test_exceptions.rs @@ -99,7 +99,7 @@ fn test_exception_nosegfault() { } #[test] -#[cfg(Py_3_8)] +#[cfg(all(Py_3_8, not(Py_GIL_DISABLED)))] fn test_write_unraisable() { use common::UnraisableCapture; use pyo3::{exceptions::PyRuntimeError, ffi, types::PyNotImplemented}; From 7d399fff797637e391bb11c4af8fff48f73faf20 Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Sat, 24 Aug 2024 21:27:04 +0200 Subject: [PATCH 241/495] initial migration of the trait bounds to `IntoPyObject` (`PyAnyMethods`) (#4480) * initial migration of the trait bounds to `IntoPyObject` * improve `PyBytes` comment wording Co-authored-by: Lily Foote --------- Co-authored-by: Lily Foote --- guide/src/migration.md | 65 +++++++ src/conversions/chrono.rs | 17 +- src/conversions/std/num.rs | 10 + src/instance.rs | 61 +++--- src/prelude.rs | 2 +- src/types/any.rs | 387 ++++++++++++++++++++++++++----------- src/types/bytes.rs | 8 +- src/types/datetime.rs | 6 +- src/types/mapping.rs | 33 ++-- 9 files changed, 425 insertions(+), 164 deletions(-) diff --git a/guide/src/migration.md b/guide/src/migration.md index 8a6ffedc73c..b1dfc9e30c4 100644 --- a/guide/src/migration.md +++ b/guide/src/migration.md @@ -133,6 +133,71 @@ This is purely additional and should just extend the possible return types. +### Python API trait bounds changed +
+Click to expand + +PyO3 0.23 introduces a new unified `IntoPyObject` trait to convert Rust types into Python objects. +Notable features of this new trait: +- conversions can now return an error +- compared to `IntoPy` the generic `T` moved into an associated type, so + - there is now only one way to convert a given type + - the output type is stronger typed and may return any Python type instead of just `PyAny` +- byte collections are special handled and convert into `PyBytes` now, see [above](#macro-conversion-changed-for-byte-collections-vecu8-u8-n-and-smallvecu8-n) +- `()` (unit) is now only special handled in return position and otherwise converts into an empty `PyTuple` + +All PyO3 provided types as well as `#[pyclass]`es already implement `IntoPyObject`. Other types will +need to adapt an implementation of `IntoPyObject` to stay compatible with the Python APIs. + + +Before: +```rust +# use pyo3::prelude::*; +# #[allow(dead_code)] +struct MyPyObjectWrapper(PyObject); + +impl IntoPy for MyPyObjectWrapper { + fn into_py(self, py: Python<'_>) -> PyObject { + self.0 + } +} + +impl ToPyObject for MyPyObjectWrapper { + fn to_object(&self, py: Python<'_>) -> PyObject { + self.0.clone_ref(py) + } +} +``` + +After: +```rust +# use pyo3::prelude::*; +# #[allow(dead_code)] +# struct MyPyObjectWrapper(PyObject); + +impl<'py> IntoPyObject<'py> for MyPyObjectWrapper { + type Target = PyAny; // the Python type + type Output = Bound<'py, Self::Target>; // in most cases this will be `Bound` + type Error = std::convert::Infallible; + + fn into_pyobject(self, py: Python<'py>) -> Result { + Ok(self.0.into_bound(py)) + } +} + +// `ToPyObject` implementations should be converted to implementations on reference types +impl<'a, 'py> IntoPyObject<'py> for &'a MyPyObjectWrapper { + type Target = PyAny; + type Output = Borrowed<'a, 'py, Self::Target>; // `Borrowed` can be used to optimized reference counting + type Error = std::convert::Infallible; + + fn into_pyobject(self, py: Python<'py>) -> Result { + Ok(self.0.bind_borrowed(py)) + } +} +``` +
+ ## from 0.21.* to 0.22 ### Deprecation of `gil-refs` feature continues diff --git a/src/conversions/chrono.rs b/src/conversions/chrono.rs index 5a0f9c0231e..1fc3f5ba4b6 100644 --- a/src/conversions/chrono.rs +++ b/src/conversions/chrono.rs @@ -47,6 +47,7 @@ use crate::sync::GILOnceCell; use crate::types::any::PyAnyMethods; #[cfg(not(Py_LIMITED_API))] use crate::types::datetime::timezone_from_offset; +use crate::types::PyNone; #[cfg(not(Py_LIMITED_API))] use crate::types::{ timezone_utc, PyDate, PyDateAccess, PyDateTime, PyDelta, PyDeltaAccess, PyTime, PyTimeAccess, @@ -553,12 +554,12 @@ impl FromPyObject<'_> for FixedOffset { #[cfg(Py_LIMITED_API)] check_type(ob, &DatetimeTypes::get(ob.py()).tzinfo, "PyTzInfo")?; - // Passing `()` (so Python's None) to the `utcoffset` function will only + // Passing Python's None to the `utcoffset` function will only // work for timezones defined as fixed offsets in Python. // Any other timezone would require a datetime as the parameter, and return // None if the datetime is not provided. // Trying to convert None to a PyDelta in the next line will then fail. - let py_timedelta = ob.call_method1("utcoffset", ((),))?; + let py_timedelta = ob.call_method1("utcoffset", (PyNone::get(ob.py()),))?; if py_timedelta.is_none() { return Err(PyTypeError::new_err(format!( "{:?} is not a fixed offset timezone", @@ -812,7 +813,7 @@ fn timezone_utc(py: Python<'_>) -> Bound<'_, PyAny> { #[cfg(test)] mod tests { use super::*; - use crate::{types::PyTuple, Py}; + use crate::types::PyTuple; use std::{cmp::Ordering, panic}; #[test] @@ -1323,11 +1324,11 @@ mod tests { }) } - fn new_py_datetime_ob<'py>( - py: Python<'py>, - name: &str, - args: impl IntoPy>, - ) -> Bound<'py, PyAny> { + fn new_py_datetime_ob<'py, A>(py: Python<'py>, name: &str, args: A) -> Bound<'py, PyAny> + where + A: IntoPyObject<'py, Target = PyTuple>, + A::Error: Into, + { py.import("datetime") .unwrap() .getattr(name) diff --git a/src/conversions/std/num.rs b/src/conversions/std/num.rs index 954aebb14a3..80bc678fddf 100644 --- a/src/conversions/std/num.rs +++ b/src/conversions/std/num.rs @@ -46,6 +46,16 @@ macro_rules! int_fits_larger_int { } } + impl<'py> IntoPyObject<'py> for &$rust_type { + type Target = PyInt; + type Output = Bound<'py, Self::Target>; + type Error = Infallible; + + fn into_pyobject(self, py: Python<'py>) -> Result { + (*self).into_pyobject(py) + } + } + impl FromPyObject<'_> for $rust_type { fn extract_bound(obj: &Bound<'_, PyAny>) -> PyResult { let val: $larger_type = obj.extract()?; diff --git a/src/instance.rs b/src/instance.rs index cb537027203..ff35fe293a7 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -1,3 +1,4 @@ +use crate::conversion::IntoPyObject; use crate::err::{self, PyErr, PyResult}; use crate::impl_::pycell::PyClassObject; use crate::internal_tricks::ptr_from_ref; @@ -1426,9 +1427,10 @@ impl Py { /// # version(sys, py).unwrap(); /// # }); /// ``` - pub fn getattr(&self, py: Python<'_>, attr_name: N) -> PyResult + pub fn getattr<'py, N>(&self, py: Python<'py>, attr_name: N) -> PyResult where - N: IntoPy>, + N: IntoPyObject<'py, Target = PyString>, + N::Error: Into, { self.bind(py).as_any().getattr(attr_name).map(Bound::unbind) } @@ -1455,32 +1457,40 @@ impl Py { /// # set_answer(ob, py).unwrap(); /// # }); /// ``` - pub fn setattr(&self, py: Python<'_>, attr_name: N, value: V) -> PyResult<()> + pub fn setattr<'py, N, V>(&self, py: Python<'py>, attr_name: N, value: V) -> PyResult<()> where - N: IntoPy>, - V: IntoPy>, + N: IntoPyObject<'py, Target = PyString>, + V: IntoPyObject<'py>, + N::Error: Into, + V::Error: Into, { - self.bind(py) - .as_any() - .setattr(attr_name, value.into_py(py).into_bound(py)) + self.bind(py).as_any().setattr(attr_name, value) } /// Calls the object. /// /// This is equivalent to the Python expression `self(*args, **kwargs)`. - pub fn call_bound( + pub fn call_bound<'py, N>( &self, - py: Python<'_>, - args: impl IntoPy>, - kwargs: Option<&Bound<'_, PyDict>>, - ) -> PyResult { + py: Python<'py>, + args: N, + kwargs: Option<&Bound<'py, PyDict>>, + ) -> PyResult + where + N: IntoPyObject<'py, Target = PyTuple>, + N::Error: Into, + { self.bind(py).as_any().call(args, kwargs).map(Bound::unbind) } /// Calls the object with only positional arguments. /// /// This is equivalent to the Python expression `self(*args)`. - pub fn call1(&self, py: Python<'_>, args: impl IntoPy>) -> PyResult { + pub fn call1<'py, N>(&self, py: Python<'py>, args: N) -> PyResult + where + N: IntoPyObject<'py, Target = PyTuple>, + N::Error: Into, + { self.bind(py).as_any().call1(args).map(Bound::unbind) } @@ -1497,16 +1507,18 @@ impl Py { /// /// To avoid repeated temporary allocations of Python strings, the [`intern!`](crate::intern) /// macro can be used to intern `name`. - pub fn call_method_bound( + pub fn call_method_bound<'py, N, A>( &self, - py: Python<'_>, + py: Python<'py>, name: N, args: A, kwargs: Option<&Bound<'_, PyDict>>, ) -> PyResult where - N: IntoPy>, - A: IntoPy>, + N: IntoPyObject<'py, Target = PyString>, + A: IntoPyObject<'py, Target = PyTuple>, + N::Error: Into, + A::Error: Into, { self.bind(py) .as_any() @@ -1520,10 +1532,12 @@ impl Py { /// /// To avoid repeated temporary allocations of Python strings, the [`intern!`](crate::intern) /// macro can be used to intern `name`. - pub fn call_method1(&self, py: Python<'_>, name: N, args: A) -> PyResult + pub fn call_method1<'py, N, A>(&self, py: Python<'py>, name: N, args: A) -> PyResult where - N: IntoPy>, - A: IntoPy>, + N: IntoPyObject<'py, Target = PyString>, + A: IntoPyObject<'py, Target = PyTuple>, + N::Error: Into, + A::Error: Into, { self.bind(py) .as_any() @@ -1537,9 +1551,10 @@ impl Py { /// /// To avoid repeated temporary allocations of Python strings, the [`intern!`](crate::intern) /// macro can be used to intern `name`. - pub fn call_method0(&self, py: Python<'_>, name: N) -> PyResult + pub fn call_method0<'py, N>(&self, py: Python<'py>, name: N) -> PyResult where - N: IntoPy>, + N: IntoPyObject<'py, Target = PyString>, + N::Error: Into, { self.bind(py).as_any().call_method0(name).map(Bound::unbind) } diff --git a/src/prelude.rs b/src/prelude.rs index 6182b21c2d1..b2b86c8449d 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -8,7 +8,7 @@ //! use pyo3::prelude::*; //! ``` -pub use crate::conversion::{FromPyObject, IntoPy, ToPyObject}; +pub use crate::conversion::{FromPyObject, IntoPy, IntoPyObject, ToPyObject}; pub use crate::err::{PyErr, PyResult}; pub use crate::instance::{Borrowed, Bound, Py, PyObject}; pub use crate::marker::Python; diff --git a/src/types/any.rs b/src/types/any.rs index c3eb163b064..2ad62111a68 100644 --- a/src/types/any.rs +++ b/src/types/any.rs @@ -1,5 +1,5 @@ use crate::class::basic::CompareOp; -use crate::conversion::{AsPyPointer, FromPyObjectBound, IntoPy, ToPyObject}; +use crate::conversion::{AsPyPointer, FromPyObjectBound, IntoPyObject}; use crate::err::{DowncastError, DowncastIntoError, PyErr, PyResult}; use crate::exceptions::{PyAttributeError, PyTypeError}; use crate::ffi_ptr_ext::FfiPtrExt; @@ -10,7 +10,7 @@ use crate::type_object::{PyTypeCheck, PyTypeInfo}; #[cfg(not(any(PyPy, GraalPy)))] use crate::types::PySuper; use crate::types::{PyDict, PyIterator, PyList, PyString, PyTuple, PyType}; -use crate::{err, ffi, Py, Python}; +use crate::{err, ffi, BoundObject, Python}; use std::cell::UnsafeCell; use std::cmp::Ordering; use std::os::raw::c_int; @@ -81,7 +81,8 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed { /// ``` fn hasattr(&self, attr_name: N) -> PyResult where - N: IntoPy>; + N: IntoPyObject<'py, Target = PyString>, + N::Error: Into; /// Retrieves an attribute value. /// @@ -107,7 +108,8 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed { /// ``` fn getattr(&self, attr_name: N) -> PyResult> where - N: IntoPy>; + N: IntoPyObject<'py, Target = PyString>, + N::Error: Into; /// Sets an attribute value. /// @@ -133,8 +135,10 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed { /// ``` fn setattr(&self, attr_name: N, value: V) -> PyResult<()> where - N: IntoPy>, - V: ToPyObject; + N: IntoPyObject<'py, Target = PyString>, + V: IntoPyObject<'py>, + N::Error: Into, + V::Error: Into; /// Deletes an attribute. /// @@ -144,7 +148,8 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed { /// to intern `attr_name`. fn delattr(&self, attr_name: N) -> PyResult<()> where - N: IntoPy>; + N: IntoPyObject<'py, Target = PyString>, + N::Error: Into; /// Returns an [`Ordering`] between `self` and `other`. /// @@ -194,7 +199,8 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed { /// ``` fn compare(&self, other: O) -> PyResult where - O: ToPyObject; + O: IntoPyObject<'py>, + O::Error: Into; /// Tests whether two Python objects obey a given [`CompareOp`]. /// @@ -232,7 +238,8 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed { /// ``` fn rich_compare(&self, other: O, compare_op: CompareOp) -> PyResult> where - O: ToPyObject; + O: IntoPyObject<'py>, + O::Error: Into; /// Computes the negative of self. /// @@ -257,114 +264,135 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed { /// This is equivalent to the Python expression `self < other`. fn lt(&self, other: O) -> PyResult where - O: ToPyObject; + O: IntoPyObject<'py>, + O::Error: Into; /// Tests whether this object is less than or equal to another. /// /// This is equivalent to the Python expression `self <= other`. fn le(&self, other: O) -> PyResult where - O: ToPyObject; + O: IntoPyObject<'py>, + O::Error: Into; /// Tests whether this object is equal to another. /// /// This is equivalent to the Python expression `self == other`. fn eq(&self, other: O) -> PyResult where - O: ToPyObject; + O: IntoPyObject<'py>, + O::Error: Into; /// Tests whether this object is not equal to another. /// /// This is equivalent to the Python expression `self != other`. fn ne(&self, other: O) -> PyResult where - O: ToPyObject; + O: IntoPyObject<'py>, + O::Error: Into; /// Tests whether this object is greater than another. /// /// This is equivalent to the Python expression `self > other`. fn gt(&self, other: O) -> PyResult where - O: ToPyObject; + O: IntoPyObject<'py>, + O::Error: Into; /// Tests whether this object is greater than or equal to another. /// /// This is equivalent to the Python expression `self >= other`. fn ge(&self, other: O) -> PyResult where - O: ToPyObject; + O: IntoPyObject<'py>, + O::Error: Into; /// Computes `self + other`. fn add(&self, other: O) -> PyResult> where - O: ToPyObject; + O: IntoPyObject<'py>, + O::Error: Into; /// Computes `self - other`. fn sub(&self, other: O) -> PyResult> where - O: ToPyObject; + O: IntoPyObject<'py>, + O::Error: Into; /// Computes `self * other`. fn mul(&self, other: O) -> PyResult> where - O: ToPyObject; + O: IntoPyObject<'py>, + O::Error: Into; /// Computes `self @ other`. fn matmul(&self, other: O) -> PyResult> where - O: ToPyObject; + O: IntoPyObject<'py>, + O::Error: Into; /// Computes `self / other`. fn div(&self, other: O) -> PyResult> where - O: ToPyObject; + O: IntoPyObject<'py>, + O::Error: Into; /// Computes `self // other`. fn floor_div(&self, other: O) -> PyResult> where - O: ToPyObject; + O: IntoPyObject<'py>, + O::Error: Into; /// Computes `self % other`. fn rem(&self, other: O) -> PyResult> where - O: ToPyObject; + O: IntoPyObject<'py>, + O::Error: Into; /// Computes `divmod(self, other)`. fn divmod(&self, other: O) -> PyResult> where - O: ToPyObject; + O: IntoPyObject<'py>, + O::Error: Into; /// Computes `self << other`. fn lshift(&self, other: O) -> PyResult> where - O: ToPyObject; + O: IntoPyObject<'py>, + O::Error: Into; /// Computes `self >> other`. fn rshift(&self, other: O) -> PyResult> where - O: ToPyObject; + O: IntoPyObject<'py>, + O::Error: Into; /// Computes `self ** other % modulus` (`pow(self, other, modulus)`). /// `py.None()` may be passed for the `modulus`. fn pow(&self, other: O1, modulus: O2) -> PyResult> where - O1: ToPyObject, - O2: ToPyObject; + O1: IntoPyObject<'py>, + O2: IntoPyObject<'py>, + O1::Error: Into, + O2::Error: Into; /// Computes `self & other`. fn bitand(&self, other: O) -> PyResult> where - O: ToPyObject; + O: IntoPyObject<'py>, + O::Error: Into; /// Computes `self | other`. fn bitor(&self, other: O) -> PyResult> where - O: ToPyObject; + O: IntoPyObject<'py>, + O::Error: Into; /// Computes `self ^ other`. fn bitxor(&self, other: O) -> PyResult> where - O: ToPyObject; + O: IntoPyObject<'py>, + O::Error: Into; /// Determines whether this object appears callable. /// @@ -427,11 +455,10 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed { /// }) /// # } /// ``` - fn call( - &self, - args: impl IntoPy>, - kwargs: Option<&Bound<'_, PyDict>>, - ) -> PyResult>; + fn call
(&self, args: A, kwargs: Option<&Bound<'_, PyDict>>) -> PyResult> + where + A: IntoPyObject<'py, Target = PyTuple>, + A::Error: Into; /// Calls the object without arguments. /// @@ -484,7 +511,10 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed { /// }) /// # } /// ``` - fn call1(&self, args: impl IntoPy>) -> PyResult>; + fn call1(&self, args: A) -> PyResult> + where + A: IntoPyObject<'py, Target = PyTuple>, + A::Error: Into; /// Calls a method on the object. /// @@ -530,8 +560,10 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed { kwargs: Option<&Bound<'_, PyDict>>, ) -> PyResult> where - N: IntoPy>, - A: IntoPy>; + N: IntoPyObject<'py, Target = PyString>, + A: IntoPyObject<'py, Target = PyTuple>, + N::Error: Into, + A::Error: Into; /// Calls a method on the object without arguments. /// @@ -568,7 +600,8 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed { /// ``` fn call_method0(&self, name: N) -> PyResult> where - N: IntoPy>; + N: IntoPyObject<'py, Target = PyString>, + N::Error: Into; /// Calls a method on the object with only positional arguments. /// @@ -606,8 +639,10 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed { /// ``` fn call_method1(&self, name: N, args: A) -> PyResult> where - N: IntoPy>, - A: IntoPy>; + N: IntoPyObject<'py, Target = PyString>, + A: IntoPyObject<'py, Target = PyTuple>, + N::Error: Into, + A::Error: Into; /// Returns whether the object is considered to be true. /// @@ -635,22 +670,26 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed { /// This is equivalent to the Python expression `self[key]`. fn get_item(&self, key: K) -> PyResult> where - K: ToPyObject; + K: IntoPyObject<'py>, + K::Error: Into; /// Sets a collection item value. /// /// This is equivalent to the Python expression `self[key] = value`. fn set_item(&self, key: K, value: V) -> PyResult<()> where - K: ToPyObject, - V: ToPyObject; + K: IntoPyObject<'py>, + V: IntoPyObject<'py>, + K::Error: Into, + V::Error: Into; /// Deletes an item from the collection. /// /// This is equivalent to the Python expression `del self[key]`. fn del_item(&self, key: K) -> PyResult<()> where - K: ToPyObject; + K: IntoPyObject<'py>, + K::Error: Into; /// Takes an object and returns an iterator for it. /// @@ -862,7 +901,8 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed { /// This is equivalent to the Python expression `value in self`. fn contains(&self, value: V) -> PyResult where - V: ToPyObject; + V: IntoPyObject<'py>, + V::Error: Into; /// Return a proxy object that delegates method calls to a parent or sibling class of type. /// @@ -876,17 +916,25 @@ macro_rules! implement_binop { #[doc = concat!("Computes `self ", $op, " other`.")] fn $name(&self, other: O) -> PyResult> where - O: ToPyObject, + O: IntoPyObject<'py>, + O::Error: Into, { fn inner<'py>( any: &Bound<'py, PyAny>, - other: Bound<'_, PyAny>, + other: &Bound<'_, PyAny>, ) -> PyResult> { unsafe { ffi::$c_api(any.as_ptr(), other.as_ptr()).assume_owned_or_err(any.py()) } } let py = self.py(); - inner(self, other.to_object(py).into_bound(py)) + inner( + self, + &other + .into_pyobject(py) + .map_err(Into::into)? + .into_any() + .as_borrowed(), + ) } }; } @@ -899,7 +947,8 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { fn hasattr(&self, attr_name: N) -> PyResult where - N: IntoPy>, + N: IntoPyObject<'py, Target = PyString>, + N::Error: Into, { // PyObject_HasAttr suppresses all exceptions, which was the behaviour of `hasattr` in Python 2. // Use an implementation which suppresses only AttributeError, which is consistent with `hasattr` in Python 3. @@ -911,16 +960,17 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { } } - inner(self.py(), self.getattr(attr_name)) + inner(self.py(), self.getattr(attr_name).map_err(Into::into)) } fn getattr(&self, attr_name: N) -> PyResult> where - N: IntoPy>, + N: IntoPyObject<'py, Target = PyString>, + N::Error: Into, { fn inner<'py>( any: &Bound<'py, PyAny>, - attr_name: Bound<'_, PyString>, + attr_name: &Bound<'_, PyString>, ) -> PyResult> { unsafe { ffi::PyObject_GetAttr(any.as_ptr(), attr_name.as_ptr()) @@ -928,19 +978,26 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { } } - let py = self.py(); - inner(self, attr_name.into_py(self.py()).into_bound(py)) + inner( + self, + &attr_name + .into_pyobject(self.py()) + .map_err(Into::into)? + .as_borrowed(), + ) } fn setattr(&self, attr_name: N, value: V) -> PyResult<()> where - N: IntoPy>, - V: ToPyObject, + N: IntoPyObject<'py, Target = PyString>, + V: IntoPyObject<'py>, + N::Error: Into, + V::Error: Into, { fn inner( any: &Bound<'_, PyAny>, - attr_name: Bound<'_, PyString>, - value: Bound<'_, PyAny>, + attr_name: &Bound<'_, PyString>, + value: &Bound<'_, PyAny>, ) -> PyResult<()> { err::error_on_minusone(any.py(), unsafe { ffi::PyObject_SetAttr(any.as_ptr(), attr_name.as_ptr(), value.as_ptr()) @@ -950,30 +1007,45 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { let py = self.py(); inner( self, - attr_name.into_py(py).into_bound(py), - value.to_object(py).into_bound(py), + &attr_name + .into_pyobject(py) + .map_err(Into::into)? + .as_borrowed(), + &value + .into_pyobject(py) + .map_err(Into::into)? + .into_any() + .as_borrowed(), ) } fn delattr(&self, attr_name: N) -> PyResult<()> where - N: IntoPy>, + N: IntoPyObject<'py, Target = PyString>, + N::Error: Into, { - fn inner(any: &Bound<'_, PyAny>, attr_name: Bound<'_, PyString>) -> PyResult<()> { + fn inner(any: &Bound<'_, PyAny>, attr_name: &Bound<'_, PyString>) -> PyResult<()> { err::error_on_minusone(any.py(), unsafe { ffi::PyObject_DelAttr(any.as_ptr(), attr_name.as_ptr()) }) } let py = self.py(); - inner(self, attr_name.into_py(py).into_bound(py)) + inner( + self, + &attr_name + .into_pyobject(py) + .map_err(Into::into)? + .as_borrowed(), + ) } fn compare(&self, other: O) -> PyResult where - O: ToPyObject, + O: IntoPyObject<'py>, + O::Error: Into, { - fn inner(any: &Bound<'_, PyAny>, other: Bound<'_, PyAny>) -> PyResult { + fn inner(any: &Bound<'_, PyAny>, other: &Bound<'_, PyAny>) -> PyResult { let other = other.as_ptr(); // Almost the same as ffi::PyObject_RichCompareBool, but this one doesn't try self == other. // See https://github.com/PyO3/pyo3/issues/985 for more. @@ -996,16 +1068,24 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { } let py = self.py(); - inner(self, other.to_object(py).into_bound(py)) + inner( + self, + &other + .into_pyobject(py) + .map_err(Into::into)? + .into_any() + .as_borrowed(), + ) } fn rich_compare(&self, other: O, compare_op: CompareOp) -> PyResult> where - O: ToPyObject, + O: IntoPyObject<'py>, + O::Error: Into, { fn inner<'py>( any: &Bound<'py, PyAny>, - other: Bound<'_, PyAny>, + other: &Bound<'_, PyAny>, compare_op: CompareOp, ) -> PyResult> { unsafe { @@ -1015,7 +1095,15 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { } let py = self.py(); - inner(self, other.to_object(py).into_bound(py), compare_op) + inner( + self, + &other + .into_pyobject(py) + .map_err(Into::into)? + .into_any() + .as_borrowed(), + compare_op, + ) } fn neg(&self) -> PyResult> { @@ -1048,7 +1136,8 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { fn lt(&self, other: O) -> PyResult where - O: ToPyObject, + O: IntoPyObject<'py>, + O::Error: Into, { self.rich_compare(other, CompareOp::Lt) .and_then(|any| any.is_truthy()) @@ -1056,7 +1145,8 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { fn le(&self, other: O) -> PyResult where - O: ToPyObject, + O: IntoPyObject<'py>, + O::Error: Into, { self.rich_compare(other, CompareOp::Le) .and_then(|any| any.is_truthy()) @@ -1064,7 +1154,8 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { fn eq(&self, other: O) -> PyResult where - O: ToPyObject, + O: IntoPyObject<'py>, + O::Error: Into, { self.rich_compare(other, CompareOp::Eq) .and_then(|any| any.is_truthy()) @@ -1072,7 +1163,8 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { fn ne(&self, other: O) -> PyResult where - O: ToPyObject, + O: IntoPyObject<'py>, + O::Error: Into, { self.rich_compare(other, CompareOp::Ne) .and_then(|any| any.is_truthy()) @@ -1080,7 +1172,8 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { fn gt(&self, other: O) -> PyResult where - O: ToPyObject, + O: IntoPyObject<'py>, + O::Error: Into, { self.rich_compare(other, CompareOp::Gt) .and_then(|any| any.is_truthy()) @@ -1088,7 +1181,8 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { fn ge(&self, other: O) -> PyResult where - O: ToPyObject, + O: IntoPyObject<'py>, + O::Error: Into, { self.rich_compare(other, CompareOp::Ge) .and_then(|any| any.is_truthy()) @@ -1110,11 +1204,12 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { /// Computes `divmod(self, other)`. fn divmod(&self, other: O) -> PyResult> where - O: ToPyObject, + O: IntoPyObject<'py>, + O::Error: Into, { fn inner<'py>( any: &Bound<'py, PyAny>, - other: Bound<'_, PyAny>, + other: &Bound<'_, PyAny>, ) -> PyResult> { unsafe { ffi::PyNumber_Divmod(any.as_ptr(), other.as_ptr()).assume_owned_or_err(any.py()) @@ -1122,20 +1217,29 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { } let py = self.py(); - inner(self, other.to_object(py).into_bound(py)) + inner( + self, + &other + .into_pyobject(py) + .map_err(Into::into)? + .into_any() + .as_borrowed(), + ) } /// Computes `self ** other % modulus` (`pow(self, other, modulus)`). /// `py.None()` may be passed for the `modulus`. fn pow(&self, other: O1, modulus: O2) -> PyResult> where - O1: ToPyObject, - O2: ToPyObject, + O1: IntoPyObject<'py>, + O2: IntoPyObject<'py>, + O1::Error: Into, + O2::Error: Into, { fn inner<'py>( any: &Bound<'py, PyAny>, - other: Bound<'_, PyAny>, - modulus: Bound<'_, PyAny>, + other: &Bound<'_, PyAny>, + modulus: &Bound<'_, PyAny>, ) -> PyResult> { unsafe { ffi::PyNumber_Power(any.as_ptr(), other.as_ptr(), modulus.as_ptr()) @@ -1146,8 +1250,16 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { let py = self.py(); inner( self, - other.to_object(py).into_bound(py), - modulus.to_object(py).into_bound(py), + &other + .into_pyobject(py) + .map_err(Into::into)? + .into_any() + .as_borrowed(), + &modulus + .into_pyobject(py) + .map_err(Into::into)? + .into_any() + .as_borrowed(), ) } @@ -1155,14 +1267,14 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { unsafe { ffi::PyCallable_Check(self.as_ptr()) != 0 } } - fn call( - &self, - args: impl IntoPy>, - kwargs: Option<&Bound<'_, PyDict>>, - ) -> PyResult> { + fn call(&self, args: A, kwargs: Option<&Bound<'_, PyDict>>) -> PyResult> + where + A: IntoPyObject<'py, Target = PyTuple>, + A::Error: Into, + { fn inner<'py>( any: &Bound<'py, PyAny>, - args: Bound<'_, PyTuple>, + args: &Bound<'_, PyTuple>, kwargs: Option<&Bound<'_, PyDict>>, ) -> PyResult> { unsafe { @@ -1176,14 +1288,22 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { } let py = self.py(); - inner(self, args.into_py(py).into_bound(py), kwargs) + inner( + self, + &args.into_pyobject(py).map_err(Into::into)?.as_borrowed(), + kwargs, + ) } fn call0(&self) -> PyResult> { unsafe { ffi::compat::PyObject_CallNoArgs(self.as_ptr()).assume_owned_or_err(self.py()) } } - fn call1(&self, args: impl IntoPy>) -> PyResult> { + fn call1(&self, args: A) -> PyResult> + where + A: IntoPyObject<'py, Target = PyTuple>, + A::Error: Into, + { self.call(args, None) } @@ -1194,8 +1314,10 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { kwargs: Option<&Bound<'_, PyDict>>, ) -> PyResult> where - N: IntoPy>, - A: IntoPy>, + N: IntoPyObject<'py, Target = PyString>, + A: IntoPyObject<'py, Target = PyTuple>, + N::Error: Into, + A::Error: Into, { self.getattr(name) .and_then(|method| method.call(args, kwargs)) @@ -1203,10 +1325,11 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { fn call_method0(&self, name: N) -> PyResult> where - N: IntoPy>, + N: IntoPyObject<'py, Target = PyString>, + N::Error: Into, { let py = self.py(); - let name = name.into_py(py).into_bound(py); + let name = name.into_pyobject(py).map_err(Into::into)?.into_bound(); unsafe { ffi::compat::PyObject_CallMethodNoArgs(self.as_ptr(), name.as_ptr()) .assume_owned_or_err(py) @@ -1215,8 +1338,10 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { fn call_method1(&self, name: N, args: A) -> PyResult> where - N: IntoPy>, - A: IntoPy>, + N: IntoPyObject<'py, Target = PyString>, + A: IntoPyObject<'py, Target = PyTuple>, + N::Error: Into, + A::Error: Into, { self.call_method(name, args, None) } @@ -1242,11 +1367,12 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { fn get_item(&self, key: K) -> PyResult> where - K: ToPyObject, + K: IntoPyObject<'py>, + K::Error: Into, { fn inner<'py>( any: &Bound<'py, PyAny>, - key: Bound<'_, PyAny>, + key: &Bound<'_, PyAny>, ) -> PyResult> { unsafe { ffi::PyObject_GetItem(any.as_ptr(), key.as_ptr()).assume_owned_or_err(any.py()) @@ -1254,18 +1380,26 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { } let py = self.py(); - inner(self, key.to_object(py).into_bound(py)) + inner( + self, + &key.into_pyobject(py) + .map_err(Into::into)? + .into_any() + .as_borrowed(), + ) } fn set_item(&self, key: K, value: V) -> PyResult<()> where - K: ToPyObject, - V: ToPyObject, + K: IntoPyObject<'py>, + V: IntoPyObject<'py>, + K::Error: Into, + V::Error: Into, { fn inner( any: &Bound<'_, PyAny>, - key: Bound<'_, PyAny>, - value: Bound<'_, PyAny>, + key: &Bound<'_, PyAny>, + value: &Bound<'_, PyAny>, ) -> PyResult<()> { err::error_on_minusone(any.py(), unsafe { ffi::PyObject_SetItem(any.as_ptr(), key.as_ptr(), value.as_ptr()) @@ -1275,23 +1409,37 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { let py = self.py(); inner( self, - key.to_object(py).into_bound(py), - value.to_object(py).into_bound(py), + &key.into_pyobject(py) + .map_err(Into::into)? + .into_any() + .as_borrowed(), + &value + .into_pyobject(py) + .map_err(Into::into)? + .into_any() + .as_borrowed(), ) } fn del_item(&self, key: K) -> PyResult<()> where - K: ToPyObject, + K: IntoPyObject<'py>, + K::Error: Into, { - fn inner(any: &Bound<'_, PyAny>, key: Bound<'_, PyAny>) -> PyResult<()> { + fn inner(any: &Bound<'_, PyAny>, key: &Bound<'_, PyAny>) -> PyResult<()> { err::error_on_minusone(any.py(), unsafe { ffi::PyObject_DelItem(any.as_ptr(), key.as_ptr()) }) } let py = self.py(); - inner(self, key.to_object(py).into_bound(py)) + inner( + self, + &key.into_pyobject(py) + .map_err(Into::into)? + .into_any() + .as_borrowed(), + ) } fn iter(&self) -> PyResult> { @@ -1440,9 +1588,10 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { fn contains(&self, value: V) -> PyResult where - V: ToPyObject, + V: IntoPyObject<'py>, + V::Error: Into, { - fn inner(any: &Bound<'_, PyAny>, value: Bound<'_, PyAny>) -> PyResult { + fn inner(any: &Bound<'_, PyAny>, value: &Bound<'_, PyAny>) -> PyResult { match unsafe { ffi::PySequence_Contains(any.as_ptr(), value.as_ptr()) } { 0 => Ok(false), 1 => Ok(true), @@ -1451,7 +1600,14 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { } let py = self.py(); - inner(self, value.to_object(py).into_bound(py)) + inner( + self, + &value + .into_pyobject(py) + .map_err(Into::into)? + .into_any() + .as_borrowed(), + ) } #[cfg(not(any(PyPy, GraalPy)))] @@ -1474,7 +1630,8 @@ impl<'py> Bound<'py, PyAny> { #[allow(dead_code)] // Currently only used with num-complex+abi3, so dead without that. pub(crate) fn lookup_special(&self, attr_name: N) -> PyResult>> where - N: IntoPy>, + N: IntoPyObject<'py, Target = PyString>, + N::Error: Into, { let py = self.py(); let self_type = self.get_type(); diff --git a/src/types/bytes.rs b/src/types/bytes.rs index 397e2eb3848..77b1d2b735d 100644 --- a/src/types/bytes.rs +++ b/src/types/bytes.rs @@ -38,10 +38,10 @@ use std::str; /// let other = PyBytes::new(py, b"foo".as_slice()); /// assert!(py_bytes.as_any().eq(other).unwrap()); /// -/// // Note that `eq` will convert it's argument to Python using `ToPyObject`, -/// // so the following does not compare equal since the slice will convert into a -/// // `list`, not a `bytes` object. -/// assert!(!py_bytes.as_any().eq(b"foo".as_slice()).unwrap()); +/// // Note that `eq` will convert its argument to Python using `IntoPyObject`. +/// // Byte collections are specialized, so that the following slice will indeed +/// // convert into a `bytes` object and not a `list`: +/// assert!(py_bytes.as_any().eq(b"foo".as_slice()).unwrap()); /// # }); /// ``` #[repr(transparent)] diff --git a/src/types/datetime.rs b/src/types/datetime.rs index b2463bedde0..77df943fd81 100644 --- a/src/types/datetime.rs +++ b/src/types/datetime.rs @@ -862,11 +862,13 @@ mod tests { #[cfg(all(feature = "macros", feature = "chrono"))] #[cfg_attr(target_arch = "wasm32", ignore)] // DateTime import fails on wasm for mysterious reasons fn test_timezone_from_offset() { + use crate::types::PyNone; + Python::with_gil(|py| { assert!( timezone_from_offset(&PyDelta::new(py, 0, -3600, 0, true).unwrap()) .unwrap() - .call_method1("utcoffset", ((),)) + .call_method1("utcoffset", (PyNone::get(py),)) .unwrap() .downcast_into::() .unwrap() @@ -877,7 +879,7 @@ mod tests { assert!( timezone_from_offset(&PyDelta::new(py, 0, 3600, 0, true).unwrap()) .unwrap() - .call_method1("utcoffset", ((),)) + .call_method1("utcoffset", (PyNone::get(py),)) .unwrap() .downcast_into::() .unwrap() diff --git a/src/types/mapping.rs b/src/types/mapping.rs index 4cacc04f184..609bd80295a 100644 --- a/src/types/mapping.rs +++ b/src/types/mapping.rs @@ -1,3 +1,4 @@ +use crate::conversion::IntoPyObject; use crate::err::PyResult; use crate::ffi_ptr_ext::FfiPtrExt; use crate::instance::Bound; @@ -6,7 +7,7 @@ use crate::sync::GILOnceCell; use crate::type_object::PyTypeInfo; use crate::types::any::PyAnyMethods; use crate::types::{PyAny, PyDict, PySequence, PyType}; -use crate::{ffi, Py, PyTypeCheck, Python, ToPyObject}; +use crate::{ffi, Py, PyErr, PyTypeCheck, Python}; /// Represents a reference to a Python object supporting the mapping protocol. /// @@ -50,7 +51,8 @@ pub trait PyMappingMethods<'py>: crate::sealed::Sealed { /// This is equivalent to the Python expression `key in self`. fn contains(&self, key: K) -> PyResult where - K: ToPyObject; + K: IntoPyObject<'py>, + K::Error: Into; /// Gets the item in self with key `key`. /// @@ -59,22 +61,26 @@ pub trait PyMappingMethods<'py>: crate::sealed::Sealed { /// This is equivalent to the Python expression `self[key]`. fn get_item(&self, key: K) -> PyResult> where - K: ToPyObject; + K: IntoPyObject<'py>, + K::Error: Into; /// Sets the item in self with key `key`. /// /// This is equivalent to the Python expression `self[key] = value`. fn set_item(&self, key: K, value: V) -> PyResult<()> where - K: ToPyObject, - V: ToPyObject; + K: IntoPyObject<'py>, + V: IntoPyObject<'py>, + K::Error: Into, + V::Error: Into; /// Deletes the item with key `key`. /// /// This is equivalent to the Python statement `del self[key]`. fn del_item(&self, key: K) -> PyResult<()> where - K: ToPyObject; + K: IntoPyObject<'py>, + K::Error: Into; /// Returns a sequence containing all keys in the mapping. fn keys(&self) -> PyResult>; @@ -101,7 +107,8 @@ impl<'py> PyMappingMethods<'py> for Bound<'py, PyMapping> { fn contains(&self, key: K) -> PyResult where - K: ToPyObject, + K: IntoPyObject<'py>, + K::Error: Into, { PyAnyMethods::contains(&**self, key) } @@ -109,7 +116,8 @@ impl<'py> PyMappingMethods<'py> for Bound<'py, PyMapping> { #[inline] fn get_item(&self, key: K) -> PyResult> where - K: ToPyObject, + K: IntoPyObject<'py>, + K::Error: Into, { PyAnyMethods::get_item(&**self, key) } @@ -117,8 +125,10 @@ impl<'py> PyMappingMethods<'py> for Bound<'py, PyMapping> { #[inline] fn set_item(&self, key: K, value: V) -> PyResult<()> where - K: ToPyObject, - V: ToPyObject, + K: IntoPyObject<'py>, + V: IntoPyObject<'py>, + K::Error: Into, + V::Error: Into, { PyAnyMethods::set_item(&**self, key, value) } @@ -126,7 +136,8 @@ impl<'py> PyMappingMethods<'py> for Bound<'py, PyMapping> { #[inline] fn del_item(&self, key: K) -> PyResult<()> where - K: ToPyObject, + K: IntoPyObject<'py>, + K::Error: Into, { PyAnyMethods::del_item(&**self, key) } From 84469377231b386cb1e29bb0a58154b548c572d0 Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Sat, 24 Aug 2024 21:27:27 +0200 Subject: [PATCH 242/495] reintroduce `PyBuffer::get` (#4486) --- pytests/src/buf_and_str.rs | 2 +- src/buffer.rs | 19 +++++++++++++------ tests/test_buffer.rs | 12 ++++++------ tests/test_buffer_protocol.rs | 2 +- 4 files changed, 21 insertions(+), 14 deletions(-) diff --git a/pytests/src/buf_and_str.rs b/pytests/src/buf_and_str.rs index f9b55efb74f..bbaad40f312 100644 --- a/pytests/src/buf_and_str.rs +++ b/pytests/src/buf_and_str.rs @@ -36,7 +36,7 @@ impl BytesExtractor { #[staticmethod] pub fn from_buffer(buf: &Bound<'_, PyAny>) -> PyResult { - let buf = PyBuffer::::get_bound(buf)?; + let buf = PyBuffer::::get(buf)?; Ok(buf.item_count()) } } diff --git a/src/buffer.rs b/src/buffer.rs index 2a2e7659435..ad070e13e05 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -184,13 +184,13 @@ pub unsafe trait Element: Copy { impl<'py, T: Element> FromPyObject<'py> for PyBuffer { fn extract_bound(obj: &Bound<'_, PyAny>) -> PyResult> { - Self::get_bound(obj) + Self::get(obj) } } impl PyBuffer { /// Gets the underlying buffer from the specified python object. - pub fn get_bound(obj: &Bound<'_, PyAny>) -> PyResult> { + pub fn get(obj: &Bound<'_, PyAny>) -> PyResult> { // TODO: use nightly API Box::new_uninit() once stable let mut buf = Box::new(mem::MaybeUninit::uninit()); let buf: Box = { @@ -224,6 +224,13 @@ impl PyBuffer { } } + /// Deprecated name for [`PyBuffer::get`]. + #[deprecated(since = "0.23.0", note = "renamed to `PyBuffer::get`")] + #[inline] + pub fn get_bound(obj: &Bound<'_, PyAny>) -> PyResult> { + Self::get(obj) + } + /// Gets the pointer to the start of the buffer memory. /// /// Warning: the buffer memory might be mutated by other Python functions, @@ -686,7 +693,7 @@ mod tests { fn test_debug() { Python::with_gil(|py| { let bytes = py.eval(ffi::c_str!("b'abcde'"), None, None).unwrap(); - let buffer: PyBuffer = PyBuffer::get_bound(&bytes).unwrap(); + let buffer: PyBuffer = PyBuffer::get(&bytes).unwrap(); let expected = format!( concat!( "PyBuffer {{ buf: {:?}, obj: {:?}, ", @@ -848,7 +855,7 @@ mod tests { fn test_bytes_buffer() { Python::with_gil(|py| { let bytes = py.eval(ffi::c_str!("b'abcde'"), None, None).unwrap(); - let buffer = PyBuffer::get_bound(&bytes).unwrap(); + let buffer = PyBuffer::get(&bytes).unwrap(); assert_eq!(buffer.dimensions(), 1); assert_eq!(buffer.item_count(), 5); assert_eq!(buffer.format().to_str().unwrap(), "B"); @@ -884,7 +891,7 @@ mod tests { .unwrap() .call_method("array", ("f", (1.0, 1.5, 2.0, 2.5)), None) .unwrap(); - let buffer = PyBuffer::get_bound(&array).unwrap(); + let buffer = PyBuffer::get(&array).unwrap(); assert_eq!(buffer.dimensions(), 1); assert_eq!(buffer.item_count(), 4); assert_eq!(buffer.format().to_str().unwrap(), "f"); @@ -914,7 +921,7 @@ mod tests { assert_eq!(buffer.to_vec(py).unwrap(), [10.0, 11.0, 12.0, 13.0]); // F-contiguous fns - let buffer = PyBuffer::get_bound(&array).unwrap(); + let buffer = PyBuffer::get(&array).unwrap(); let slice = buffer.as_fortran_slice(py).unwrap(); assert_eq!(slice.len(), 4); assert_eq!(slice[1].get(), 11.0); diff --git a/tests/test_buffer.rs b/tests/test_buffer.rs index 9e2a6a4d1c5..60db80b81c8 100644 --- a/tests/test_buffer.rs +++ b/tests/test_buffer.rs @@ -95,11 +95,11 @@ fn test_get_buffer_errors() { ) .unwrap(); - assert!(PyBuffer::::get_bound(instance.bind(py)).is_ok()); + assert!(PyBuffer::::get(instance.bind(py)).is_ok()); instance.borrow_mut(py).error = Some(TestGetBufferError::NullShape); assert_eq!( - PyBuffer::::get_bound(instance.bind(py)) + PyBuffer::::get(instance.bind(py)) .unwrap_err() .to_string(), "BufferError: shape is null" @@ -107,7 +107,7 @@ fn test_get_buffer_errors() { instance.borrow_mut(py).error = Some(TestGetBufferError::NullStrides); assert_eq!( - PyBuffer::::get_bound(instance.bind(py)) + PyBuffer::::get(instance.bind(py)) .unwrap_err() .to_string(), "BufferError: strides is null" @@ -115,7 +115,7 @@ fn test_get_buffer_errors() { instance.borrow_mut(py).error = Some(TestGetBufferError::IncorrectItemSize); assert_eq!( - PyBuffer::::get_bound(instance.bind(py)) + PyBuffer::::get(instance.bind(py)) .unwrap_err() .to_string(), "BufferError: buffer contents are not compatible with u32" @@ -123,7 +123,7 @@ fn test_get_buffer_errors() { instance.borrow_mut(py).error = Some(TestGetBufferError::IncorrectFormat); assert_eq!( - PyBuffer::::get_bound(instance.bind(py)) + PyBuffer::::get(instance.bind(py)) .unwrap_err() .to_string(), "BufferError: buffer contents are not compatible with u32" @@ -131,7 +131,7 @@ fn test_get_buffer_errors() { instance.borrow_mut(py).error = Some(TestGetBufferError::IncorrectAlignment); assert_eq!( - PyBuffer::::get_bound(instance.bind(py)) + PyBuffer::::get(instance.bind(py)) .unwrap_err() .to_string(), "BufferError: buffer contents are insufficiently aligned for u32" diff --git a/tests/test_buffer_protocol.rs b/tests/test_buffer_protocol.rs index 4d3efec3e4f..75549f39718 100644 --- a/tests/test_buffer_protocol.rs +++ b/tests/test_buffer_protocol.rs @@ -77,7 +77,7 @@ fn test_buffer_referenced() { } .into_py(py); - let buf = PyBuffer::::get_bound(instance.bind(py)).unwrap(); + let buf = PyBuffer::::get(instance.bind(py)).unwrap(); assert_eq!(buf.to_vec(py).unwrap(), input); drop(instance); buf From 2e891d00216cf610b60124b6e7d7bbdd2708d1ca Mon Sep 17 00:00:00 2001 From: Chayim Refael Friedman Date: Sun, 25 Aug 2024 09:00:00 +0300 Subject: [PATCH 243/495] Use vectorcall (where possible) when calling Python functions (#4456) * Use vectorcall (where possible) when calling Python functions This works without any changes to user code. The way it works is by creating a methods on `IntoPy` to call functions, and specializing them for tuples. This currently supports only non-kwargs for methods, and kwargs with somewhat slow approach (converting from PyDict) for functions. This can be improved, but that will require additional API. We may consider adding more impls IntoPy> that specialize (for example, for arrays and `Vec`), but this i a good start. * Add vectorcall benchmarks * Fix Clippy (elide a lifetime) --------- Co-authored-by: David Hewitt --- newsfragments/4456.changed.md | 1 + pyo3-benches/benches/bench_call.rs | 146 ++++++++++++++++++++++++++++- pyo3-ffi/src/cpython/abstract_.rs | 2 +- src/conversion.rs | 136 ++++++++++++++++++++++++++- src/conversions/chrono.rs | 9 +- src/instance.rs | 14 +-- src/types/any.rs | 80 +++++++--------- src/types/tuple.rs | 113 +++++++++++++++++++++- 8 files changed, 439 insertions(+), 62 deletions(-) create mode 100644 newsfragments/4456.changed.md diff --git a/newsfragments/4456.changed.md b/newsfragments/4456.changed.md new file mode 100644 index 00000000000..094dece12a5 --- /dev/null +++ b/newsfragments/4456.changed.md @@ -0,0 +1 @@ +Improve performance of calls to Python by using the vectorcall calling convention where possible. \ No newline at end of file diff --git a/pyo3-benches/benches/bench_call.rs b/pyo3-benches/benches/bench_call.rs index ca18bbd5e60..ca8c17093af 100644 --- a/pyo3-benches/benches/bench_call.rs +++ b/pyo3-benches/benches/bench_call.rs @@ -2,8 +2,9 @@ use std::hint::black_box; use codspeed_criterion_compat::{criterion_group, criterion_main, Bencher, Criterion}; -use pyo3::prelude::*; use pyo3::ffi::c_str; +use pyo3::prelude::*; +use pyo3::types::IntoPyDict; macro_rules! test_module { ($py:ident, $code:literal) => { @@ -26,6 +27,62 @@ fn bench_call_0(b: &mut Bencher<'_>) { }) } +fn bench_call_1(b: &mut Bencher<'_>) { + Python::with_gil(|py| { + let module = test_module!(py, "def foo(a, b, c): pass"); + + let foo_module = &module.getattr("foo").unwrap(); + let args = ( + <_ as IntoPy>::into_py(1, py).into_bound(py), + <_ as IntoPy>::into_py("s", py).into_bound(py), + <_ as IntoPy>::into_py(1.23, py).into_bound(py), + ); + + b.iter(|| { + for _ in 0..1000 { + black_box(foo_module).call1(args.clone()).unwrap(); + } + }); + }) +} + +fn bench_call(b: &mut Bencher<'_>) { + Python::with_gil(|py| { + let module = test_module!(py, "def foo(a, b, c, d, e): pass"); + + let foo_module = &module.getattr("foo").unwrap(); + let args = ( + <_ as IntoPy>::into_py(1, py).into_bound(py), + <_ as IntoPy>::into_py("s", py).into_bound(py), + <_ as IntoPy>::into_py(1.23, py).into_bound(py), + ); + let kwargs = [("d", 1), ("e", 42)].into_py_dict(py); + + b.iter(|| { + for _ in 0..1000 { + black_box(foo_module) + .call(args.clone(), Some(&kwargs)) + .unwrap(); + } + }); + }) +} + +fn bench_call_one_arg(b: &mut Bencher<'_>) { + Python::with_gil(|py| { + let module = test_module!(py, "def foo(a): pass"); + + let foo_module = &module.getattr("foo").unwrap(); + let arg = <_ as IntoPy>::into_py(1, py).into_bound(py); + + b.iter(|| { + for _ in 0..1000 { + black_box(foo_module).call1((arg.clone(),)).unwrap(); + } + }); + }) +} + fn bench_call_method_0(b: &mut Bencher<'_>) { Python::with_gil(|py| { let module = test_module!( @@ -47,9 +104,96 @@ class Foo: }) } +fn bench_call_method_1(b: &mut Bencher<'_>) { + Python::with_gil(|py| { + let module = test_module!( + py, + " +class Foo: + def foo(self, a, b, c): + pass +" + ); + + let foo_module = &module.getattr("Foo").unwrap().call0().unwrap(); + let args = ( + <_ as IntoPy>::into_py(1, py).into_bound(py), + <_ as IntoPy>::into_py("s", py).into_bound(py), + <_ as IntoPy>::into_py(1.23, py).into_bound(py), + ); + + b.iter(|| { + for _ in 0..1000 { + black_box(foo_module) + .call_method1("foo", args.clone()) + .unwrap(); + } + }); + }) +} + +fn bench_call_method(b: &mut Bencher<'_>) { + Python::with_gil(|py| { + let module = test_module!( + py, + " +class Foo: + def foo(self, a, b, c, d, e): + pass +" + ); + + let foo_module = &module.getattr("Foo").unwrap().call0().unwrap(); + let args = ( + <_ as IntoPy>::into_py(1, py).into_bound(py), + <_ as IntoPy>::into_py("s", py).into_bound(py), + <_ as IntoPy>::into_py(1.23, py).into_bound(py), + ); + let kwargs = [("d", 1), ("e", 42)].into_py_dict(py); + + b.iter(|| { + for _ in 0..1000 { + black_box(foo_module) + .call_method("foo", args.clone(), Some(&kwargs)) + .unwrap(); + } + }); + }) +} + +fn bench_call_method_one_arg(b: &mut Bencher<'_>) { + Python::with_gil(|py| { + let module = test_module!( + py, + " +class Foo: + def foo(self, a): + pass +" + ); + + let foo_module = &module.getattr("Foo").unwrap().call0().unwrap(); + let arg = <_ as IntoPy>::into_py(1, py).into_bound(py); + + b.iter(|| { + for _ in 0..1000 { + black_box(foo_module) + .call_method1("foo", (arg.clone(),)) + .unwrap(); + } + }); + }) +} + fn criterion_benchmark(c: &mut Criterion) { c.bench_function("call_0", bench_call_0); + c.bench_function("call_1", bench_call_1); + c.bench_function("call", bench_call); + c.bench_function("call_one_arg", bench_call_one_arg); c.bench_function("call_method_0", bench_call_method_0); + c.bench_function("call_method_1", bench_call_method_1); + c.bench_function("call_method", bench_call_method); + c.bench_function("call_method_one_arg", bench_call_method_one_arg); } criterion_group!(benches, criterion_benchmark); diff --git a/pyo3-ffi/src/cpython/abstract_.rs b/pyo3-ffi/src/cpython/abstract_.rs index 34525cec16f..83295e58f61 100644 --- a/pyo3-ffi/src/cpython/abstract_.rs +++ b/pyo3-ffi/src/cpython/abstract_.rs @@ -40,7 +40,7 @@ extern "C" { } #[cfg(Py_3_8)] -const PY_VECTORCALL_ARGUMENTS_OFFSET: size_t = +pub const PY_VECTORCALL_ARGUMENTS_OFFSET: size_t = 1 << (8 * std::mem::size_of::() as size_t - 1); #[cfg(Py_3_8)] diff --git a/src/conversion.rs b/src/conversion.rs index d909382bdb9..991f3c56bc1 100644 --- a/src/conversion.rs +++ b/src/conversion.rs @@ -1,10 +1,11 @@ //! Defines conversions between Rust and Python types. use crate::err::PyResult; +use crate::ffi_ptr_ext::FfiPtrExt; #[cfg(feature = "experimental-inspect")] use crate::inspect::types::TypeInfo; use crate::pyclass::boolean_struct::False; use crate::types::any::PyAnyMethods; -use crate::types::PyTuple; +use crate::types::{PyDict, PyString, PyTuple}; use crate::{ ffi, Borrowed, Bound, BoundObject, Py, PyAny, PyClass, PyErr, PyObject, PyRef, PyRefMut, Python, }; @@ -172,6 +173,93 @@ pub trait IntoPy: Sized { fn type_output() -> TypeInfo { TypeInfo::Any } + + // The following methods are helpers to use the vectorcall API where possible. + // They are overridden on tuples to perform a vectorcall. + // Be careful when you're implementing these: they can never refer to `Bound` call methods, + // as those refer to these methods, so this will create an infinite recursion. + #[doc(hidden)] + #[inline] + fn __py_call_vectorcall1<'py>( + self, + py: Python<'py>, + function: Borrowed<'_, 'py, PyAny>, + _: private::Token, + ) -> PyResult> + where + Self: IntoPy>, + { + #[inline] + fn inner<'py>( + py: Python<'py>, + function: Borrowed<'_, 'py, PyAny>, + args: Bound<'py, PyTuple>, + ) -> PyResult> { + unsafe { + ffi::PyObject_Call(function.as_ptr(), args.as_ptr(), std::ptr::null_mut()) + .assume_owned_or_err(py) + } + } + inner( + py, + function, + >>::into_py(self, py).into_bound(py), + ) + } + + #[doc(hidden)] + #[inline] + fn __py_call_vectorcall<'py>( + self, + py: Python<'py>, + function: Borrowed<'_, 'py, PyAny>, + kwargs: Option>, + _: private::Token, + ) -> PyResult> + where + Self: IntoPy>, + { + #[inline] + fn inner<'py>( + py: Python<'py>, + function: Borrowed<'_, 'py, PyAny>, + args: Bound<'py, PyTuple>, + kwargs: Option>, + ) -> PyResult> { + unsafe { + ffi::PyObject_Call( + function.as_ptr(), + args.as_ptr(), + kwargs.map_or_else(std::ptr::null_mut, |kwargs| kwargs.as_ptr()), + ) + .assume_owned_or_err(py) + } + } + inner( + py, + function, + >>::into_py(self, py).into_bound(py), + kwargs, + ) + } + + #[doc(hidden)] + #[inline] + fn __py_call_method_vectorcall1<'py>( + self, + _py: Python<'py>, + object: Borrowed<'_, 'py, PyAny>, + method_name: Borrowed<'_, 'py, PyString>, + _: private::Token, + ) -> PyResult> + where + Self: IntoPy>, + { + // Don't `self.into_py()`! This will lose the optimization of vectorcall. + object + .getattr(method_name) + .and_then(|method| method.call1(self)) + } } /// Defines a conversion from a Rust type to a Python object, which may fail. @@ -502,6 +590,52 @@ impl IntoPy> for () { fn into_py(self, py: Python<'_>) -> Py { PyTuple::empty(py).unbind() } + + #[inline] + fn __py_call_vectorcall1<'py>( + self, + py: Python<'py>, + function: Borrowed<'_, 'py, PyAny>, + _: private::Token, + ) -> PyResult> { + unsafe { ffi::compat::PyObject_CallNoArgs(function.as_ptr()).assume_owned_or_err(py) } + } + + #[inline] + fn __py_call_vectorcall<'py>( + self, + py: Python<'py>, + function: Borrowed<'_, 'py, PyAny>, + kwargs: Option>, + _: private::Token, + ) -> PyResult> { + unsafe { + match kwargs { + Some(kwargs) => ffi::PyObject_Call( + function.as_ptr(), + PyTuple::empty(py).as_ptr(), + kwargs.as_ptr(), + ) + .assume_owned_or_err(py), + None => ffi::compat::PyObject_CallNoArgs(function.as_ptr()).assume_owned_or_err(py), + } + } + } + + #[inline] + #[allow(clippy::used_underscore_binding)] + fn __py_call_method_vectorcall1<'py>( + self, + py: Python<'py>, + object: Borrowed<'_, 'py, PyAny>, + method_name: Borrowed<'_, 'py, PyString>, + _: private::Token, + ) -> PyResult> { + unsafe { + ffi::compat::PyObject_CallMethodNoArgs(object.as_ptr(), method_name.as_ptr()) + .assume_owned_or_err(py) + } + } } impl<'py> IntoPyObject<'py> for () { diff --git a/src/conversions/chrono.rs b/src/conversions/chrono.rs index 1fc3f5ba4b6..42126bcb950 100644 --- a/src/conversions/chrono.rs +++ b/src/conversions/chrono.rs @@ -813,7 +813,7 @@ fn timezone_utc(py: Python<'_>) -> Bound<'_, PyAny> { #[cfg(test)] mod tests { use super::*; - use crate::types::PyTuple; + use crate::{types::PyTuple, BoundObject}; use std::{cmp::Ordering, panic}; #[test] @@ -1333,7 +1333,12 @@ mod tests { .unwrap() .getattr(name) .unwrap() - .call1(args) + .call1( + args.into_pyobject(py) + .map_err(Into::into) + .unwrap() + .into_bound(), + ) .unwrap() } diff --git a/src/instance.rs b/src/instance.rs index ff35fe293a7..5e7b3173759 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -1477,8 +1477,7 @@ impl Py { kwargs: Option<&Bound<'py, PyDict>>, ) -> PyResult where - N: IntoPyObject<'py, Target = PyTuple>, - N::Error: Into, + N: IntoPy>, { self.bind(py).as_any().call(args, kwargs).map(Bound::unbind) } @@ -1486,10 +1485,9 @@ impl Py { /// Calls the object with only positional arguments. /// /// This is equivalent to the Python expression `self(*args)`. - pub fn call1<'py, N>(&self, py: Python<'py>, args: N) -> PyResult + pub fn call1(&self, py: Python<'_>, args: N) -> PyResult where - N: IntoPyObject<'py, Target = PyTuple>, - N::Error: Into, + N: IntoPy>, { self.bind(py).as_any().call1(args).map(Bound::unbind) } @@ -1516,9 +1514,8 @@ impl Py { ) -> PyResult where N: IntoPyObject<'py, Target = PyString>, - A: IntoPyObject<'py, Target = PyTuple>, + A: IntoPy>, N::Error: Into, - A::Error: Into, { self.bind(py) .as_any() @@ -1535,9 +1532,8 @@ impl Py { pub fn call_method1<'py, N, A>(&self, py: Python<'py>, name: N, args: A) -> PyResult where N: IntoPyObject<'py, Target = PyString>, - A: IntoPyObject<'py, Target = PyTuple>, + A: IntoPy>, N::Error: Into, - A::Error: Into, { self.bind(py) .as_any() diff --git a/src/types/any.rs b/src/types/any.rs index 2ad62111a68..801b651ac1a 100644 --- a/src/types/any.rs +++ b/src/types/any.rs @@ -1,5 +1,5 @@ use crate::class::basic::CompareOp; -use crate::conversion::{AsPyPointer, FromPyObjectBound, IntoPyObject}; +use crate::conversion::{private, AsPyPointer, FromPyObjectBound, IntoPy, IntoPyObject}; use crate::err::{DowncastError, DowncastIntoError, PyErr, PyResult}; use crate::exceptions::{PyAttributeError, PyTypeError}; use crate::ffi_ptr_ext::FfiPtrExt; @@ -10,7 +10,7 @@ use crate::type_object::{PyTypeCheck, PyTypeInfo}; #[cfg(not(any(PyPy, GraalPy)))] use crate::types::PySuper; use crate::types::{PyDict, PyIterator, PyList, PyString, PyTuple, PyType}; -use crate::{err, ffi, BoundObject, Python}; +use crate::{err, ffi, BoundObject, Py, Python}; use std::cell::UnsafeCell; use std::cmp::Ordering; use std::os::raw::c_int; @@ -457,8 +457,7 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed { /// ``` fn call(&self, args: A, kwargs: Option<&Bound<'_, PyDict>>) -> PyResult> where - A: IntoPyObject<'py, Target = PyTuple>, - A::Error: Into; + A: IntoPy>; /// Calls the object without arguments. /// @@ -513,8 +512,7 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed { /// ``` fn call1(&self, args: A) -> PyResult> where - A: IntoPyObject<'py, Target = PyTuple>, - A::Error: Into; + A: IntoPy>; /// Calls a method on the object. /// @@ -561,9 +559,8 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed { ) -> PyResult> where N: IntoPyObject<'py, Target = PyString>, - A: IntoPyObject<'py, Target = PyTuple>, - N::Error: Into, - A::Error: Into; + A: IntoPy>, + N::Error: Into; /// Calls a method on the object without arguments. /// @@ -640,9 +637,8 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed { fn call_method1(&self, name: N, args: A) -> PyResult> where N: IntoPyObject<'py, Target = PyString>, - A: IntoPyObject<'py, Target = PyTuple>, - N::Error: Into, - A::Error: Into; + A: IntoPy>, + N::Error: Into; /// Returns whether the object is considered to be true. /// @@ -1269,44 +1265,29 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { fn call(&self, args: A, kwargs: Option<&Bound<'_, PyDict>>) -> PyResult> where - A: IntoPyObject<'py, Target = PyTuple>, - A::Error: Into, + A: IntoPy>, { - fn inner<'py>( - any: &Bound<'py, PyAny>, - args: &Bound<'_, PyTuple>, - kwargs: Option<&Bound<'_, PyDict>>, - ) -> PyResult> { - unsafe { - ffi::PyObject_Call( - any.as_ptr(), - args.as_ptr(), - kwargs.map_or(std::ptr::null_mut(), |dict| dict.as_ptr()), - ) - .assume_owned_or_err(any.py()) - } - } - - let py = self.py(); - inner( - self, - &args.into_pyobject(py).map_err(Into::into)?.as_borrowed(), - kwargs, + args.__py_call_vectorcall( + self.py(), + self.as_borrowed(), + kwargs.map(Bound::as_borrowed), + private::Token, ) } + #[inline] fn call0(&self) -> PyResult> { unsafe { ffi::compat::PyObject_CallNoArgs(self.as_ptr()).assume_owned_or_err(self.py()) } } fn call1(&self, args: A) -> PyResult> where - A: IntoPyObject<'py, Target = PyTuple>, - A::Error: Into, + A: IntoPy>, { - self.call(args, None) + args.__py_call_vectorcall1(self.py(), self.as_borrowed(), private::Token) } + #[inline] fn call_method( &self, name: N, @@ -1315,14 +1296,19 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { ) -> PyResult> where N: IntoPyObject<'py, Target = PyString>, - A: IntoPyObject<'py, Target = PyTuple>, + A: IntoPy>, N::Error: Into, - A::Error: Into, { - self.getattr(name) - .and_then(|method| method.call(args, kwargs)) + // Don't `args.into_py()`! This will lose the optimization of vectorcall. + match kwargs { + Some(_) => self + .getattr(name) + .and_then(|method| method.call(args, kwargs)), + None => self.call_method1(name, args), + } } + #[inline] fn call_method0(&self, name: N) -> PyResult> where N: IntoPyObject<'py, Target = PyString>, @@ -1339,11 +1325,17 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { fn call_method1(&self, name: N, args: A) -> PyResult> where N: IntoPyObject<'py, Target = PyString>, - A: IntoPyObject<'py, Target = PyTuple>, + A: IntoPy>, N::Error: Into, - A::Error: Into, { - self.call_method(name, args, None) + args.__py_call_method_vectorcall1( + self.py(), + self.as_borrowed(), + name.into_pyobject(self.py()) + .map_err(Into::into)? + .as_borrowed(), + private::Token, + ) } fn is_truthy(&self) -> PyResult { diff --git a/src/types/tuple.rs b/src/types/tuple.rs index 44a9d15e836..832cf85d2fc 100644 --- a/src/types/tuple.rs +++ b/src/types/tuple.rs @@ -1,13 +1,15 @@ use std::iter::FusedIterator; -use crate::conversion::IntoPyObject; +use crate::conversion::{private, IntoPyObject}; use crate::ffi::{self, Py_ssize_t}; use crate::ffi_ptr_ext::FfiPtrExt; #[cfg(feature = "experimental-inspect")] use crate::inspect::types::TypeInfo; use crate::instance::Borrowed; use crate::internal_tricks::get_ssize_index; -use crate::types::{any::PyAnyMethods, sequence::PySequenceMethods, PyList, PySequence}; +use crate::types::{ + any::PyAnyMethods, sequence::PySequenceMethods, PyDict, PyList, PySequence, PyString, +}; use crate::{ exceptions, Bound, BoundObject, FromPyObject, IntoPy, Py, PyAny, PyErr, PyObject, PyResult, Python, ToPyObject, @@ -522,7 +524,7 @@ macro_rules! tuple_conversion ({$length:expr,$(($refN:ident, $n:tt, $T:ident)),+ } #[cfg(feature = "experimental-inspect")] -fn type_output() -> TypeInfo { + fn type_output() -> TypeInfo { TypeInfo::Tuple(Some(vec![$( $T::type_output() ),+])) } } @@ -550,6 +552,109 @@ fn type_output() -> TypeInfo { fn type_output() -> TypeInfo { TypeInfo::Tuple(Some(vec![$( $T::type_output() ),+])) } + + #[inline] + fn __py_call_vectorcall1<'py>( + self, + py: Python<'py>, + function: Borrowed<'_, 'py, PyAny>, + _: private::Token, + ) -> PyResult> { + cfg_if::cfg_if! { + if #[cfg(all(Py_3_9, not(any(PyPy, GraalPy, Py_LIMITED_API))))] { + // We need this to drop the arguments correctly. + let args_bound = [$(self.$n.into_py(py).into_bound(py),)*]; + if $length == 1 { + unsafe { + ffi::PyObject_CallOneArg(function.as_ptr(), args_bound[0].as_ptr()).assume_owned_or_err(py) + } + } else { + // Prepend one null argument for `PY_VECTORCALL_ARGUMENTS_OFFSET`. + let mut args = [std::ptr::null_mut(), $(args_bound[$n].as_ptr()),*]; + unsafe { + ffi::PyObject_Vectorcall( + function.as_ptr(), + args.as_mut_ptr().add(1), + $length + ffi::PY_VECTORCALL_ARGUMENTS_OFFSET, + std::ptr::null_mut(), + ) + .assume_owned_or_err(py) + } + } + } else { + function.call1(>>::into_py(self, py).into_bound(py)) + } + } + } + + #[inline] + fn __py_call_vectorcall<'py>( + self, + py: Python<'py>, + function: Borrowed<'_, 'py, PyAny>, + kwargs: Option>, + _: private::Token, + ) -> PyResult> { + cfg_if::cfg_if! { + if #[cfg(all(Py_3_9, not(any(PyPy, GraalPy, Py_LIMITED_API))))] { + // We need this to drop the arguments correctly. + let args_bound = [$(self.$n.into_py(py).into_bound(py),)*]; + // Prepend one null argument for `PY_VECTORCALL_ARGUMENTS_OFFSET`. + let mut args = [std::ptr::null_mut(), $(args_bound[$n].as_ptr()),*]; + unsafe { + ffi::PyObject_VectorcallDict( + function.as_ptr(), + args.as_mut_ptr().add(1), + $length + ffi::PY_VECTORCALL_ARGUMENTS_OFFSET, + kwargs.map_or_else(std::ptr::null_mut, |kwargs| kwargs.as_ptr()), + ) + .assume_owned_or_err(py) + } + } else { + function.call(>>::into_py(self, py).into_bound(py), kwargs.as_deref()) + } + } + } + + #[inline] + fn __py_call_method_vectorcall1<'py>( + self, + py: Python<'py>, + object: Borrowed<'_, 'py, PyAny>, + method_name: Borrowed<'_, 'py, PyString>, + _: private::Token, + ) -> PyResult> { + cfg_if::cfg_if! { + if #[cfg(all(Py_3_9, not(any(PyPy, GraalPy, Py_LIMITED_API))))] { + // We need this to drop the arguments correctly. + let args_bound = [$(self.$n.into_py(py).into_bound(py),)*]; + if $length == 1 { + unsafe { + ffi::PyObject_CallMethodOneArg( + object.as_ptr(), + method_name.as_ptr(), + args_bound[0].as_ptr(), + ) + .assume_owned_or_err(py) + } + } else { + let mut args = [object.as_ptr(), $(args_bound[$n].as_ptr()),*]; + unsafe { + ffi::PyObject_VectorcallMethod( + method_name.as_ptr(), + args.as_mut_ptr(), + // +1 for the receiver. + 1 + $length + ffi::PY_VECTORCALL_ARGUMENTS_OFFSET, + std::ptr::null_mut(), + ) + .assume_owned_or_err(py) + } + } + } else { + object.call_method1(method_name, >>::into_py(self, py).into_bound(py)) + } + } + } } impl<'py, $($T: FromPyObject<'py>),+> FromPyObject<'py> for ($($T,)+) { @@ -568,7 +673,7 @@ fn type_output() -> TypeInfo { } #[cfg(feature = "experimental-inspect")] -fn type_input() -> TypeInfo { + fn type_input() -> TypeInfo { TypeInfo::Tuple(Some(vec![$( $T::type_input() ),+])) } } From 3d46a3ad3551f855eb2eeeb63bfac98ba4789a85 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Sun, 25 Aug 2024 07:07:05 +0100 Subject: [PATCH 244/495] ci: run codecov job using PR head (#4488) * ci: run codecov job using PR head * ci: run all coverage OS on PR, and none on merge group --- .github/workflows/ci.yml | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ede5d7172b5..c6b31cdc378 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -386,6 +386,7 @@ jobs: - run: cargo rustdoc --lib --no-default-features --features full -Zunstable-options --config "build.rustdocflags=[\"--cfg\", \"docsrs\"]" coverage: + if : ${{ github.event_name != 'merge_group' }} needs: [fmt] name: coverage ${{ matrix.os }} strategy: @@ -393,33 +394,25 @@ jobs: os: ["windows-latest", "macos-14", "ubuntu-latest"] # first available arm macos runner runs-on: ${{ matrix.os }} steps: - - if: ${{ github.event_name == 'pull_request' && matrix.os != 'ubuntu-latest' }} - id: should-skip - shell: bash - run: echo 'skip=true' >> $GITHUB_OUTPUT - uses: actions/checkout@v4 - if: steps.should-skip.outputs.skip != 'true' + with: + # Use the PR head, not the merge commit, because the head commit (and the base) + # is what codecov uses to calculate diffs. + ref: ${{ github.event.pull_request.head.sha }} - uses: actions/setup-python@v5 - if: steps.should-skip.outputs.skip != 'true' with: python-version: '3.12' - uses: Swatinem/rust-cache@v2 - if: steps.should-skip.outputs.skip != 'true' with: save-if: ${{ github.event_name != 'merge_group' }} - uses: dtolnay/rust-toolchain@stable - if: steps.should-skip.outputs.skip != 'true' with: 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 - run: python -m pip install --upgrade pip && pip install nox - if: steps.should-skip.outputs.skip != 'true' - run: nox -s coverage - if: steps.should-skip.outputs.skip != 'true' - uses: codecov/codecov-action@v4 - if: steps.should-skip.outputs.skip != 'true' with: file: coverage.json name: ${{ matrix.os }} From c26bc3dd476f83b6fae8d8579e13b651cce97e4a Mon Sep 17 00:00:00 2001 From: Chayim Refael Friedman Date: Sun, 25 Aug 2024 11:00:53 +0300 Subject: [PATCH 245/495] Remove the `BoundObject` impl for `&Bound`, use `Borrowed` instead (#4487) * Remove the `BoundObject` impl for `&Bound`, use `Borrowed` instead For rationale see https://github.com/PyO3/pyo3/issues/4467. I chose to make `bound_object_sealed::Sealed` unsafe instead of `BoundObject` so that users won't see the `# Safety` section, as it's not relevant for them. * delete newsfragment --------- Co-authored-by: David Hewitt --- src/conversion.rs | 8 ++++---- src/instance.rs | 38 ++++++++++---------------------------- src/pycell.rs | 10 +++++----- 3 files changed, 19 insertions(+), 37 deletions(-) diff --git a/src/conversion.rs b/src/conversion.rs index 991f3c56bc1..ffa290903d4 100644 --- a/src/conversion.rs +++ b/src/conversion.rs @@ -280,8 +280,8 @@ pub trait IntoPyObject<'py>: Sized { type Target; /// The smart pointer type to use. /// - /// This will usually be [`Bound<'py, Target>`], but can special cases `&'a Bound<'py, Target>` - /// or [`Borrowed<'a, 'py, Target>`] can be used to minimize reference counting overhead. + /// This will usually be [`Bound<'py, Target>`], but in special cases [`Borrowed<'a, 'py, Target>`] can be + /// used to minimize reference counting overhead. type Output: BoundObject<'py, Self::Target>; /// The type returned in the event of a conversion error. type Error; @@ -361,11 +361,11 @@ impl<'py, T> IntoPyObject<'py> for Bound<'py, T> { impl<'a, 'py, T> IntoPyObject<'py> for &'a Bound<'py, T> { type Target = T; - type Output = &'a Bound<'py, Self::Target>; + type Output = Borrowed<'a, 'py, Self::Target>; type Error = Infallible; fn into_pyobject(self, _py: Python<'py>) -> Result { - Ok(self) + Ok(self.as_borrowed()) } } diff --git a/src/instance.rs b/src/instance.rs index 5e7b3173759..d6b9a0fb1e9 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -17,6 +17,8 @@ use std::ops::Deref; use std::ptr::NonNull; /// Owned or borrowed gil-bound Python smart pointer +/// +/// This is implemented for [`Bound`] and [`Borrowed`]. pub trait BoundObject<'py, T>: bound_object_sealed::Sealed { /// Type erased version of `Self` type Any: BoundObject<'py, PyAny>; @@ -33,11 +35,15 @@ pub trait BoundObject<'py, T>: bound_object_sealed::Sealed { } mod bound_object_sealed { - pub trait Sealed {} + /// # Safety + /// + /// Type must be layout-compatible with `*mut ffi::PyObject`. + pub unsafe trait Sealed {} - impl<'py, T> Sealed for super::Bound<'py, T> {} - impl<'a, 'py, T> Sealed for &'a super::Bound<'py, T> {} - impl<'a, 'py, T> Sealed for super::Borrowed<'a, 'py, T> {} + // SAFETY: `Bound` is layout-compatible with `*mut ffi::PyObject`. + unsafe impl<'py, T> Sealed for super::Bound<'py, T> {} + // SAFETY: `Borrowed` is layout-compatible with `*mut ffi::PyObject`. + unsafe impl<'a, 'py, T> Sealed for super::Borrowed<'a, 'py, T> {} } /// A GIL-attached equivalent to [`Py`]. @@ -615,30 +621,6 @@ impl<'py, T> BoundObject<'py, T> for Bound<'py, T> { } } -impl<'a, 'py, T> BoundObject<'py, T> for &'a Bound<'py, T> { - type Any = &'a Bound<'py, PyAny>; - - fn as_borrowed(&self) -> Borrowed<'a, 'py, T> { - Bound::as_borrowed(self) - } - - fn into_bound(self) -> Bound<'py, T> { - self.clone() - } - - fn into_any(self) -> Self::Any { - self.as_any() - } - - fn into_ptr(self) -> *mut ffi::PyObject { - self.clone().into_ptr() - } - - fn unbind(self) -> Py { - self.clone().unbind() - } -} - /// A borrowed equivalent to `Bound`. /// /// The advantage of this over `&Bound` is that it avoids the need to have a pointer-to-pointer, as Bound diff --git a/src/pycell.rs b/src/pycell.rs index 0a05acd0f02..dc8b8b495a5 100644 --- a/src/pycell.rs +++ b/src/pycell.rs @@ -199,7 +199,7 @@ use crate::ffi_ptr_ext::FfiPtrExt; use crate::internal_tricks::{ptr_from_mut, ptr_from_ref}; use crate::pyclass::{boolean_struct::False, PyClass}; use crate::types::any::PyAnyMethods; -use crate::{ffi, Bound, IntoPy, PyErr, PyObject, Python}; +use crate::{ffi, Borrowed, Bound, IntoPy, PyErr, PyObject, Python}; use std::convert::Infallible; use std::fmt; use std::mem::ManuallyDrop; @@ -479,11 +479,11 @@ impl<'py, T: PyClass> IntoPyObject<'py> for PyRef<'py, T> { impl<'a, 'py, T: PyClass> IntoPyObject<'py> for &'a PyRef<'py, T> { type Target = T; - type Output = &'a Bound<'py, T>; + type Output = Borrowed<'a, 'py, T>; type Error = Infallible; fn into_pyobject(self, _py: Python<'py>) -> Result { - Ok(&self.inner) + Ok(self.inner.as_borrowed()) } } @@ -668,11 +668,11 @@ impl<'py, T: PyClass> IntoPyObject<'py> for PyRefMut<'py, T> { impl<'a, 'py, T: PyClass> IntoPyObject<'py> for &'a PyRefMut<'py, T> { type Target = T; - type Output = &'a Bound<'py, T>; + type Output = Borrowed<'a, 'py, T>; type Error = Infallible; fn into_pyobject(self, _py: Python<'py>) -> Result { - Ok(&self.inner) + Ok(self.inner.as_borrowed()) } } From 71b10b8ee812a3345ef68a51f85b1dc3bfaee1ca Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Sun, 25 Aug 2024 14:22:51 +0200 Subject: [PATCH 246/495] reintroduce weakref constructors (#4491) --- src/types/weakref/anyref.rs | 24 +++---- src/types/weakref/proxy.rs | 119 +++++++++++++++++++-------------- src/types/weakref/reference.rs | 83 +++++++++++++---------- 3 files changed, 128 insertions(+), 98 deletions(-) diff --git a/src/types/weakref/anyref.rs b/src/types/weakref/anyref.rs index 4f88a014689..182bf9f549d 100644 --- a/src/types/weakref/anyref.rs +++ b/src/types/weakref/anyref.rs @@ -71,7 +71,7 @@ pub trait PyWeakrefMethods<'py> { /// # fn main() -> PyResult<()> { /// Python::with_gil(|py| { /// let data = Bound::new(py, Foo{})?; - /// let reference = PyWeakrefReference::new_bound(&data)?; + /// let reference = PyWeakrefReference::new(&data)?; /// /// assert_eq!( /// parse_data(reference.as_borrowed())?, @@ -147,7 +147,7 @@ pub trait PyWeakrefMethods<'py> { /// # fn main() -> PyResult<()> { /// Python::with_gil(|py| { /// let data = Bound::new(py, Foo{})?; - /// let reference = PyWeakrefReference::new_bound(&data)?; + /// let reference = PyWeakrefReference::new(&data)?; /// /// assert_eq!( /// parse_data(reference.as_borrowed())?, @@ -232,7 +232,7 @@ pub trait PyWeakrefMethods<'py> { /// # fn main() -> PyResult<()> { /// Python::with_gil(|py| { /// let data = Bound::new(py, Foo{})?; - /// let reference = PyWeakrefReference::new_bound(&data)?; + /// let reference = PyWeakrefReference::new(&data)?; /// /// assert_eq!( /// parse_data(reference.as_borrowed()), @@ -306,7 +306,7 @@ pub trait PyWeakrefMethods<'py> { /// # fn main() -> PyResult<()> { /// Python::with_gil(|py| { /// let data = Bound::new(py, Foo{})?; - /// let reference = PyWeakrefReference::new_bound(&data)?; + /// let reference = PyWeakrefReference::new(&data)?; /// /// assert_eq!( /// parse_data(reference.as_borrowed()), @@ -379,7 +379,7 @@ pub trait PyWeakrefMethods<'py> { /// # fn main() -> PyResult<()> { /// Python::with_gil(|py| { /// let data = Bound::new(py, Foo{})?; - /// let reference = PyWeakrefReference::new_bound(&data)?; + /// let reference = PyWeakrefReference::new(&data)?; /// /// assert_eq!( /// parse_data(reference.as_borrowed())?, @@ -455,7 +455,7 @@ pub trait PyWeakrefMethods<'py> { /// # fn main() -> PyResult<()> { /// Python::with_gil(|py| { /// let data = Bound::new(py, Foo{})?; - /// let reference = PyWeakrefReference::new_bound(&data)?; + /// let reference = PyWeakrefReference::new(&data)?; /// /// assert_eq!( /// parse_data(reference.as_borrowed())?, @@ -530,7 +530,7 @@ pub trait PyWeakrefMethods<'py> { /// # fn main() -> PyResult<()> { /// Python::with_gil(|py| { /// let data = Bound::new(py, Foo{})?; - /// let reference = PyWeakrefReference::new_bound(&data)?; + /// let reference = PyWeakrefReference::new(&data)?; /// /// assert_eq!( /// parse_data(reference.as_borrowed())?, @@ -600,7 +600,7 @@ pub trait PyWeakrefMethods<'py> { /// # fn main() -> PyResult<()> { /// Python::with_gil(|py| { /// let data = Bound::new(py, Foo{})?; - /// let reference = PyWeakrefReference::new_bound(&data)?; + /// let reference = PyWeakrefReference::new(&data)?; /// /// assert_eq!( /// parse_data(reference.as_borrowed())?, @@ -672,7 +672,7 @@ pub trait PyWeakrefMethods<'py> { /// # fn main() -> PyResult<()> { /// Python::with_gil(|py| { /// let object = Bound::new(py, Foo{})?; - /// let reference = PyWeakrefReference::new_bound(&object)?; + /// let reference = PyWeakrefReference::new(&object)?; /// /// assert_eq!( /// get_class(reference.as_borrowed())?, @@ -733,7 +733,7 @@ pub trait PyWeakrefMethods<'py> { /// # fn main() -> PyResult<()> { /// Python::with_gil(|py| { /// let object = Bound::new(py, Foo{})?; - /// let reference = PyWeakrefReference::new_bound(&object)?; + /// let reference = PyWeakrefReference::new(&object)?; /// /// assert_eq!( /// get_class(reference.as_borrowed())?, @@ -776,12 +776,12 @@ mod tests { use crate::{Bound, PyResult, Python}; fn new_reference<'py>(object: &Bound<'py, PyAny>) -> PyResult> { - let reference = PyWeakrefReference::new_bound(object)?; + let reference = PyWeakrefReference::new(object)?; reference.into_any().downcast_into().map_err(Into::into) } fn new_proxy<'py>(object: &Bound<'py, PyAny>) -> PyResult> { - let reference = PyWeakrefProxy::new_bound(object)?; + let reference = PyWeakrefProxy::new(object)?; reference.into_any().downcast_into().map_err(Into::into) } diff --git a/src/types/weakref/proxy.rs b/src/types/weakref/proxy.rs index 870399fbef4..c902f5d94a5 100644 --- a/src/types/weakref/proxy.rs +++ b/src/types/weakref/proxy.rs @@ -52,14 +52,14 @@ impl PyWeakrefProxy { /// # fn main() -> PyResult<()> { /// Python::with_gil(|py| { /// let foo = Bound::new(py, Foo {})?; - /// let weakref = PyWeakrefProxy::new_bound(&foo)?; + /// let weakref = PyWeakrefProxy::new(&foo)?; /// assert!( /// // In normal situations where a direct `Bound<'py, Foo>` is required use `upgrade::` /// weakref.upgrade() /// .map_or(false, |obj| obj.is(&foo)) /// ); /// - /// let weakref2 = PyWeakrefProxy::new_bound(&foo)?; + /// let weakref2 = PyWeakrefProxy::new(&foo)?; /// assert!(weakref.is(&weakref2)); /// /// drop(foo); @@ -70,19 +70,21 @@ impl PyWeakrefProxy { /// # } /// ``` #[inline] - pub fn new_bound<'py>(object: &Bound<'py, PyAny>) -> PyResult> { - // TODO: Is this inner pattern still necessary Here? - fn inner<'py>(object: &Bound<'py, PyAny>) -> PyResult> { - unsafe { - Bound::from_owned_ptr_or_err( - object.py(), - ffi::PyWeakref_NewProxy(object.as_ptr(), ffi::Py_None()), - ) - .downcast_into_unchecked() - } + pub fn new<'py>(object: &Bound<'py, PyAny>) -> PyResult> { + unsafe { + Bound::from_owned_ptr_or_err( + object.py(), + ffi::PyWeakref_NewProxy(object.as_ptr(), ffi::Py_None()), + ) + .downcast_into_unchecked() } + } - inner(object) + /// Deprecated name for [`PyWeakrefProxy::new`]. + #[deprecated(since = "0.23.0", note = "renamed to `PyWeakrefProxy::new`")] + #[inline] + pub fn new_bound<'py>(object: &Bound<'py, PyAny>) -> PyResult> { + Self::new(object) } /// Constructs a new Weak Reference (`weakref.proxy`/`weakref.ProxyType`/`weakref.CallableProxyType`) for the given object with a callback. @@ -119,7 +121,7 @@ impl PyWeakrefProxy { /// let foo = Bound::new(py, Foo{})?; /// /// // This is fine. - /// let weakref = PyWeakrefProxy::new_bound_with(&foo, py.None())?; + /// let weakref = PyWeakrefProxy::new_with(&foo, py.None())?; /// assert!(weakref.upgrade_as::()?.is_some()); /// assert!( /// // In normal situations where a direct `Bound<'py, Foo>` is required use `upgrade::` @@ -128,7 +130,7 @@ impl PyWeakrefProxy { /// ); /// assert_eq!(py.eval(c_str!("counter"), None, None)?.extract::()?, 0); /// - /// let weakref2 = PyWeakrefProxy::new_bound_with(&foo, wrap_pyfunction!(callback, py)?)?; + /// let weakref2 = PyWeakrefProxy::new_with(&foo, wrap_pyfunction!(callback, py)?)?; /// assert!(!weakref.is(&weakref2)); // Not the same weakref /// assert!(weakref.eq(&weakref2)?); // But Equal, since they point to the same object /// @@ -141,7 +143,7 @@ impl PyWeakrefProxy { /// # } /// ``` #[inline] - pub fn new_bound_with<'py, C>( + pub fn new_with<'py, C>( object: &Bound<'py, PyAny>, callback: C, ) -> PyResult> @@ -164,6 +166,19 @@ impl PyWeakrefProxy { let py = object.py(); inner(object, callback.to_object(py).into_bound(py)) } + + /// Deprecated name for [`PyWeakrefProxy::new_with`]. + #[deprecated(since = "0.23.0", note = "renamed to `PyWeakrefProxy::new_with`")] + #[inline] + pub fn new_bound_with<'py, C>( + object: &Bound<'py, PyAny>, + callback: C, + ) -> PyResult> + where + C: ToPyObject, + { + Self::new_with(object, callback) + } } impl<'py> PyWeakrefMethods<'py> for Bound<'py, PyWeakrefProxy> { @@ -252,7 +267,7 @@ mod tests { Python::with_gil(|py| { let class = get_type(py)?; let object = class.call0()?; - let reference = PyWeakrefProxy::new_bound(&object)?; + let reference = PyWeakrefProxy::new(&object)?; assert!(!reference.is(&object)); assert!(reference.get_object().is(&object)); @@ -314,7 +329,7 @@ mod tests { Python::with_gil(|py| { let class = get_type(py)?; let object = class.call0()?; - let reference = PyWeakrefProxy::new_bound(&object)?; + let reference = PyWeakrefProxy::new(&object)?; { // This test is a bit weird but ok. @@ -349,7 +364,7 @@ mod tests { Python::with_gil(|py| { let class = get_type(py)?; let object = class.call0()?; - let reference = PyWeakrefProxy::new_bound(&object)?; + let reference = PyWeakrefProxy::new(&object)?; { // This test is a bit weird but ok. @@ -384,7 +399,7 @@ mod tests { Python::with_gil(|py| { let class = get_type(py)?; let object = class.call0()?; - let reference = PyWeakrefProxy::new_bound(&object)?; + let reference = PyWeakrefProxy::new(&object)?; { // This test is a bit weird but ok. @@ -413,7 +428,7 @@ mod tests { Python::with_gil(|py| { let class = get_type(py)?; let object = class.call0()?; - let reference = PyWeakrefProxy::new_bound(&object)?; + let reference = PyWeakrefProxy::new(&object)?; { // This test is a bit weird but ok. @@ -442,7 +457,7 @@ mod tests { Python::with_gil(|py| { let class = get_type(py)?; let object = class.call0()?; - let reference = PyWeakrefProxy::new_bound(&object)?; + let reference = PyWeakrefProxy::new(&object)?; assert!(reference.upgrade().is_some()); assert!(reference.upgrade().map_or(false, |obj| obj.is(&object))); @@ -460,7 +475,7 @@ mod tests { Python::with_gil(|py| { let class = get_type(py)?; let object = class.call0()?; - let reference = PyWeakrefProxy::new_bound(&object)?; + let reference = PyWeakrefProxy::new(&object)?; assert!(reference.upgrade_borrowed().is_some()); assert!(reference @@ -480,7 +495,7 @@ mod tests { Python::with_gil(|py| { let class = get_type(py)?; let object = class.call0()?; - let reference = PyWeakrefProxy::new_bound(&object)?; + let reference = PyWeakrefProxy::new(&object)?; assert!(reference.get_object().is(&object)); @@ -497,7 +512,7 @@ mod tests { Python::with_gil(|py| { let class = get_type(py)?; let object = class.call0()?; - let reference = PyWeakrefProxy::new_bound(&object)?; + let reference = PyWeakrefProxy::new(&object)?; assert!(reference.get_object_borrowed().is(&object)); @@ -524,7 +539,7 @@ mod tests { Python::with_gil(|py| { let object: Bound<'_, WeakrefablePyClass> = Bound::new(py, WeakrefablePyClass {})?; - let reference = PyWeakrefProxy::new_bound(&object)?; + let reference = PyWeakrefProxy::new(&object)?; assert!(!reference.is(&object)); assert!(reference.get_object().is(&object)); @@ -587,7 +602,7 @@ mod tests { fn test_weakref_upgrade_as() -> PyResult<()> { Python::with_gil(|py| { let object = Py::new(py, WeakrefablePyClass {})?; - let reference = PyWeakrefProxy::new_bound(object.bind(py))?; + let reference = PyWeakrefProxy::new(object.bind(py))?; { let obj = reference.upgrade_as::(); @@ -618,7 +633,7 @@ mod tests { fn test_weakref_upgrade_borrowed_as() -> PyResult<()> { Python::with_gil(|py| { let object = Py::new(py, WeakrefablePyClass {})?; - let reference = PyWeakrefProxy::new_bound(object.bind(py))?; + let reference = PyWeakrefProxy::new(object.bind(py))?; { let obj = reference.upgrade_borrowed_as::(); @@ -649,7 +664,7 @@ mod tests { fn test_weakref_upgrade_as_unchecked() -> PyResult<()> { Python::with_gil(|py| { let object = Py::new(py, WeakrefablePyClass {})?; - let reference = PyWeakrefProxy::new_bound(object.bind(py))?; + let reference = PyWeakrefProxy::new(object.bind(py))?; { let obj = unsafe { reference.upgrade_as_unchecked::() }; @@ -674,7 +689,7 @@ mod tests { fn test_weakref_upgrade_borrowed_as_unchecked() -> PyResult<()> { Python::with_gil(|py| { let object = Py::new(py, WeakrefablePyClass {})?; - let reference = PyWeakrefProxy::new_bound(object.bind(py))?; + let reference = PyWeakrefProxy::new(object.bind(py))?; { let obj = unsafe { @@ -703,7 +718,7 @@ mod tests { fn test_weakref_upgrade() -> PyResult<()> { Python::with_gil(|py| { let object = Py::new(py, WeakrefablePyClass {})?; - let reference = PyWeakrefProxy::new_bound(object.bind(py))?; + let reference = PyWeakrefProxy::new(object.bind(py))?; assert!(reference.upgrade().is_some()); assert!(reference.upgrade().map_or(false, |obj| obj.is(&object))); @@ -720,7 +735,7 @@ mod tests { fn test_weakref_upgrade_borrowed() -> PyResult<()> { Python::with_gil(|py| { let object = Py::new(py, WeakrefablePyClass {})?; - let reference = PyWeakrefProxy::new_bound(object.bind(py))?; + let reference = PyWeakrefProxy::new(object.bind(py))?; assert!(reference.upgrade_borrowed().is_some()); assert!(reference @@ -739,7 +754,7 @@ mod tests { fn test_weakref_get_object() -> PyResult<()> { Python::with_gil(|py| { let object = Py::new(py, WeakrefablePyClass {})?; - let reference = PyWeakrefProxy::new_bound(object.bind(py))?; + let reference = PyWeakrefProxy::new(object.bind(py))?; assert!(reference.get_object().is(&object)); @@ -755,7 +770,7 @@ mod tests { fn test_weakref_get_object_borrowed() -> PyResult<()> { Python::with_gil(|py| { let object = Py::new(py, WeakrefablePyClass {})?; - let reference = PyWeakrefProxy::new_bound(object.bind(py))?; + let reference = PyWeakrefProxy::new(object.bind(py))?; assert!(reference.get_object_borrowed().is(&object)); @@ -798,7 +813,7 @@ mod tests { Python::with_gil(|py| { let class = get_type(py)?; let object = class.call0()?; - let reference = PyWeakrefProxy::new_bound(&object)?; + let reference = PyWeakrefProxy::new(&object)?; assert!(!reference.is(&object)); assert!(reference.get_object().is(&object)); @@ -847,7 +862,7 @@ mod tests { Python::with_gil(|py| { let class = get_type(py)?; let object = class.call0()?; - let reference = PyWeakrefProxy::new_bound(&object)?; + let reference = PyWeakrefProxy::new(&object)?; { // This test is a bit weird but ok. @@ -882,7 +897,7 @@ mod tests { Python::with_gil(|py| { let class = get_type(py)?; let object = class.call0()?; - let reference = PyWeakrefProxy::new_bound(&object)?; + let reference = PyWeakrefProxy::new(&object)?; { // This test is a bit weird but ok. @@ -917,7 +932,7 @@ mod tests { Python::with_gil(|py| { let class = get_type(py)?; let object = class.call0()?; - let reference = PyWeakrefProxy::new_bound(&object)?; + let reference = PyWeakrefProxy::new(&object)?; { // This test is a bit weird but ok. @@ -946,7 +961,7 @@ mod tests { Python::with_gil(|py| { let class = get_type(py)?; let object = class.call0()?; - let reference = PyWeakrefProxy::new_bound(&object)?; + let reference = PyWeakrefProxy::new(&object)?; { // This test is a bit weird but ok. @@ -975,7 +990,7 @@ mod tests { Python::with_gil(|py| { let class = get_type(py)?; let object = class.call0()?; - let reference = PyWeakrefProxy::new_bound(&object)?; + let reference = PyWeakrefProxy::new(&object)?; assert!(reference.upgrade().is_some()); assert!(reference.upgrade().map_or(false, |obj| obj.is(&object))); @@ -993,7 +1008,7 @@ mod tests { Python::with_gil(|py| { let class = get_type(py)?; let object = class.call0()?; - let reference = PyWeakrefProxy::new_bound(&object)?; + let reference = PyWeakrefProxy::new(&object)?; assert!(reference.upgrade_borrowed().is_some()); assert!(reference @@ -1013,7 +1028,7 @@ mod tests { Python::with_gil(|py| { let class = get_type(py)?; let object = class.call0()?; - let reference = PyWeakrefProxy::new_bound(&object)?; + let reference = PyWeakrefProxy::new(&object)?; assert!(reference.get_object().is(&object)); @@ -1030,7 +1045,7 @@ mod tests { Python::with_gil(|py| { let class = get_type(py)?; let object = class.call0()?; - let reference = PyWeakrefProxy::new_bound(&object)?; + let reference = PyWeakrefProxy::new(&object)?; assert!(reference.get_object_borrowed().is(&object)); @@ -1064,7 +1079,7 @@ mod tests { Python::with_gil(|py| { let object: Bound<'_, WeakrefablePyClass> = Bound::new(py, WeakrefablePyClass {})?; - let reference = PyWeakrefProxy::new_bound(&object)?; + let reference = PyWeakrefProxy::new(&object)?; assert!(!reference.is(&object)); assert!(reference.get_object().is(&object)); @@ -1115,7 +1130,7 @@ mod tests { fn test_weakref_upgrade_as() -> PyResult<()> { Python::with_gil(|py| { let object = Py::new(py, WeakrefablePyClass {})?; - let reference = PyWeakrefProxy::new_bound(object.bind(py))?; + let reference = PyWeakrefProxy::new(object.bind(py))?; { let obj = reference.upgrade_as::(); @@ -1145,7 +1160,7 @@ mod tests { fn test_weakref_upgrade_borrowed_as() -> PyResult<()> { Python::with_gil(|py| { let object = Py::new(py, WeakrefablePyClass {})?; - let reference = PyWeakrefProxy::new_bound(object.bind(py))?; + let reference = PyWeakrefProxy::new(object.bind(py))?; { let obj = reference.upgrade_borrowed_as::(); @@ -1176,7 +1191,7 @@ mod tests { fn test_weakref_upgrade_as_unchecked() -> PyResult<()> { Python::with_gil(|py| { let object = Py::new(py, WeakrefablePyClass {})?; - let reference = PyWeakrefProxy::new_bound(object.bind(py))?; + let reference = PyWeakrefProxy::new(object.bind(py))?; { let obj = unsafe { reference.upgrade_as_unchecked::() }; @@ -1200,7 +1215,7 @@ mod tests { fn test_weakref_upgrade_borrowed_as_unchecked() -> PyResult<()> { Python::with_gil(|py| { let object = Py::new(py, WeakrefablePyClass {})?; - let reference = PyWeakrefProxy::new_bound(object.bind(py))?; + let reference = PyWeakrefProxy::new(object.bind(py))?; { let obj = unsafe { @@ -1229,7 +1244,7 @@ mod tests { fn test_weakref_upgrade() -> PyResult<()> { Python::with_gil(|py| { let object = Py::new(py, WeakrefablePyClass {})?; - let reference = PyWeakrefProxy::new_bound(object.bind(py))?; + let reference = PyWeakrefProxy::new(object.bind(py))?; assert!(reference.upgrade().is_some()); assert!(reference.upgrade().map_or(false, |obj| obj.is(&object))); @@ -1246,7 +1261,7 @@ mod tests { fn test_weakref_upgrade_borrowed() -> PyResult<()> { Python::with_gil(|py| { let object = Py::new(py, WeakrefablePyClass {})?; - let reference = PyWeakrefProxy::new_bound(object.bind(py))?; + let reference = PyWeakrefProxy::new(object.bind(py))?; assert!(reference.upgrade_borrowed().is_some()); assert!(reference @@ -1265,7 +1280,7 @@ mod tests { fn test_weakref_get_object() -> PyResult<()> { Python::with_gil(|py| { let object = Py::new(py, WeakrefablePyClass {})?; - let reference = PyWeakrefProxy::new_bound(object.bind(py))?; + let reference = PyWeakrefProxy::new(object.bind(py))?; assert!(reference.get_object().is(&object)); @@ -1281,7 +1296,7 @@ mod tests { fn test_weakref_get_object_borrowed() -> PyResult<()> { Python::with_gil(|py| { let object = Py::new(py, WeakrefablePyClass {})?; - let reference = PyWeakrefProxy::new_bound(object.bind(py))?; + let reference = PyWeakrefProxy::new(object.bind(py))?; assert!(reference.get_object_borrowed().is(&object)); diff --git a/src/types/weakref/reference.rs b/src/types/weakref/reference.rs index 59f6f5bf3be..b86be515b5b 100644 --- a/src/types/weakref/reference.rs +++ b/src/types/weakref/reference.rs @@ -60,14 +60,14 @@ impl PyWeakrefReference { /// # fn main() -> PyResult<()> { /// Python::with_gil(|py| { /// let foo = Bound::new(py, Foo {})?; - /// let weakref = PyWeakrefReference::new_bound(&foo)?; + /// let weakref = PyWeakrefReference::new(&foo)?; /// assert!( /// // In normal situations where a direct `Bound<'py, Foo>` is required use `upgrade::` /// weakref.upgrade() /// .map_or(false, |obj| obj.is(&foo)) /// ); /// - /// let weakref2 = PyWeakrefReference::new_bound(&foo)?; + /// let weakref2 = PyWeakrefReference::new(&foo)?; /// assert!(weakref.is(&weakref2)); /// /// drop(foo); @@ -77,19 +77,21 @@ impl PyWeakrefReference { /// }) /// # } /// ``` - pub fn new_bound<'py>(object: &Bound<'py, PyAny>) -> PyResult> { - // TODO: Is this inner pattern still necessary Here? - fn inner<'py>(object: &Bound<'py, PyAny>) -> PyResult> { - unsafe { - Bound::from_owned_ptr_or_err( - object.py(), - ffi::PyWeakref_NewRef(object.as_ptr(), ffi::Py_None()), - ) - .downcast_into_unchecked() - } + pub fn new<'py>(object: &Bound<'py, PyAny>) -> PyResult> { + unsafe { + Bound::from_owned_ptr_or_err( + object.py(), + ffi::PyWeakref_NewRef(object.as_ptr(), ffi::Py_None()), + ) + .downcast_into_unchecked() } + } - inner(object) + /// Deprecated name for [`PyWeakrefReference::new`]. + #[deprecated(since = "0.23.0", note = "renamed to `PyWeakrefReference::new`")] + #[inline] + pub fn new_bound<'py>(object: &Bound<'py, PyAny>) -> PyResult> { + Self::new(object) } /// Constructs a new Weak Reference (`weakref.ref`/`weakref.ReferenceType`) for the given object with a callback. @@ -126,7 +128,7 @@ impl PyWeakrefReference { /// let foo = Bound::new(py, Foo{})?; /// /// // This is fine. - /// let weakref = PyWeakrefReference::new_bound_with(&foo, py.None())?; + /// let weakref = PyWeakrefReference::new_with(&foo, py.None())?; /// assert!(weakref.upgrade_as::()?.is_some()); /// assert!( /// // In normal situations where a direct `Bound<'py, Foo>` is required use `upgrade::` @@ -135,7 +137,7 @@ impl PyWeakrefReference { /// ); /// assert_eq!(py.eval(c_str!("counter"), None, None)?.extract::()?, 0); /// - /// let weakref2 = PyWeakrefReference::new_bound_with(&foo, wrap_pyfunction!(callback, py)?)?; + /// let weakref2 = PyWeakrefReference::new_with(&foo, wrap_pyfunction!(callback, py)?)?; /// assert!(!weakref.is(&weakref2)); // Not the same weakref /// assert!(weakref.eq(&weakref2)?); // But Equal, since they point to the same object /// @@ -147,7 +149,7 @@ impl PyWeakrefReference { /// }) /// # } /// ``` - pub fn new_bound_with<'py, C>( + pub fn new_with<'py, C>( object: &Bound<'py, PyAny>, callback: C, ) -> PyResult> @@ -170,6 +172,19 @@ impl PyWeakrefReference { let py = object.py(); inner(object, callback.to_object(py).into_bound(py)) } + + /// Deprecated name for [`PyWeakrefReference::new_with`]. + #[deprecated(since = "0.23.0", note = "renamed to `PyWeakrefReference::new_with`")] + #[inline] + pub fn new_bound_with<'py, C>( + object: &Bound<'py, PyAny>, + callback: C, + ) -> PyResult> + where + C: ToPyObject, + { + Self::new_with(object, callback) + } } impl<'py> PyWeakrefMethods<'py> for Bound<'py, PyWeakrefReference> { @@ -244,7 +259,7 @@ mod tests { Python::with_gil(|py| { let class = get_type(py)?; let object = class.call0()?; - let reference = PyWeakrefReference::new_bound(&object)?; + let reference = PyWeakrefReference::new(&object)?; assert!(!reference.is(&object)); assert!(reference.get_object().is(&object)); @@ -286,7 +301,7 @@ mod tests { Python::with_gil(|py| { let class = get_type(py)?; let object = class.call0()?; - let reference = PyWeakrefReference::new_bound(&object)?; + let reference = PyWeakrefReference::new(&object)?; { // This test is a bit weird but ok. @@ -321,7 +336,7 @@ mod tests { Python::with_gil(|py| { let class = get_type(py)?; let object = class.call0()?; - let reference = PyWeakrefReference::new_bound(&object)?; + let reference = PyWeakrefReference::new(&object)?; { // This test is a bit weird but ok. @@ -356,7 +371,7 @@ mod tests { Python::with_gil(|py| { let class = get_type(py)?; let object = class.call0()?; - let reference = PyWeakrefReference::new_bound(&object)?; + let reference = PyWeakrefReference::new(&object)?; { // This test is a bit weird but ok. @@ -385,7 +400,7 @@ mod tests { Python::with_gil(|py| { let class = get_type(py)?; let object = class.call0()?; - let reference = PyWeakrefReference::new_bound(&object)?; + let reference = PyWeakrefReference::new(&object)?; { // This test is a bit weird but ok. @@ -414,7 +429,7 @@ mod tests { Python::with_gil(|py| { let class = get_type(py)?; let object = class.call0()?; - let reference = PyWeakrefReference::new_bound(&object)?; + let reference = PyWeakrefReference::new(&object)?; assert!(reference.call0()?.is(&object)); assert!(reference.upgrade().is_some()); @@ -434,7 +449,7 @@ mod tests { Python::with_gil(|py| { let class = get_type(py)?; let object = class.call0()?; - let reference = PyWeakrefReference::new_bound(&object)?; + let reference = PyWeakrefReference::new(&object)?; assert!(reference.call0()?.is(&object)); assert!(reference.upgrade_borrowed().is_some()); @@ -456,7 +471,7 @@ mod tests { Python::with_gil(|py| { let class = get_type(py)?; let object = class.call0()?; - let reference = PyWeakrefReference::new_bound(&object)?; + let reference = PyWeakrefReference::new(&object)?; assert!(reference.call0()?.is(&object)); assert!(reference.get_object().is(&object)); @@ -476,7 +491,7 @@ mod tests { Python::with_gil(|py| { let class = get_type(py)?; let object = class.call0()?; - let reference = PyWeakrefReference::new_bound(&object)?; + let reference = PyWeakrefReference::new(&object)?; assert!(reference.call0()?.is(&object)); assert!(reference.get_object_borrowed().is(&object)); @@ -504,7 +519,7 @@ mod tests { fn test_weakref_reference_behavior() -> PyResult<()> { Python::with_gil(|py| { let object: Bound<'_, WeakrefablePyClass> = Bound::new(py, WeakrefablePyClass {})?; - let reference = PyWeakrefReference::new_bound(&object)?; + let reference = PyWeakrefReference::new(&object)?; assert!(!reference.is(&object)); assert!(reference.get_object().is(&object)); @@ -543,7 +558,7 @@ mod tests { fn test_weakref_upgrade_as() -> PyResult<()> { Python::with_gil(|py| { let object = Py::new(py, WeakrefablePyClass {})?; - let reference = PyWeakrefReference::new_bound(object.bind(py))?; + let reference = PyWeakrefReference::new(object.bind(py))?; { let obj = reference.upgrade_as::(); @@ -574,7 +589,7 @@ mod tests { fn test_weakref_upgrade_borrowed_as() -> PyResult<()> { Python::with_gil(|py| { let object = Py::new(py, WeakrefablePyClass {})?; - let reference = PyWeakrefReference::new_bound(object.bind(py))?; + let reference = PyWeakrefReference::new(object.bind(py))?; { let obj = reference.upgrade_borrowed_as::(); @@ -605,7 +620,7 @@ mod tests { fn test_weakref_upgrade_as_unchecked() -> PyResult<()> { Python::with_gil(|py| { let object = Py::new(py, WeakrefablePyClass {})?; - let reference = PyWeakrefReference::new_bound(object.bind(py))?; + let reference = PyWeakrefReference::new(object.bind(py))?; { let obj = unsafe { reference.upgrade_as_unchecked::() }; @@ -630,7 +645,7 @@ mod tests { fn test_weakref_upgrade_borrowed_as_unchecked() -> PyResult<()> { Python::with_gil(|py| { let object = Py::new(py, WeakrefablePyClass {})?; - let reference = PyWeakrefReference::new_bound(object.bind(py))?; + let reference = PyWeakrefReference::new(object.bind(py))?; { let obj = @@ -657,7 +672,7 @@ mod tests { fn test_weakref_upgrade() -> PyResult<()> { Python::with_gil(|py| { let object = Py::new(py, WeakrefablePyClass {})?; - let reference = PyWeakrefReference::new_bound(object.bind(py))?; + let reference = PyWeakrefReference::new(object.bind(py))?; assert!(reference.call0()?.is(&object)); assert!(reference.upgrade().is_some()); @@ -676,7 +691,7 @@ mod tests { fn test_weakref_upgrade_borrowed() -> PyResult<()> { Python::with_gil(|py| { let object = Py::new(py, WeakrefablePyClass {})?; - let reference = PyWeakrefReference::new_bound(object.bind(py))?; + let reference = PyWeakrefReference::new(object.bind(py))?; assert!(reference.call0()?.is(&object)); assert!(reference.upgrade_borrowed().is_some()); @@ -697,7 +712,7 @@ mod tests { fn test_weakref_get_object() -> PyResult<()> { Python::with_gil(|py| { let object = Py::new(py, WeakrefablePyClass {})?; - let reference = PyWeakrefReference::new_bound(object.bind(py))?; + let reference = PyWeakrefReference::new(object.bind(py))?; assert!(reference.call0()?.is(&object)); assert!(reference.get_object().is(&object)); @@ -716,7 +731,7 @@ mod tests { fn test_weakref_get_object_borrowed() -> PyResult<()> { Python::with_gil(|py| { let object = Py::new(py, WeakrefablePyClass {})?; - let reference = PyWeakrefReference::new_bound(object.bind(py))?; + let reference = PyWeakrefReference::new(object.bind(py))?; assert!(reference.call0()?.is(&object)); assert!(reference.get_object_borrowed().is(&object)); From b2a2a1d0186089eac81bd91f261baa53c3716423 Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Sun, 25 Aug 2024 14:40:52 +0200 Subject: [PATCH 247/495] restrict `IntoPyObject::Error` to convert into `PyErr` (#4489) --- src/conversion.rs | 4 +- src/conversions/chrono.rs | 1 - src/conversions/either.rs | 4 -- src/conversions/hashbrown.rs | 12 ++-- src/conversions/indexmap.rs | 10 ++- src/conversions/smallvec.rs | 2 - src/conversions/std/array.rs | 2 - src/conversions/std/map.rs | 20 +++--- src/conversions/std/set.rs | 4 -- src/conversions/std/slice.rs | 2 - src/conversions/std/vec.rs | 2 - src/impl_/pyclass.rs | 48 ++++--------- src/impl_/wrap.rs | 6 +- src/instance.rs | 6 -- src/types/any.rs | 128 +++++++++-------------------------- src/types/mapping.rs | 20 ++---- src/types/tuple.rs | 3 +- 17 files changed, 71 insertions(+), 203 deletions(-) diff --git a/src/conversion.rs b/src/conversion.rs index ffa290903d4..c74c529bbb8 100644 --- a/src/conversion.rs +++ b/src/conversion.rs @@ -284,7 +284,7 @@ pub trait IntoPyObject<'py>: Sized { /// used to minimize reference counting overhead. type Output: BoundObject<'py, Self::Target>; /// The type returned in the event of a conversion error. - type Error; + type Error: Into; /// Performs the conversion. fn into_pyobject(self, py: Python<'py>) -> Result; @@ -300,7 +300,6 @@ pub trait IntoPyObject<'py>: Sized { where I: IntoIterator + AsRef<[Self]>, I::IntoIter: ExactSizeIterator, - PyErr: From, { let mut iter = iter.into_iter().map(|e| { e.into_pyobject(py) @@ -324,7 +323,6 @@ pub trait IntoPyObject<'py>: Sized { Self: private::Reference, I: IntoIterator + AsRef<[::BaseType]>, I::IntoIter: ExactSizeIterator, - PyErr: From, { let mut iter = iter.into_iter().map(|e| { e.into_pyobject(py) diff --git a/src/conversions/chrono.rs b/src/conversions/chrono.rs index 42126bcb950..e33c12b93e7 100644 --- a/src/conversions/chrono.rs +++ b/src/conversions/chrono.rs @@ -1327,7 +1327,6 @@ mod tests { fn new_py_datetime_ob<'py, A>(py: Python<'py>, name: &str, args: A) -> Bound<'py, PyAny> where A: IntoPyObject<'py, Target = PyTuple>, - A::Error: Into, { py.import("datetime") .unwrap() diff --git a/src/conversions/either.rs b/src/conversions/either.rs index 3d4aeafa32b..43822347c81 100644 --- a/src/conversions/either.rs +++ b/src/conversions/either.rs @@ -71,8 +71,6 @@ impl<'py, L, R> IntoPyObject<'py> for Either where L: IntoPyObject<'py>, R: IntoPyObject<'py>, - L::Error: Into, - R::Error: Into, { type Target = PyAny; type Output = Bound<'py, Self::Target>; @@ -99,8 +97,6 @@ impl<'a, 'py, L, R> IntoPyObject<'py> for &'a Either where &'a L: IntoPyObject<'py>, &'a R: IntoPyObject<'py>, - <&'a L as IntoPyObject<'py>>::Error: Into, - <&'a R as IntoPyObject<'py>>::Error: Into, { type Target = PyAny; type Output = Bound<'py, Self::Target>; diff --git a/src/conversions/hashbrown.rs b/src/conversions/hashbrown.rs index 4189ee52a2e..84c9e888ca9 100644 --- a/src/conversions/hashbrown.rs +++ b/src/conversions/hashbrown.rs @@ -59,7 +59,6 @@ where K: IntoPyObject<'py> + cmp::Eq + hash::Hash, V: IntoPyObject<'py>, H: hash::BuildHasher, - PyErr: From + From, { type Target = PyDict; type Output = Bound<'py, Self::Target>; @@ -69,8 +68,8 @@ where let dict = PyDict::new(py); for (k, v) in self { dict.set_item( - k.into_pyobject(py)?.into_bound(), - v.into_pyobject(py)?.into_bound(), + k.into_pyobject(py).map_err(Into::into)?.into_bound(), + v.into_pyobject(py).map_err(Into::into)?.into_bound(), )?; } Ok(dict) @@ -82,7 +81,6 @@ where &'a K: IntoPyObject<'py> + cmp::Eq + hash::Hash, &'a V: IntoPyObject<'py>, H: hash::BuildHasher, - PyErr: From<<&'a K as IntoPyObject<'py>>::Error> + From<<&'a V as IntoPyObject<'py>>::Error>, { type Target = PyDict; type Output = Bound<'py, Self::Target>; @@ -92,8 +90,8 @@ where let dict = PyDict::new(py); for (k, v) in self { dict.set_item( - k.into_pyobject(py)?.into_bound(), - v.into_pyobject(py)?.into_bound(), + k.into_pyobject(py).map_err(Into::into)?.into_bound(), + v.into_pyobject(py).map_err(Into::into)?.into_bound(), )?; } Ok(dict) @@ -143,7 +141,6 @@ impl<'py, K, H> IntoPyObject<'py> for hashbrown::HashSet where K: IntoPyObject<'py> + cmp::Eq + hash::Hash, H: hash::BuildHasher, - PyErr: From, { type Target = PySet; type Output = Bound<'py, Self::Target>; @@ -166,7 +163,6 @@ impl<'a, 'py, K, H> IntoPyObject<'py> for &'a hashbrown::HashSet where &'a K: IntoPyObject<'py> + cmp::Eq + hash::Hash, H: hash::BuildHasher, - PyErr: From<<&'a K as IntoPyObject<'py>>::Error>, { type Target = PySet; type Output = Bound<'py, Self::Target>; diff --git a/src/conversions/indexmap.rs b/src/conversions/indexmap.rs index bd45d4d3164..03aba0e0fc3 100644 --- a/src/conversions/indexmap.rs +++ b/src/conversions/indexmap.rs @@ -122,7 +122,6 @@ where K: IntoPyObject<'py> + cmp::Eq + hash::Hash, V: IntoPyObject<'py>, H: hash::BuildHasher, - PyErr: From + From, { type Target = PyDict; type Output = Bound<'py, Self::Target>; @@ -132,8 +131,8 @@ where let dict = PyDict::new(py); for (k, v) in self { dict.set_item( - k.into_pyobject(py)?.into_bound(), - v.into_pyobject(py)?.into_bound(), + k.into_pyobject(py).map_err(Into::into)?.into_bound(), + v.into_pyobject(py).map_err(Into::into)?.into_bound(), )?; } Ok(dict) @@ -145,7 +144,6 @@ where &'a K: IntoPyObject<'py> + cmp::Eq + hash::Hash, &'a V: IntoPyObject<'py>, H: hash::BuildHasher, - PyErr: From<<&'a K as IntoPyObject<'py>>::Error> + From<<&'a V as IntoPyObject<'py>>::Error>, { type Target = PyDict; type Output = Bound<'py, Self::Target>; @@ -155,8 +153,8 @@ where let dict = PyDict::new(py); for (k, v) in self { dict.set_item( - k.into_pyobject(py)?.into_bound(), - v.into_pyobject(py)?.into_bound(), + k.into_pyobject(py).map_err(Into::into)?.into_bound(), + v.into_pyobject(py).map_err(Into::into)?.into_bound(), )?; } Ok(dict) diff --git a/src/conversions/smallvec.rs b/src/conversions/smallvec.rs index bffa54d00b9..8c13c7e8299 100644 --- a/src/conversions/smallvec.rs +++ b/src/conversions/smallvec.rs @@ -60,7 +60,6 @@ impl<'py, A> IntoPyObject<'py> for SmallVec where A: Array, A::Item: IntoPyObject<'py>, - PyErr: From<>::Error>, { type Target = PyAny; type Output = Bound<'py, Self::Target>; @@ -80,7 +79,6 @@ impl<'a, 'py, A> IntoPyObject<'py> for &'a SmallVec where A: Array, &'a A::Item: IntoPyObject<'py>, - PyErr: From<<&'a A::Item as IntoPyObject<'py>>::Error>, { type Target = PyAny; type Output = Bound<'py, Self::Target>; diff --git a/src/conversions/std/array.rs b/src/conversions/std/array.rs index 5645d87cfe5..5a07b224c5b 100644 --- a/src/conversions/std/array.rs +++ b/src/conversions/std/array.rs @@ -40,7 +40,6 @@ where impl<'py, T, const N: usize> IntoPyObject<'py> for [T; N] where T: IntoPyObject<'py>, - PyErr: From, { type Target = PyAny; type Output = Bound<'py, Self::Target>; @@ -59,7 +58,6 @@ where impl<'a, 'py, T, const N: usize> IntoPyObject<'py> for &'a [T; N] where &'a T: IntoPyObject<'py>, - PyErr: From<<&'a T as IntoPyObject<'py>>::Error>, { type Target = PyAny; type Output = Bound<'py, Self::Target>; diff --git a/src/conversions/std/map.rs b/src/conversions/std/map.rs index e1638150518..4aba5066ffe 100644 --- a/src/conversions/std/map.rs +++ b/src/conversions/std/map.rs @@ -54,7 +54,6 @@ where K: IntoPyObject<'py> + cmp::Eq + hash::Hash, V: IntoPyObject<'py>, H: hash::BuildHasher, - PyErr: From + From, { type Target = PyDict; type Output = Bound<'py, Self::Target>; @@ -64,8 +63,8 @@ where let dict = PyDict::new(py); for (k, v) in self { dict.set_item( - k.into_pyobject(py)?.into_bound(), - v.into_pyobject(py)?.into_bound(), + k.into_pyobject(py).map_err(Into::into)?.into_bound(), + v.into_pyobject(py).map_err(Into::into)?.into_bound(), )?; } Ok(dict) @@ -77,7 +76,6 @@ where &'a K: IntoPyObject<'py> + cmp::Eq + hash::Hash, &'a V: IntoPyObject<'py>, H: hash::BuildHasher, - PyErr: From<<&'a K as IntoPyObject<'py>>::Error> + From<<&'a V as IntoPyObject<'py>>::Error>, { type Target = PyDict; type Output = Bound<'py, Self::Target>; @@ -87,8 +85,8 @@ where let dict = PyDict::new(py); for (k, v) in self { dict.set_item( - k.into_pyobject(py)?.into_bound(), - v.into_pyobject(py)?.into_bound(), + k.into_pyobject(py).map_err(Into::into)?.into_bound(), + v.into_pyobject(py).map_err(Into::into)?.into_bound(), )?; } Ok(dict) @@ -117,7 +115,6 @@ impl<'py, K, V> IntoPyObject<'py> for collections::BTreeMap where K: IntoPyObject<'py> + cmp::Eq, V: IntoPyObject<'py>, - PyErr: From + From, { type Target = PyDict; type Output = Bound<'py, Self::Target>; @@ -127,8 +124,8 @@ where let dict = PyDict::new(py); for (k, v) in self { dict.set_item( - k.into_pyobject(py)?.into_bound(), - v.into_pyobject(py)?.into_bound(), + k.into_pyobject(py).map_err(Into::into)?.into_bound(), + v.into_pyobject(py).map_err(Into::into)?.into_bound(), )?; } Ok(dict) @@ -139,7 +136,6 @@ impl<'a, 'py, K, V> IntoPyObject<'py> for &'a collections::BTreeMap where &'a K: IntoPyObject<'py> + cmp::Eq, &'a V: IntoPyObject<'py>, - PyErr: From<<&'a K as IntoPyObject<'py>>::Error> + From<<&'a V as IntoPyObject<'py>>::Error>, { type Target = PyDict; type Output = Bound<'py, Self::Target>; @@ -149,8 +145,8 @@ where let dict = PyDict::new(py); for (k, v) in self { dict.set_item( - k.into_pyobject(py)?.into_bound(), - v.into_pyobject(py)?.into_bound(), + k.into_pyobject(py).map_err(Into::into)?.into_bound(), + v.into_pyobject(py).map_err(Into::into)?.into_bound(), )?; } Ok(dict) diff --git a/src/conversions/std/set.rs b/src/conversions/std/set.rs index 7108787370b..f0ea51f59ac 100644 --- a/src/conversions/std/set.rs +++ b/src/conversions/std/set.rs @@ -58,7 +58,6 @@ impl<'py, K, S> IntoPyObject<'py> for collections::HashSet where K: IntoPyObject<'py> + Eq + hash::Hash, S: hash::BuildHasher + Default, - PyErr: From, { type Target = PySet; type Output = Bound<'py, Self::Target>; @@ -81,7 +80,6 @@ impl<'a, 'py, K, H> IntoPyObject<'py> for &'a collections::HashSet where &'a K: IntoPyObject<'py> + Eq + hash::Hash, H: hash::BuildHasher, - PyErr: From<<&'a K as IntoPyObject<'py>>::Error>, { type Target = PySet; type Output = Bound<'py, Self::Target>; @@ -143,7 +141,6 @@ where impl<'py, K> IntoPyObject<'py> for collections::BTreeSet where K: IntoPyObject<'py> + cmp::Ord, - PyErr: From, { type Target = PySet; type Output = Bound<'py, Self::Target>; @@ -165,7 +162,6 @@ where impl<'a, 'py, K> IntoPyObject<'py> for &'a collections::BTreeSet where &'a K: IntoPyObject<'py> + cmp::Ord, - PyErr: From<<&'a K as IntoPyObject<'py>>::Error>, { type Target = PySet; type Output = Bound<'py, Self::Target>; diff --git a/src/conversions/std/slice.rs b/src/conversions/std/slice.rs index 9b9eb3dd3ec..a90d70a49f7 100644 --- a/src/conversions/std/slice.rs +++ b/src/conversions/std/slice.rs @@ -22,7 +22,6 @@ impl<'a> IntoPy for &'a [u8] { impl<'a, 'py, T> IntoPyObject<'py> for &'a [T] where &'a T: IntoPyObject<'py>, - PyErr: From<<&'a T as IntoPyObject<'py>>::Error>, { type Target = PyAny; type Output = Bound<'py, Self::Target>; @@ -86,7 +85,6 @@ impl<'py, T> IntoPyObject<'py> for Cow<'_, [T]> where T: Clone, for<'a> &'a T: IntoPyObject<'py>, - for<'a> PyErr: From<<&'a T as IntoPyObject<'py>>::Error>, { type Target = PyAny; type Output = Bound<'py, Self::Target>; diff --git a/src/conversions/std/vec.rs b/src/conversions/std/vec.rs index 40ad7eea8a0..1548f604e42 100644 --- a/src/conversions/std/vec.rs +++ b/src/conversions/std/vec.rs @@ -43,7 +43,6 @@ where impl<'py, T> IntoPyObject<'py> for Vec where T: IntoPyObject<'py>, - PyErr: From, { type Target = PyAny; type Output = Bound<'py, Self::Target>; @@ -62,7 +61,6 @@ where impl<'a, 'py, T> IntoPyObject<'py> for &'a Vec where &'a T: IntoPyObject<'py>, - PyErr: From<<&'a T as IntoPyObject<'py>>::Error>, { type Target = PyAny; type Output = Bound<'py, Self::Target>; diff --git a/src/impl_/pyclass.rs b/src/impl_/pyclass.rs index 86b6d4daf5b..1c47d01f9a2 100644 --- a/src/impl_/pyclass.rs +++ b/src/impl_/pyclass.rs @@ -1334,7 +1334,6 @@ where ClassT: PyClass, for<'a, 'py> &'a FieldT: IntoPyObject<'py>, Offset: OffsetCalculator, - for<'a, 'py> PyErr: From<<&'a FieldT as IntoPyObject<'py>>::Error>, { pub const fn generate(&self, name: &'static CStr, doc: &'static CStr) -> PyMethodDefType { PyMethodDefType::Getter(PyGetterDef { @@ -1362,7 +1361,6 @@ where ClassT: PyClass, Offset: OffsetCalculator, for<'py> FieldT: IntoPyObject<'py> + Clone, - for<'py> PyErr: std::convert::From<>::Error>, { pub const fn generate(&self, name: &'static CStr, doc: &'static CStr) -> PyMethodDefType { PyMethodDefType::Getter(PyGetterDef { @@ -1390,42 +1388,21 @@ where } } -#[cfg(diagnostic_namespace)] -#[diagnostic::on_unimplemented( - message = "`{Self}` cannot be converted to a Python object", - label = "required by `#[pyo3(get)]` to create a readable property from a field of type `{Self}`", - note = "implement `IntoPyObject` for `&{Self}` or `IntoPyObject + Clone` for `{Self}` to define the conversion" +#[cfg_attr( + diagnostic_namespace, + diagnostic::on_unimplemented( + message = "`{Self}` cannot be converted to a Python object", + label = "required by `#[pyo3(get)]` to create a readable property from a field of type `{Self}`", + note = "implement `IntoPyObject` for `&{Self}` or `IntoPyObject + Clone` for `{Self}` to define the conversion" + ) )] -pub trait PyO3GetField<'py>: IntoPyObject<'py, Error: Into> + Clone {} - -#[cfg(diagnostic_namespace)] -impl<'py, T> PyO3GetField<'py> for T -where - T: IntoPyObject<'py> + Clone, - PyErr: std::convert::From<>::Error>, -{ -} +pub trait PyO3GetField<'py>: IntoPyObject<'py> + Clone {} +impl<'py, T> PyO3GetField<'py> for T where T: IntoPyObject<'py> + Clone {} /// Base case attempts to use IntoPyObject + Clone impl> PyClassGetterGenerator { - #[cfg(not(diagnostic_namespace))] - pub const fn generate(&self, _name: &'static CStr, _doc: &'static CStr) -> PyMethodDefType - // The bound goes here rather than on the block so that this impl is always available - // if no specialization is used instead - where - for<'py> FieldT: IntoPyObject<'py> + Clone, - for<'py> PyErr: std::convert::From<>::Error>, - { - // unreachable not allowed in const - panic!( - "exists purely to emit diagnostics on unimplemented traits. When `ToPyObject` \ - and `IntoPy` are fully removed this will be replaced by the temporary `IntoPyObject` case above." - ) - } - - #[cfg(diagnostic_namespace)] pub const fn generate(&self, _name: &'static CStr, _doc: &'static CStr) -> PyMethodDefType // The bound goes here rather than on the block so that this impl is always available // if no specialization is used instead @@ -1541,14 +1518,16 @@ where ClassT: PyClass, for<'a, 'py> &'a FieldT: IntoPyObject<'py>, Offset: OffsetCalculator, - for<'a, 'py> PyErr: From<<&'a FieldT as IntoPyObject<'py>>::Error>, { let _holder = unsafe { ensure_no_mutable_alias::(py, &obj)? }; let value = field_from_object::(obj); // SAFETY: Offset is known to describe the location of the value, and // _holder is preventing mutable aliasing - Ok((unsafe { &*value }).into_pyobject(py)?.into_ptr()) + Ok((unsafe { &*value }) + .into_pyobject(py) + .map_err(Into::into)? + .into_ptr()) } fn pyo3_get_value_into_pyobject( @@ -1559,7 +1538,6 @@ where ClassT: PyClass, for<'py> FieldT: IntoPyObject<'py> + Clone, Offset: OffsetCalculator, - for<'py> >::Error: Into, { let _holder = unsafe { ensure_no_mutable_alias::(py, &obj)? }; let value = field_from_object::(obj); diff --git a/src/impl_/wrap.rs b/src/impl_/wrap.rs index f32faaf5b2b..c999cf40249 100644 --- a/src/impl_/wrap.rs +++ b/src/impl_/wrap.rs @@ -1,8 +1,8 @@ use std::{convert::Infallible, marker::PhantomData, ops::Deref}; use crate::{ - conversion::IntoPyObject, ffi, types::PyNone, Bound, BoundObject, IntoPy, PyErr, PyObject, - PyResult, Python, + conversion::IntoPyObject, ffi, types::PyNone, Bound, BoundObject, IntoPy, PyObject, PyResult, + Python, }; /// Used to wrap values in `Option` for default arguments. @@ -95,7 +95,6 @@ impl<'py, T: IntoPyObject<'py>, E> IntoPyObjectConverter> { pub fn map_into_pyobject(&self, py: Python<'py>, obj: PyResult) -> PyResult where T: IntoPyObject<'py>, - PyErr: From, { obj.and_then(|obj| obj.into_pyobject(py).map_err(Into::into)) .map(BoundObject::into_any) @@ -106,7 +105,6 @@ impl<'py, T: IntoPyObject<'py>, E> IntoPyObjectConverter> { pub fn map_into_ptr(&self, py: Python<'py>, obj: PyResult) -> PyResult<*mut ffi::PyObject> where T: IntoPyObject<'py>, - PyErr: From, { obj.and_then(|obj| obj.into_pyobject(py).map_err(Into::into)) .map(BoundObject::into_bound) diff --git a/src/instance.rs b/src/instance.rs index d6b9a0fb1e9..2517ff12d6b 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -1412,7 +1412,6 @@ impl Py { pub fn getattr<'py, N>(&self, py: Python<'py>, attr_name: N) -> PyResult where N: IntoPyObject<'py, Target = PyString>, - N::Error: Into, { self.bind(py).as_any().getattr(attr_name).map(Bound::unbind) } @@ -1443,8 +1442,6 @@ impl Py { where N: IntoPyObject<'py, Target = PyString>, V: IntoPyObject<'py>, - N::Error: Into, - V::Error: Into, { self.bind(py).as_any().setattr(attr_name, value) } @@ -1497,7 +1494,6 @@ impl Py { where N: IntoPyObject<'py, Target = PyString>, A: IntoPy>, - N::Error: Into, { self.bind(py) .as_any() @@ -1515,7 +1511,6 @@ impl Py { where N: IntoPyObject<'py, Target = PyString>, A: IntoPy>, - N::Error: Into, { self.bind(py) .as_any() @@ -1532,7 +1527,6 @@ impl Py { pub fn call_method0<'py, N>(&self, py: Python<'py>, name: N) -> PyResult where N: IntoPyObject<'py, Target = PyString>, - N::Error: Into, { self.bind(py).as_any().call_method0(name).map(Bound::unbind) } diff --git a/src/types/any.rs b/src/types/any.rs index 801b651ac1a..f71973d4417 100644 --- a/src/types/any.rs +++ b/src/types/any.rs @@ -81,8 +81,7 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed { /// ``` fn hasattr(&self, attr_name: N) -> PyResult where - N: IntoPyObject<'py, Target = PyString>, - N::Error: Into; + N: IntoPyObject<'py, Target = PyString>; /// Retrieves an attribute value. /// @@ -108,8 +107,7 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed { /// ``` fn getattr(&self, attr_name: N) -> PyResult> where - N: IntoPyObject<'py, Target = PyString>, - N::Error: Into; + N: IntoPyObject<'py, Target = PyString>; /// Sets an attribute value. /// @@ -136,9 +134,7 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed { fn setattr(&self, attr_name: N, value: V) -> PyResult<()> where N: IntoPyObject<'py, Target = PyString>, - V: IntoPyObject<'py>, - N::Error: Into, - V::Error: Into; + V: IntoPyObject<'py>; /// Deletes an attribute. /// @@ -148,8 +144,7 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed { /// to intern `attr_name`. fn delattr(&self, attr_name: N) -> PyResult<()> where - N: IntoPyObject<'py, Target = PyString>, - N::Error: Into; + N: IntoPyObject<'py, Target = PyString>; /// Returns an [`Ordering`] between `self` and `other`. /// @@ -199,8 +194,7 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed { /// ``` fn compare(&self, other: O) -> PyResult where - O: IntoPyObject<'py>, - O::Error: Into; + O: IntoPyObject<'py>; /// Tests whether two Python objects obey a given [`CompareOp`]. /// @@ -238,8 +232,7 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed { /// ``` fn rich_compare(&self, other: O, compare_op: CompareOp) -> PyResult> where - O: IntoPyObject<'py>, - O::Error: Into; + O: IntoPyObject<'py>; /// Computes the negative of self. /// @@ -264,135 +257,114 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed { /// This is equivalent to the Python expression `self < other`. fn lt(&self, other: O) -> PyResult where - O: IntoPyObject<'py>, - O::Error: Into; + O: IntoPyObject<'py>; /// Tests whether this object is less than or equal to another. /// /// This is equivalent to the Python expression `self <= other`. fn le(&self, other: O) -> PyResult where - O: IntoPyObject<'py>, - O::Error: Into; + O: IntoPyObject<'py>; /// Tests whether this object is equal to another. /// /// This is equivalent to the Python expression `self == other`. fn eq(&self, other: O) -> PyResult where - O: IntoPyObject<'py>, - O::Error: Into; + O: IntoPyObject<'py>; /// Tests whether this object is not equal to another. /// /// This is equivalent to the Python expression `self != other`. fn ne(&self, other: O) -> PyResult where - O: IntoPyObject<'py>, - O::Error: Into; + O: IntoPyObject<'py>; /// Tests whether this object is greater than another. /// /// This is equivalent to the Python expression `self > other`. fn gt(&self, other: O) -> PyResult where - O: IntoPyObject<'py>, - O::Error: Into; + O: IntoPyObject<'py>; /// Tests whether this object is greater than or equal to another. /// /// This is equivalent to the Python expression `self >= other`. fn ge(&self, other: O) -> PyResult where - O: IntoPyObject<'py>, - O::Error: Into; + O: IntoPyObject<'py>; /// Computes `self + other`. fn add(&self, other: O) -> PyResult> where - O: IntoPyObject<'py>, - O::Error: Into; + O: IntoPyObject<'py>; /// Computes `self - other`. fn sub(&self, other: O) -> PyResult> where - O: IntoPyObject<'py>, - O::Error: Into; + O: IntoPyObject<'py>; /// Computes `self * other`. fn mul(&self, other: O) -> PyResult> where - O: IntoPyObject<'py>, - O::Error: Into; + O: IntoPyObject<'py>; /// Computes `self @ other`. fn matmul(&self, other: O) -> PyResult> where - O: IntoPyObject<'py>, - O::Error: Into; + O: IntoPyObject<'py>; /// Computes `self / other`. fn div(&self, other: O) -> PyResult> where - O: IntoPyObject<'py>, - O::Error: Into; + O: IntoPyObject<'py>; /// Computes `self // other`. fn floor_div(&self, other: O) -> PyResult> where - O: IntoPyObject<'py>, - O::Error: Into; + O: IntoPyObject<'py>; /// Computes `self % other`. fn rem(&self, other: O) -> PyResult> where - O: IntoPyObject<'py>, - O::Error: Into; + O: IntoPyObject<'py>; /// Computes `divmod(self, other)`. fn divmod(&self, other: O) -> PyResult> where - O: IntoPyObject<'py>, - O::Error: Into; + O: IntoPyObject<'py>; /// Computes `self << other`. fn lshift(&self, other: O) -> PyResult> where - O: IntoPyObject<'py>, - O::Error: Into; + O: IntoPyObject<'py>; /// Computes `self >> other`. fn rshift(&self, other: O) -> PyResult> where - O: IntoPyObject<'py>, - O::Error: Into; + O: IntoPyObject<'py>; /// Computes `self ** other % modulus` (`pow(self, other, modulus)`). /// `py.None()` may be passed for the `modulus`. fn pow(&self, other: O1, modulus: O2) -> PyResult> where O1: IntoPyObject<'py>, - O2: IntoPyObject<'py>, - O1::Error: Into, - O2::Error: Into; + O2: IntoPyObject<'py>; /// Computes `self & other`. fn bitand(&self, other: O) -> PyResult> where - O: IntoPyObject<'py>, - O::Error: Into; + O: IntoPyObject<'py>; /// Computes `self | other`. fn bitor(&self, other: O) -> PyResult> where - O: IntoPyObject<'py>, - O::Error: Into; + O: IntoPyObject<'py>; /// Computes `self ^ other`. fn bitxor(&self, other: O) -> PyResult> where - O: IntoPyObject<'py>, - O::Error: Into; + O: IntoPyObject<'py>; /// Determines whether this object appears callable. /// @@ -559,8 +531,7 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed { ) -> PyResult> where N: IntoPyObject<'py, Target = PyString>, - A: IntoPy>, - N::Error: Into; + A: IntoPy>; /// Calls a method on the object without arguments. /// @@ -597,8 +568,7 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed { /// ``` fn call_method0(&self, name: N) -> PyResult> where - N: IntoPyObject<'py, Target = PyString>, - N::Error: Into; + N: IntoPyObject<'py, Target = PyString>; /// Calls a method on the object with only positional arguments. /// @@ -637,8 +607,7 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed { fn call_method1(&self, name: N, args: A) -> PyResult> where N: IntoPyObject<'py, Target = PyString>, - A: IntoPy>, - N::Error: Into; + A: IntoPy>; /// Returns whether the object is considered to be true. /// @@ -666,8 +635,7 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed { /// This is equivalent to the Python expression `self[key]`. fn get_item(&self, key: K) -> PyResult> where - K: IntoPyObject<'py>, - K::Error: Into; + K: IntoPyObject<'py>; /// Sets a collection item value. /// @@ -675,17 +643,14 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed { fn set_item(&self, key: K, value: V) -> PyResult<()> where K: IntoPyObject<'py>, - V: IntoPyObject<'py>, - K::Error: Into, - V::Error: Into; + V: IntoPyObject<'py>; /// Deletes an item from the collection. /// /// This is equivalent to the Python expression `del self[key]`. fn del_item(&self, key: K) -> PyResult<()> where - K: IntoPyObject<'py>, - K::Error: Into; + K: IntoPyObject<'py>; /// Takes an object and returns an iterator for it. /// @@ -897,8 +862,7 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed { /// This is equivalent to the Python expression `value in self`. fn contains(&self, value: V) -> PyResult where - V: IntoPyObject<'py>, - V::Error: Into; + V: IntoPyObject<'py>; /// Return a proxy object that delegates method calls to a parent or sibling class of type. /// @@ -913,7 +877,6 @@ macro_rules! implement_binop { fn $name(&self, other: O) -> PyResult> where O: IntoPyObject<'py>, - O::Error: Into, { fn inner<'py>( any: &Bound<'py, PyAny>, @@ -944,7 +907,6 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { fn hasattr(&self, attr_name: N) -> PyResult where N: IntoPyObject<'py, Target = PyString>, - N::Error: Into, { // PyObject_HasAttr suppresses all exceptions, which was the behaviour of `hasattr` in Python 2. // Use an implementation which suppresses only AttributeError, which is consistent with `hasattr` in Python 3. @@ -962,7 +924,6 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { fn getattr(&self, attr_name: N) -> PyResult> where N: IntoPyObject<'py, Target = PyString>, - N::Error: Into, { fn inner<'py>( any: &Bound<'py, PyAny>, @@ -987,8 +948,6 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { where N: IntoPyObject<'py, Target = PyString>, V: IntoPyObject<'py>, - N::Error: Into, - V::Error: Into, { fn inner( any: &Bound<'_, PyAny>, @@ -1018,7 +977,6 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { fn delattr(&self, attr_name: N) -> PyResult<()> where N: IntoPyObject<'py, Target = PyString>, - N::Error: Into, { fn inner(any: &Bound<'_, PyAny>, attr_name: &Bound<'_, PyString>) -> PyResult<()> { err::error_on_minusone(any.py(), unsafe { @@ -1039,7 +997,6 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { fn compare(&self, other: O) -> PyResult where O: IntoPyObject<'py>, - O::Error: Into, { fn inner(any: &Bound<'_, PyAny>, other: &Bound<'_, PyAny>) -> PyResult { let other = other.as_ptr(); @@ -1077,7 +1034,6 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { fn rich_compare(&self, other: O, compare_op: CompareOp) -> PyResult> where O: IntoPyObject<'py>, - O::Error: Into, { fn inner<'py>( any: &Bound<'py, PyAny>, @@ -1133,7 +1089,6 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { fn lt(&self, other: O) -> PyResult where O: IntoPyObject<'py>, - O::Error: Into, { self.rich_compare(other, CompareOp::Lt) .and_then(|any| any.is_truthy()) @@ -1142,7 +1097,6 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { fn le(&self, other: O) -> PyResult where O: IntoPyObject<'py>, - O::Error: Into, { self.rich_compare(other, CompareOp::Le) .and_then(|any| any.is_truthy()) @@ -1151,7 +1105,6 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { fn eq(&self, other: O) -> PyResult where O: IntoPyObject<'py>, - O::Error: Into, { self.rich_compare(other, CompareOp::Eq) .and_then(|any| any.is_truthy()) @@ -1160,7 +1113,6 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { fn ne(&self, other: O) -> PyResult where O: IntoPyObject<'py>, - O::Error: Into, { self.rich_compare(other, CompareOp::Ne) .and_then(|any| any.is_truthy()) @@ -1169,7 +1121,6 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { fn gt(&self, other: O) -> PyResult where O: IntoPyObject<'py>, - O::Error: Into, { self.rich_compare(other, CompareOp::Gt) .and_then(|any| any.is_truthy()) @@ -1178,7 +1129,6 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { fn ge(&self, other: O) -> PyResult where O: IntoPyObject<'py>, - O::Error: Into, { self.rich_compare(other, CompareOp::Ge) .and_then(|any| any.is_truthy()) @@ -1201,7 +1151,6 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { fn divmod(&self, other: O) -> PyResult> where O: IntoPyObject<'py>, - O::Error: Into, { fn inner<'py>( any: &Bound<'py, PyAny>, @@ -1229,8 +1178,6 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { where O1: IntoPyObject<'py>, O2: IntoPyObject<'py>, - O1::Error: Into, - O2::Error: Into, { fn inner<'py>( any: &Bound<'py, PyAny>, @@ -1297,7 +1244,6 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { where N: IntoPyObject<'py, Target = PyString>, A: IntoPy>, - N::Error: Into, { // Don't `args.into_py()`! This will lose the optimization of vectorcall. match kwargs { @@ -1312,7 +1258,6 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { fn call_method0(&self, name: N) -> PyResult> where N: IntoPyObject<'py, Target = PyString>, - N::Error: Into, { let py = self.py(); let name = name.into_pyobject(py).map_err(Into::into)?.into_bound(); @@ -1326,7 +1271,6 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { where N: IntoPyObject<'py, Target = PyString>, A: IntoPy>, - N::Error: Into, { args.__py_call_method_vectorcall1( self.py(), @@ -1360,7 +1304,6 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { fn get_item(&self, key: K) -> PyResult> where K: IntoPyObject<'py>, - K::Error: Into, { fn inner<'py>( any: &Bound<'py, PyAny>, @@ -1385,8 +1328,6 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { where K: IntoPyObject<'py>, V: IntoPyObject<'py>, - K::Error: Into, - V::Error: Into, { fn inner( any: &Bound<'_, PyAny>, @@ -1416,7 +1357,6 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { fn del_item(&self, key: K) -> PyResult<()> where K: IntoPyObject<'py>, - K::Error: Into, { fn inner(any: &Bound<'_, PyAny>, key: &Bound<'_, PyAny>) -> PyResult<()> { err::error_on_minusone(any.py(), unsafe { @@ -1581,7 +1521,6 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { fn contains(&self, value: V) -> PyResult where V: IntoPyObject<'py>, - V::Error: Into, { fn inner(any: &Bound<'_, PyAny>, value: &Bound<'_, PyAny>) -> PyResult { match unsafe { ffi::PySequence_Contains(any.as_ptr(), value.as_ptr()) } { @@ -1623,7 +1562,6 @@ impl<'py> Bound<'py, PyAny> { pub(crate) fn lookup_special(&self, attr_name: N) -> PyResult>> where N: IntoPyObject<'py, Target = PyString>, - N::Error: Into, { let py = self.py(); let self_type = self.get_type(); diff --git a/src/types/mapping.rs b/src/types/mapping.rs index 609bd80295a..009c5c0e5e2 100644 --- a/src/types/mapping.rs +++ b/src/types/mapping.rs @@ -7,7 +7,7 @@ use crate::sync::GILOnceCell; use crate::type_object::PyTypeInfo; use crate::types::any::PyAnyMethods; use crate::types::{PyAny, PyDict, PySequence, PyType}; -use crate::{ffi, Py, PyErr, PyTypeCheck, Python}; +use crate::{ffi, Py, PyTypeCheck, Python}; /// Represents a reference to a Python object supporting the mapping protocol. /// @@ -51,8 +51,7 @@ pub trait PyMappingMethods<'py>: crate::sealed::Sealed { /// This is equivalent to the Python expression `key in self`. fn contains(&self, key: K) -> PyResult where - K: IntoPyObject<'py>, - K::Error: Into; + K: IntoPyObject<'py>; /// Gets the item in self with key `key`. /// @@ -61,8 +60,7 @@ pub trait PyMappingMethods<'py>: crate::sealed::Sealed { /// This is equivalent to the Python expression `self[key]`. fn get_item(&self, key: K) -> PyResult> where - K: IntoPyObject<'py>, - K::Error: Into; + K: IntoPyObject<'py>; /// Sets the item in self with key `key`. /// @@ -70,17 +68,14 @@ pub trait PyMappingMethods<'py>: crate::sealed::Sealed { fn set_item(&self, key: K, value: V) -> PyResult<()> where K: IntoPyObject<'py>, - V: IntoPyObject<'py>, - K::Error: Into, - V::Error: Into; + V: IntoPyObject<'py>; /// Deletes the item with key `key`. /// /// This is equivalent to the Python statement `del self[key]`. fn del_item(&self, key: K) -> PyResult<()> where - K: IntoPyObject<'py>, - K::Error: Into; + K: IntoPyObject<'py>; /// Returns a sequence containing all keys in the mapping. fn keys(&self) -> PyResult>; @@ -108,7 +103,6 @@ impl<'py> PyMappingMethods<'py> for Bound<'py, PyMapping> { fn contains(&self, key: K) -> PyResult where K: IntoPyObject<'py>, - K::Error: Into, { PyAnyMethods::contains(&**self, key) } @@ -117,7 +111,6 @@ impl<'py> PyMappingMethods<'py> for Bound<'py, PyMapping> { fn get_item(&self, key: K) -> PyResult> where K: IntoPyObject<'py>, - K::Error: Into, { PyAnyMethods::get_item(&**self, key) } @@ -127,8 +120,6 @@ impl<'py> PyMappingMethods<'py> for Bound<'py, PyMapping> { where K: IntoPyObject<'py>, V: IntoPyObject<'py>, - K::Error: Into, - V::Error: Into, { PyAnyMethods::set_item(&**self, key, value) } @@ -137,7 +128,6 @@ impl<'py> PyMappingMethods<'py> for Bound<'py, PyMapping> { fn del_item(&self, key: K) -> PyResult<()> where K: IntoPyObject<'py>, - K::Error: Into, { PyAnyMethods::del_item(&**self, key) } diff --git a/src/types/tuple.rs b/src/types/tuple.rs index 832cf85d2fc..d54bdf79f8d 100644 --- a/src/types/tuple.rs +++ b/src/types/tuple.rs @@ -532,14 +532,13 @@ macro_rules! tuple_conversion ({$length:expr,$(($refN:ident, $n:tt, $T:ident)),+ impl <'py, $($T),+> IntoPyObject<'py> for ($($T,)+) where $($T: IntoPyObject<'py>,)+ - PyErr: $(From<$T::Error> + )+ { type Target = PyTuple; type Output = Bound<'py, Self::Target>; type Error = PyErr; fn into_pyobject(self, py: Python<'py>) -> Result { - Ok(array_into_tuple(py, [$(self.$n.into_pyobject(py)?.into_any().unbind()),+]).into_bound(py)) + Ok(array_into_tuple(py, [$(self.$n.into_pyobject(py).map_err(Into::into)?.into_any().unbind()),+]).into_bound(py)) } } From 15e00ba7e88e9cc3c7f3c7f1d120d4581f803159 Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Sun, 25 Aug 2024 19:48:40 +0200 Subject: [PATCH 248/495] reintroduce `PySet` constructors (#4492) --- src/conversions/hashbrown.rs | 2 +- src/conversions/std/set.rs | 4 +-- src/types/set.rs | 47 ++++++++++++++++++++++++------------ 3 files changed, 35 insertions(+), 18 deletions(-) diff --git a/src/conversions/hashbrown.rs b/src/conversions/hashbrown.rs index 84c9e888ca9..e6ebb35232e 100644 --- a/src/conversions/hashbrown.rs +++ b/src/conversions/hashbrown.rs @@ -272,7 +272,7 @@ mod tests { #[test] fn test_extract_hashbrown_hashset() { Python::with_gil(|py| { - let set = PySet::new_bound(py, &[1, 2, 3, 4, 5]).unwrap(); + let set = PySet::new(py, &[1, 2, 3, 4, 5]).unwrap(); let hash_set: hashbrown::HashSet = set.extract().unwrap(); assert_eq!(hash_set, [1, 2, 3, 4, 5].iter().copied().collect()); diff --git a/src/conversions/std/set.rs b/src/conversions/std/set.rs index f0ea51f59ac..c9738069080 100644 --- a/src/conversions/std/set.rs +++ b/src/conversions/std/set.rs @@ -212,7 +212,7 @@ mod tests { #[test] fn test_extract_hashset() { Python::with_gil(|py| { - let set = PySet::new_bound(py, &[1, 2, 3, 4, 5]).unwrap(); + let set = PySet::new(py, &[1, 2, 3, 4, 5]).unwrap(); let hash_set: HashSet = set.extract().unwrap(); assert_eq!(hash_set, [1, 2, 3, 4, 5].iter().copied().collect()); @@ -225,7 +225,7 @@ mod tests { #[test] fn test_extract_btreeset() { Python::with_gil(|py| { - let set = PySet::new_bound(py, &[1, 2, 3, 4, 5]).unwrap(); + let set = PySet::new(py, &[1, 2, 3, 4, 5]).unwrap(); let hash_set: BTreeSet = set.extract().unwrap(); assert_eq!(hash_set, [1, 2, 3, 4, 5].iter().copied().collect()); diff --git a/src/types/set.rs b/src/types/set.rs index fd65d4bcaa4..8292c199d7d 100644 --- a/src/types/set.rs +++ b/src/types/set.rs @@ -39,21 +39,38 @@ impl PySet { /// /// Returns an error if some element is not hashable. #[inline] - pub fn new_bound<'a, 'p, T: ToPyObject + 'a>( + pub fn new<'a, 'p, T: ToPyObject + 'a>( py: Python<'p>, elements: impl IntoIterator, ) -> PyResult> { new_from_iter(py, elements) } + /// Deprecated name for [`PySet::new`]. + #[deprecated(since = "0.23.0", note = "renamed to `PySet::new`")] + #[inline] + pub fn new_bound<'a, 'p, T: ToPyObject + 'a>( + py: Python<'p>, + elements: impl IntoIterator, + ) -> PyResult> { + Self::new(py, elements) + } + /// Creates a new empty set. - pub fn empty_bound(py: Python<'_>) -> PyResult> { + pub fn empty(py: Python<'_>) -> PyResult> { unsafe { ffi::PySet_New(ptr::null_mut()) .assume_owned_or_err(py) .downcast_into_unchecked() } } + + /// Deprecated name for [`PySet::empty`]. + #[deprecated(since = "0.23.0", note = "renamed to `PySet::empty`")] + #[inline] + pub fn empty_bound(py: Python<'_>) -> PyResult> { + Self::empty(py) + } } /// Implementation of functionality for [`PySet`]. @@ -286,18 +303,18 @@ mod tests { #[test] fn test_set_new() { Python::with_gil(|py| { - let set = PySet::new_bound(py, &[1]).unwrap(); + let set = PySet::new(py, &[1]).unwrap(); assert_eq!(1, set.len()); let v = vec![1]; - assert!(PySet::new_bound(py, &[v]).is_err()); + assert!(PySet::new(py, &[v]).is_err()); }); } #[test] fn test_set_empty() { Python::with_gil(|py| { - let set = PySet::empty_bound(py).unwrap(); + let set = PySet::empty(py).unwrap(); assert_eq!(0, set.len()); assert!(set.is_empty()); }); @@ -320,7 +337,7 @@ mod tests { #[test] fn test_set_clear() { Python::with_gil(|py| { - let set = PySet::new_bound(py, &[1]).unwrap(); + let set = PySet::new(py, &[1]).unwrap(); assert_eq!(1, set.len()); set.clear(); assert_eq!(0, set.len()); @@ -330,7 +347,7 @@ mod tests { #[test] fn test_set_contains() { Python::with_gil(|py| { - let set = PySet::new_bound(py, &[1]).unwrap(); + let set = PySet::new(py, &[1]).unwrap(); assert!(set.contains(1).unwrap()); }); } @@ -338,7 +355,7 @@ mod tests { #[test] fn test_set_discard() { Python::with_gil(|py| { - let set = PySet::new_bound(py, &[1]).unwrap(); + let set = PySet::new(py, &[1]).unwrap(); assert!(!set.discard(2).unwrap()); assert_eq!(1, set.len()); @@ -353,7 +370,7 @@ mod tests { #[test] fn test_set_add() { Python::with_gil(|py| { - let set = PySet::new_bound(py, &[1, 2]).unwrap(); + let set = PySet::new(py, &[1, 2]).unwrap(); set.add(1).unwrap(); // Add a dupliated element assert!(set.contains(1).unwrap()); }); @@ -362,7 +379,7 @@ mod tests { #[test] fn test_set_pop() { Python::with_gil(|py| { - let set = PySet::new_bound(py, &[1]).unwrap(); + let set = PySet::new(py, &[1]).unwrap(); let val = set.pop(); assert!(val.is_some()); let val2 = set.pop(); @@ -380,7 +397,7 @@ mod tests { #[test] fn test_set_iter() { Python::with_gil(|py| { - let set = PySet::new_bound(py, &[1]).unwrap(); + let set = PySet::new(py, &[1]).unwrap(); for el in set { assert_eq!(1i32, el.extract::<'_, i32>().unwrap()); @@ -393,7 +410,7 @@ mod tests { use crate::types::any::PyAnyMethods; Python::with_gil(|py| { - let set = PySet::new_bound(py, &[1]).unwrap(); + let set = PySet::new(py, &[1]).unwrap(); for el in &set { assert_eq!(1i32, el.extract::().unwrap()); @@ -405,7 +422,7 @@ mod tests { #[should_panic] fn test_set_iter_mutation() { Python::with_gil(|py| { - let set = PySet::new_bound(py, &[1, 2, 3, 4, 5]).unwrap(); + let set = PySet::new(py, &[1, 2, 3, 4, 5]).unwrap(); for _ in &set { let _ = set.add(42); @@ -417,7 +434,7 @@ mod tests { #[should_panic] fn test_set_iter_mutation_same_len() { Python::with_gil(|py| { - let set = PySet::new_bound(py, &[1, 2, 3, 4, 5]).unwrap(); + let set = PySet::new(py, &[1, 2, 3, 4, 5]).unwrap(); for item in &set { let item: i32 = item.extract().unwrap(); @@ -430,7 +447,7 @@ mod tests { #[test] fn test_set_iter_size_hint() { Python::with_gil(|py| { - let set = PySet::new_bound(py, &[1]).unwrap(); + let set = PySet::new(py, &[1]).unwrap(); let mut iter = set.iter(); // Exact size From 82fa359d96732f90a210b3d6d879e17e0fe0aa2c Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Mon, 26 Aug 2024 06:35:12 +0100 Subject: [PATCH 249/495] fixup `Py_NotImplemented` in 3.13 abi3 (#4494) --- pyo3-ffi/src/object.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyo3-ffi/src/object.rs b/pyo3-ffi/src/object.rs index deb67caf768..fc3484be102 100644 --- a/pyo3-ffi/src/object.rs +++ b/pyo3-ffi/src/object.rs @@ -871,7 +871,7 @@ extern "C" { #[inline] pub unsafe fn Py_NotImplemented() -> *mut PyObject { #[cfg(all(not(GraalPy), all(Py_3_13, Py_LIMITED_API)))] - return Py_GetConstantBorrowed(Py_CONSTANT_NONE); + return Py_GetConstantBorrowed(Py_CONSTANT_NOT_IMPLEMENTED); #[cfg(all(not(GraalPy), not(all(Py_3_13, Py_LIMITED_API))))] return ptr::addr_of_mut!(_Py_NotImplementedStruct); From 4689a35101547668d27d98cd2efad17de958b89c Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Wed, 28 Aug 2024 22:37:18 +0100 Subject: [PATCH 250/495] avoid creating `PyRef` inside `__traverse__` handler (#4479) --- Cargo.toml | 1 + newsfragments/4479.fixed.md | 1 + src/impl_/pymethods.rs | 81 +++++++++++++++++++++++++++++++------ src/pycell.rs | 8 ---- src/pyclass.rs | 1 + src/pyclass/gc.rs | 71 ++++++++++++++++++++++---------- 6 files changed, 120 insertions(+), 43 deletions(-) create mode 100644 newsfragments/4479.fixed.md diff --git a/Cargo.toml b/Cargo.toml index d276dd80350..516b977a6c2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -62,6 +62,7 @@ serde_json = "1.0.61" rayon = "1.6.1" futures = "0.3.28" tempfile = "3.12.0" +static_assertions = "1.1.0" [build-dependencies] pyo3-build-config = { path = "pyo3-build-config", version = "=0.23.0-dev", features = ["resolve-config"] } diff --git a/newsfragments/4479.fixed.md b/newsfragments/4479.fixed.md new file mode 100644 index 00000000000..15d634543af --- /dev/null +++ b/newsfragments/4479.fixed.md @@ -0,0 +1 @@ +Remove illegal reference counting op inside implementation of `__traverse__` handlers. diff --git a/src/impl_/pymethods.rs b/src/impl_/pymethods.rs index b150f474c72..300af22db3a 100644 --- a/src/impl_/pymethods.rs +++ b/src/impl_/pymethods.rs @@ -2,15 +2,18 @@ use crate::callback::IntoPyCallbackOutput; use crate::exceptions::PyStopAsyncIteration; use crate::gil::LockGIL; use crate::impl_::panic::PanicTrap; +use crate::impl_::pycell::{PyClassObject, PyClassObjectLayout}; +use crate::pycell::impl_::PyClassBorrowChecker as _; use crate::pycell::{PyBorrowError, PyBorrowMutError}; use crate::pyclass::boolean_struct::False; use crate::types::any::PyAnyMethods; use crate::{ - ffi, Borrowed, Bound, DowncastError, Py, PyAny, PyClass, PyClassInitializer, PyErr, PyObject, - PyRef, PyRefMut, PyResult, PyTraverseError, PyTypeCheck, PyVisit, Python, + ffi, Bound, DowncastError, Py, PyAny, PyClass, PyClassInitializer, PyErr, PyObject, PyRef, + PyRefMut, PyResult, PyTraverseError, PyTypeCheck, PyVisit, Python, }; use std::ffi::CStr; use std::fmt; +use std::marker::PhantomData; use std::os::raw::{c_int, c_void}; use std::panic::{catch_unwind, AssertUnwindSafe}; use std::ptr::null_mut; @@ -232,6 +235,40 @@ impl PySetterDef { } /// Calls an implementation of __traverse__ for tp_traverse +/// +/// NB cannot accept `'static` visitor, this is a sanity check below: +/// +/// ```rust,compile_fail +/// use pyo3::prelude::*; +/// use pyo3::pyclass::{PyTraverseError, PyVisit}; +/// +/// #[pyclass] +/// struct Foo; +/// +/// #[pymethods] +/// impl Foo { +/// fn __traverse__(&self, _visit: PyVisit<'static>) -> Result<(), PyTraverseError> { +/// Ok(()) +/// } +/// } +/// ``` +/// +/// Elided lifetime should compile ok: +/// +/// ```rust +/// use pyo3::prelude::*; +/// use pyo3::pyclass::{PyTraverseError, PyVisit}; +/// +/// #[pyclass] +/// struct Foo; +/// +/// #[pymethods] +/// impl Foo { +/// fn __traverse__(&self, _visit: PyVisit<'_>) -> Result<(), PyTraverseError> { +/// Ok(()) +/// } +/// } +/// ``` #[doc(hidden)] pub unsafe fn _call_traverse( slf: *mut ffi::PyObject, @@ -250,25 +287,43 @@ where // Since we do not create a `GILPool` at all, it is important that our usage of the GIL // token does not produce any owned objects thereby calling into `register_owned`. let trap = PanicTrap::new("uncaught panic inside __traverse__ handler"); + let lock = LockGIL::during_traverse(); + + // SAFETY: `slf` is a valid Python object pointer to a class object of type T, and + // traversal is running so no mutations can occur. + let class_object: &PyClassObject = &*slf.cast(); + + let retval = + // `#[pyclass(unsendable)]` types can only be deallocated by their own thread, so + // do not traverse them if not on their owning thread :( + if class_object.check_threadsafe().is_ok() + // ... and we cannot traverse a type which might be being mutated by a Rust thread + && class_object.borrow_checker().try_borrow().is_ok() { + struct TraverseGuard<'a, T: PyClass>(&'a PyClassObject); + impl<'a, T: PyClass> Drop for TraverseGuard<'a, T> { + fn drop(&mut self) { + self.0.borrow_checker().release_borrow() + } + } - let py = Python::assume_gil_acquired(); - let slf = Borrowed::from_ptr_unchecked(py, slf).downcast_unchecked::(); - let borrow = PyRef::try_borrow_threadsafe(&slf); - let visit = PyVisit::from_raw(visit, arg, py); + // `.try_borrow()` above created a borrow, we need to release it when we're done + // traversing the object. This allows us to read `instance` safely. + let _guard = TraverseGuard(class_object); + let instance = &*class_object.contents.value.get(); - let retval = if let Ok(borrow) = borrow { - let _lock = LockGIL::during_traverse(); + let visit = PyVisit { visit, arg, _guard: PhantomData }; - match catch_unwind(AssertUnwindSafe(move || impl_(&*borrow, visit))) { - Ok(res) => match res { - Ok(()) => 0, - Err(PyTraverseError(value)) => value, - }, + match catch_unwind(AssertUnwindSafe(move || impl_(instance, visit))) { + Ok(Ok(())) => 0, + Ok(Err(traverse_error)) => traverse_error.into_inner(), Err(_err) => -1, } } else { 0 }; + + // Drop lock before trap just in case dropping lock panics + drop(lock); trap.disarm(); retval } diff --git a/src/pycell.rs b/src/pycell.rs index dc8b8b495a5..438f7245a22 100644 --- a/src/pycell.rs +++ b/src/pycell.rs @@ -312,14 +312,6 @@ impl<'py, T: PyClass> PyRef<'py, T> { .try_borrow() .map(|_| Self { inner: obj.clone() }) } - - pub(crate) fn try_borrow_threadsafe(obj: &Bound<'py, T>) -> Result { - let cell = obj.get_class_object(); - cell.check_threadsafe()?; - cell.borrow_checker() - .try_borrow() - .map(|_| Self { inner: obj.clone() }) - } } impl<'p, T, U> PyRef<'p, T> diff --git a/src/pyclass.rs b/src/pyclass.rs index 4e409566af5..0315ae5e8e8 100644 --- a/src/pyclass.rs +++ b/src/pyclass.rs @@ -6,6 +6,7 @@ mod create_type_object; mod gc; pub(crate) use self::create_type_object::{create_type_object, PyClassTypeObject}; + pub use self::gc::{PyTraverseError, PyVisit}; /// Types that can be used as Python classes. diff --git a/src/pyclass/gc.rs b/src/pyclass/gc.rs index 7878ccf5ca8..b6747a63f89 100644 --- a/src/pyclass/gc.rs +++ b/src/pyclass/gc.rs @@ -1,23 +1,31 @@ -use std::os::raw::{c_int, c_void}; +use std::{ + marker::PhantomData, + os::raw::{c_int, c_void}, +}; -use crate::{ffi, AsPyPointer, Python}; +use crate::{ffi, AsPyPointer}; /// Error returned by a `__traverse__` visitor implementation. #[repr(transparent)] -pub struct PyTraverseError(pub(crate) c_int); +pub struct PyTraverseError(NonZeroCInt); + +impl PyTraverseError { + /// Returns the error code. + pub(crate) fn into_inner(self) -> c_int { + self.0.into() + } +} /// Object visitor for GC. #[derive(Clone)] -pub struct PyVisit<'p> { +pub struct PyVisit<'a> { pub(crate) visit: ffi::visitproc, pub(crate) arg: *mut c_void, - /// VisitProc contains a Python instance to ensure that - /// 1) it is cannot be moved out of the traverse() call - /// 2) it cannot be sent to other threads - pub(crate) _py: Python<'p>, + /// Prevents the `PyVisit` from outliving the `__traverse__` call. + pub(crate) _guard: PhantomData<&'a ()>, } -impl<'p> PyVisit<'p> { +impl<'a> PyVisit<'a> { /// Visit `obj`. pub fn call(&self, obj: &T) -> Result<(), PyTraverseError> where @@ -25,24 +33,43 @@ impl<'p> PyVisit<'p> { { let ptr = obj.as_ptr(); if !ptr.is_null() { - let r = unsafe { (self.visit)(ptr, self.arg) }; - if r == 0 { - Ok(()) - } else { - Err(PyTraverseError(r)) + match NonZeroCInt::new(unsafe { (self.visit)(ptr, self.arg) }) { + None => Ok(()), + Some(r) => Err(PyTraverseError(r)), } } else { Ok(()) } } +} - /// Creates the PyVisit from the arguments to tp_traverse - #[doc(hidden)] - pub unsafe fn from_raw(visit: ffi::visitproc, arg: *mut c_void, py: Python<'p>) -> Self { - Self { - visit, - arg, - _py: py, - } +/// Workaround for `NonZero` not being available until MSRV 1.79 +mod get_nonzero_c_int { + pub struct GetNonZeroCInt(); + + pub trait NonZeroCIntType { + type Type; + } + impl NonZeroCIntType for GetNonZeroCInt<16> { + type Type = std::num::NonZeroI16; + } + impl NonZeroCIntType for GetNonZeroCInt<32> { + type Type = std::num::NonZeroI32; + } + + pub type Type = + () * 8 }> as NonZeroCIntType>::Type; +} + +use get_nonzero_c_int::Type as NonZeroCInt; + +#[cfg(test)] +mod tests { + use super::PyVisit; + use static_assertions::assert_not_impl_any; + + #[test] + fn py_visit_not_send_sync() { + assert_not_impl_any!(PyVisit<'_>: Send, Sync); } } From 5d47fc67efce4ff60235dd77639d097f83f3e66a Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Thu, 29 Aug 2024 06:24:12 +0100 Subject: [PATCH 251/495] make `abi3` feature apply to config imported from `PYO3_BUILD_CONFIG` (#4497) * make `abi3` feature apply to config imported from `PYO3_BUILD_CONFIG` * fix pytests for abi3 feature --- newsfragments/4497.changed.md | 1 + pyo3-build-config/build.rs | 28 ++++++---------------------- pyo3-build-config/src/impl_.rs | 29 +++++++++++++++++++++++++++++ pytests/src/pyclasses.rs | 3 +++ pytests/tests/test_pyclasses.py | 7 ++++++- 5 files changed, 45 insertions(+), 23 deletions(-) create mode 100644 newsfragments/4497.changed.md diff --git a/newsfragments/4497.changed.md b/newsfragments/4497.changed.md new file mode 100644 index 00000000000..594b6d373e5 --- /dev/null +++ b/newsfragments/4497.changed.md @@ -0,0 +1 @@ +The `abi3` feature will now override config files provided via `PYO3_BUILD_CONFIG`. diff --git a/pyo3-build-config/build.rs b/pyo3-build-config/build.rs index 309a78c87da..a6e767edcf0 100644 --- a/pyo3-build-config/build.rs +++ b/pyo3-build-config/build.rs @@ -12,7 +12,7 @@ mod errors; use std::{env, path::Path}; use errors::{Context, Result}; -use impl_::{env_var, make_interpreter_config, InterpreterConfig}; +use impl_::{make_interpreter_config, InterpreterConfig}; fn configure(interpreter_config: Option, name: &str) -> Result { let target = Path::new(&env::var_os("OUT_DIR").unwrap()).join(name); @@ -29,28 +29,12 @@ fn configure(interpreter_config: Option, name: &str) -> Resul } } -/// If PYO3_CONFIG_FILE is set, copy it into the crate. -fn config_file() -> Result> { - if let Some(path) = env_var("PYO3_CONFIG_FILE") { - let path = Path::new(&path); - println!("cargo:rerun-if-changed={}", path.display()); - // Absolute path is necessary because this build script is run with a cwd different to the - // original `cargo build` instruction. - ensure!( - path.is_absolute(), - "PYO3_CONFIG_FILE must be an absolute path" - ); - - let interpreter_config = InterpreterConfig::from_path(path) - .context("failed to parse contents of PYO3_CONFIG_FILE")?; - Ok(Some(interpreter_config)) - } else { - Ok(None) - } -} - fn generate_build_configs() -> Result<()> { - let configured = configure(config_file()?, "pyo3-build-config-file.txt")?; + // If PYO3_CONFIG_FILE is set, copy it into the crate. + let configured = configure( + InterpreterConfig::from_pyo3_config_file_env().transpose()?, + "pyo3-build-config-file.txt", + )?; if configured { // Don't bother trying to find an interpreter on the host system diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index c8f68864727..e254a26cd27 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -402,6 +402,35 @@ print("ext_suffix", get_config_var("EXT_SUFFIX")) }) } + /// Import an externally-provided config file. + /// + /// The `abi3` features, if set, may apply an `abi3` constraint to the Python version. + #[allow(dead_code)] // only used in build.rs + pub(super) fn from_pyo3_config_file_env() -> Option> { + cargo_env_var("PYO3_CONFIG_FILE").map(|path| { + let path = Path::new(&path); + println!("cargo:rerun-if-changed={}", path.display()); + // Absolute path is necessary because this build script is run with a cwd different to the + // original `cargo build` instruction. + ensure!( + path.is_absolute(), + "PYO3_CONFIG_FILE must be an absolute path" + ); + + let mut config = InterpreterConfig::from_path(path) + .context("failed to parse contents of PYO3_CONFIG_FILE")?; + // If the abi3 feature is enabled, the minimum Python version is constrained by the abi3 + // feature. + // + // TODO: abi3 is a property of the build mode, not the interpreter. Should this be + // removed from `InterpreterConfig`? + config.abi3 |= is_abi3(); + config.fixup_for_abi3_version(get_abi3_version())?; + + Ok(config) + }) + } + #[doc(hidden)] pub fn from_path(path: impl AsRef) -> Result { let path = path.as_ref(); diff --git a/pytests/src/pyclasses.rs b/pytests/src/pyclasses.rs index f7e4681af70..e499b436395 100644 --- a/pytests/src/pyclasses.rs +++ b/pytests/src/pyclasses.rs @@ -66,9 +66,11 @@ impl AssertingBaseClass { #[pyclass] struct ClassWithoutConstructor; +#[cfg(any(Py_3_10, not(Py_LIMITED_API)))] #[pyclass(dict)] struct ClassWithDict; +#[cfg(any(Py_3_10, not(Py_LIMITED_API)))] #[pymethods] impl ClassWithDict { #[new] @@ -83,6 +85,7 @@ pub fn pyclasses(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; + #[cfg(any(Py_3_10, not(Py_LIMITED_API)))] m.add_class::()?; Ok(()) diff --git a/pytests/tests/test_pyclasses.py b/pytests/tests/test_pyclasses.py index e91e75fa58a..a1424fc75aa 100644 --- a/pytests/tests/test_pyclasses.py +++ b/pytests/tests/test_pyclasses.py @@ -89,7 +89,12 @@ def test_no_constructor_defined_propagates_cause(cls: Type): def test_dict(): - d = pyclasses.ClassWithDict() + try: + ClassWithDict = pyclasses.ClassWithDict + except AttributeError: + pytest.skip("not defined using abi3 < 3.9") + + d = ClassWithDict() assert d.__dict__ == {} d.foo = 42 From c77b8536f6891c7c89b742d5dd18cf7e621ca3ed Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Thu, 29 Aug 2024 15:40:55 -0600 Subject: [PATCH 252/495] use unique module names in unit and integration tests (#4501) --- Cargo.toml | 1 + src/conversions/num_bigint.rs | 9 ++++++++- src/conversions/num_complex.rs | 9 +++++---- src/instance.rs | 7 +++++-- src/tests/common.rs | 7 +++++++ src/types/any.rs | 5 +++-- src/types/typeobject.rs | 19 +++++++++++++------ tests/test_module.rs | 2 +- 8 files changed, 43 insertions(+), 16 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 516b977a6c2..0259b1bae9e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -63,6 +63,7 @@ rayon = "1.6.1" futures = "0.3.28" tempfile = "3.12.0" static_assertions = "1.1.0" +uuid = {version = "1.10.0", features = ["v4"] } [build-dependencies] pyo3-build-config = { path = "pyo3-build-config", version = "=0.23.0-dev", features = ["resolve-config"] } diff --git a/src/conversions/num_bigint.rs b/src/conversions/num_bigint.rs index 3c699868c52..e05a807bb2c 100644 --- a/src/conversions/num_bigint.rs +++ b/src/conversions/num_bigint.rs @@ -337,6 +337,7 @@ fn int_n_bits(long: &Bound<'_, PyInt>) -> PyResult { #[cfg(test)] mod tests { use super::*; + use crate::tests::common::generate_unique_module_name; use crate::types::{PyDict, PyModule}; use indoc::indoc; use pyo3_ffi::c_str; @@ -409,7 +410,13 @@ mod tests { return self.x "# )); - PyModule::from_code(py, index_code, c_str!("index.py"), c_str!("index")).unwrap() + PyModule::from_code( + py, + index_code, + c_str!("index.py"), + &generate_unique_module_name("index"), + ) + .unwrap() } #[test] diff --git a/src/conversions/num_complex.rs b/src/conversions/num_complex.rs index ee6a3d46ec7..bf28aa73932 100644 --- a/src/conversions/num_complex.rs +++ b/src/conversions/num_complex.rs @@ -216,6 +216,7 @@ complex_conversion!(f64); #[cfg(test)] mod tests { use super::*; + use crate::tests::common::generate_unique_module_name; use crate::types::{complex::PyComplexMethods, PyModule}; use pyo3_ffi::c_str; @@ -259,7 +260,7 @@ class C: "# ), c_str!("test.py"), - c_str!("test"), + &generate_unique_module_name("test"), ) .unwrap(); let from_complex = module.getattr("A").unwrap().call0().unwrap(); @@ -303,7 +304,7 @@ class C(First, IndexMixin): pass "# ), c_str!("test.py"), - c_str!("test"), + &generate_unique_module_name("test"), ) .unwrap(); let from_complex = module.getattr("A").unwrap().call0().unwrap(); @@ -343,7 +344,7 @@ class A: "# ), c_str!("test.py"), - c_str!("test"), + &generate_unique_module_name("test"), ) .unwrap(); let obj = module.getattr("A").unwrap().call0().unwrap(); @@ -368,7 +369,7 @@ class A: "# ), c_str!("test.py"), - c_str!("test"), + &generate_unique_module_name("test"), ) .unwrap(); let obj = module.getattr("A").unwrap().call0().unwrap(); diff --git a/src/instance.rs b/src/instance.rs index 2517ff12d6b..b0a4a38d176 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -1887,6 +1887,7 @@ impl PyObject { #[cfg(test)] mod tests { use super::{Bound, Py, PyObject}; + use crate::tests::common::generate_unique_module_name; use crate::types::{dict::IntoPyDict, PyAnyMethods, PyCapsule, PyDict, PyString}; use crate::{ffi, Borrowed, PyAny, PyResult, Python, ToPyObject}; use pyo3_ffi::c_str; @@ -1962,7 +1963,8 @@ class A: a = A() "# ); - let module = PyModule::from_code(py, CODE, c_str!(""), c_str!(""))?; + let module = + PyModule::from_code(py, CODE, c_str!(""), &generate_unique_module_name(""))?; let instance: Py = module.getattr("a")?.into(); instance.getattr(py, "foo").unwrap_err(); @@ -1991,7 +1993,8 @@ class A: a = A() "# ); - let module = PyModule::from_code(py, CODE, c_str!(""), c_str!(""))?; + let module = + PyModule::from_code(py, CODE, c_str!(""), &generate_unique_module_name(""))?; let instance: Py = module.getattr("a")?.into(); let foo = crate::intern!(py, "foo"); diff --git a/src/tests/common.rs b/src/tests/common.rs index 8af20a2c2fa..efe0d4c89f1 100644 --- a/src/tests/common.rs +++ b/src/tests/common.rs @@ -15,6 +15,8 @@ mod inner { #[cfg(not(Py_GIL_DISABLED))] use pyo3::types::{IntoPyDict, PyList}; + use uuid::Uuid; + #[macro_export] macro_rules! py_assert { ($py:expr, $($val:ident)+, $assertion:literal) => { @@ -166,6 +168,11 @@ mod inner { .unwrap(); }}; } + + pub fn generate_unique_module_name(base: &str) -> std::ffi::CString { + let uuid = Uuid::new_v4().simple().to_string(); + std::ffi::CString::new(format!("{base}_{uuid}")).unwrap() + } } #[allow(unused_imports)] // some tests use just the macros and none of the other functionality diff --git a/src/types/any.rs b/src/types/any.rs index f71973d4417..be3b56deb35 100644 --- a/src/types/any.rs +++ b/src/types/any.rs @@ -1603,6 +1603,7 @@ mod tests { use crate::{ basic::CompareOp, ffi, + tests::common::generate_unique_module_name, types::{IntoPyDict, PyAny, PyAnyMethods, PyBool, PyInt, PyList, PyModule, PyTypeMethods}, Bound, PyTypeInfo, Python, ToPyObject, }; @@ -1647,7 +1648,7 @@ class NonHeapNonDescriptorInt: "# ), c_str!("test.py"), - c_str!("test"), + &generate_unique_module_name("test"), ) .unwrap(); @@ -1716,7 +1717,7 @@ class SimpleClass: "# ), c_str!(file!()), - c_str!("test_module"), + &generate_unique_module_name("test_module"), ) .expect("module creation failed"); diff --git a/src/types/typeobject.rs b/src/types/typeobject.rs index 152da881ea9..8d4e759580c 100644 --- a/src/types/typeobject.rs +++ b/src/types/typeobject.rs @@ -250,6 +250,7 @@ impl<'py> PyTypeMethods<'py> for Bound<'py, PyType> { #[cfg(test)] mod tests { + use crate::tests::common::generate_unique_module_name; use crate::types::{PyAnyMethods, PyBool, PyInt, PyModule, PyTuple, PyType, PyTypeMethods}; use crate::PyAny; use crate::Python; @@ -314,6 +315,7 @@ mod tests { #[test] fn test_type_names_standard() { Python::with_gil(|py| { + let module_name = generate_unique_module_name("test_module"); let module = PyModule::from_code( py, c_str!( @@ -323,7 +325,7 @@ class MyClass: "# ), c_str!(file!()), - c_str!("test_module"), + &module_name, ) .expect("module create failed"); @@ -331,10 +333,12 @@ class MyClass: let my_class_type = my_class.downcast_into::().unwrap(); assert_eq!(my_class_type.name().unwrap(), "MyClass"); assert_eq!(my_class_type.qualname().unwrap(), "MyClass"); - assert_eq!(my_class_type.module().unwrap(), "test_module"); + let module_name = module_name.to_str().unwrap(); + let qualname = format!("{module_name}.MyClass"); + assert_eq!(my_class_type.module().unwrap(), module_name); assert_eq!( my_class_type.fully_qualified_name().unwrap(), - "test_module.MyClass" + qualname.as_str() ); }); } @@ -353,6 +357,7 @@ class MyClass: #[test] fn test_type_names_nested() { Python::with_gil(|py| { + let module_name = generate_unique_module_name("test_module"); let module = PyModule::from_code( py, c_str!( @@ -363,7 +368,7 @@ class OuterClass: "# ), c_str!(file!()), - c_str!("test_module"), + &module_name, ) .expect("module create failed"); @@ -375,10 +380,12 @@ class OuterClass: inner_class_type.qualname().unwrap(), "OuterClass.InnerClass" ); - assert_eq!(inner_class_type.module().unwrap(), "test_module"); + let module_name = module_name.to_str().unwrap(); + let qualname = format!("{module_name}.OuterClass.InnerClass"); + assert_eq!(inner_class_type.module().unwrap(), module_name); assert_eq!( inner_class_type.fully_qualified_name().unwrap(), - "test_module.OuterClass.InnerClass" + qualname.as_str() ); }); } diff --git a/tests/test_module.rs b/tests/test_module.rs index 115bbf5b6be..6f19ad5763c 100644 --- a/tests/test_module.rs +++ b/tests/test_module.rs @@ -163,7 +163,7 @@ fn test_module_from_code_bound() { py, c_str!("def add(a,b):\n\treturn a+b"), c_str!("adder_mod.py"), - c_str!("adder_mod"), + &common::generate_unique_module_name("adder_mod"), ) .expect("Module code should be loaded"); From f1e2b4b4c655bbee4720cee280b5c6714ad51c36 Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Sat, 31 Aug 2024 12:05:57 +0200 Subject: [PATCH 253/495] reintroduce `PyUnicodeDecodeError` constructors (#4506) --- src/exceptions.rs | 44 ++++++++++++++++++++++++++++++++++---------- src/types/string.rs | 6 +++--- 2 files changed, 37 insertions(+), 13 deletions(-) diff --git a/src/exceptions.rs b/src/exceptions.rs index 958ad58110c..eec7dca4070 100644 --- a/src/exceptions.rs +++ b/src/exceptions.rs @@ -624,13 +624,13 @@ impl_windows_native_exception!( impl PyUnicodeDecodeError { /// Creates a Python `UnicodeDecodeError`. - pub fn new_bound<'p>( - py: Python<'p>, + pub fn new<'py>( + py: Python<'py>, encoding: &CStr, input: &[u8], range: ops::Range, reason: &CStr, - ) -> PyResult> { + ) -> PyResult> { use crate::ffi_ptr_ext::FfiPtrExt; use crate::py_result_ext::PyResultExt; unsafe { @@ -647,6 +647,19 @@ impl PyUnicodeDecodeError { .downcast_into() } + /// Deprecated name for [`PyUnicodeDecodeError::new`]. + #[deprecated(since = "0.23.0", note = "renamed to `PyUnicodeDecodeError::new`")] + #[inline] + pub fn new_bound<'py>( + py: Python<'py>, + encoding: &CStr, + input: &[u8], + range: ops::Range, + reason: &CStr, + ) -> PyResult> { + Self::new(py, encoding, input, range, reason) + } + /// Creates a Python `UnicodeDecodeError` from a Rust UTF-8 decoding error. /// /// # Examples @@ -660,7 +673,7 @@ impl PyUnicodeDecodeError { /// Python::with_gil(|py| { /// let invalid_utf8 = b"fo\xd8o"; /// let err = std::str::from_utf8(invalid_utf8).expect_err("should be invalid utf8"); - /// let decode_err = PyUnicodeDecodeError::new_utf8_bound(py, invalid_utf8, err)?; + /// let decode_err = PyUnicodeDecodeError::new_utf8(py, invalid_utf8, err)?; /// assert_eq!( /// decode_err.to_string(), /// "'utf-8' codec can't decode byte 0xd8 in position 2: invalid utf-8" @@ -668,13 +681,13 @@ impl PyUnicodeDecodeError { /// Ok(()) /// }) /// # } - pub fn new_utf8_bound<'p>( - py: Python<'p>, + pub fn new_utf8<'py>( + py: Python<'py>, input: &[u8], err: std::str::Utf8Error, - ) -> PyResult> { + ) -> PyResult> { let pos = err.valid_up_to(); - PyUnicodeDecodeError::new_bound( + PyUnicodeDecodeError::new( py, ffi::c_str!("utf-8"), input, @@ -682,6 +695,17 @@ impl PyUnicodeDecodeError { ffi::c_str!("invalid utf-8"), ) } + + /// Deprecated name for [`PyUnicodeDecodeError::new_utf8`]. + #[deprecated(since = "0.23.0", note = "renamed to `PyUnicodeDecodeError::new_utf8`")] + #[inline] + pub fn new_utf8_bound<'py>( + py: Python<'py>, + input: &[u8], + err: std::str::Utf8Error, + ) -> PyResult> { + Self::new_utf8(py, input, err) + } } impl_native_exception!(PyWarning, PyExc_Warning, native_doc!("Warning")); @@ -1017,7 +1041,7 @@ mod tests { #[cfg_attr(invalid_from_utf8_lint, allow(invalid_from_utf8))] let err = std::str::from_utf8(invalid_utf8).expect_err("should be invalid utf8"); Python::with_gil(|py| { - let decode_err = PyUnicodeDecodeError::new_utf8_bound(py, invalid_utf8, err).unwrap(); + let decode_err = PyUnicodeDecodeError::new_utf8(py, invalid_utf8, err).unwrap(); assert_eq!( format!("{:?}", decode_err), "UnicodeDecodeError('utf-8', b'fo\\xd8o', 2, 3, 'invalid utf-8')" @@ -1074,7 +1098,7 @@ mod tests { #[cfg_attr(invalid_from_utf8_lint, allow(invalid_from_utf8))] let err = std::str::from_utf8(invalid_utf8).expect_err("should be invalid utf8"); PyErr::from_value( - PyUnicodeDecodeError::new_utf8_bound(py, invalid_utf8, err) + PyUnicodeDecodeError::new_utf8(py, invalid_utf8, err) .unwrap() .into_any(), ) diff --git a/src/types/string.rs b/src/types/string.rs index ff1e33c74f8..c4aea67423d 100644 --- a/src/types/string.rs +++ b/src/types/string.rs @@ -70,7 +70,7 @@ impl<'a> PyStringData<'a> { match self { Self::Ucs1(data) => match str::from_utf8(data) { Ok(s) => Ok(Cow::Borrowed(s)), - Err(e) => Err(PyUnicodeDecodeError::new_utf8_bound(py, data, e)?.into()), + Err(e) => Err(PyUnicodeDecodeError::new_utf8(py, data, e)?.into()), }, Self::Ucs2(data) => match String::from_utf16(data) { Ok(s) => Ok(Cow::Owned(s)), @@ -78,7 +78,7 @@ impl<'a> PyStringData<'a> { let mut message = e.to_string().as_bytes().to_vec(); message.push(0); - Err(PyUnicodeDecodeError::new_bound( + Err(PyUnicodeDecodeError::new( py, ffi::c_str!("utf-16"), self.as_bytes(), @@ -90,7 +90,7 @@ impl<'a> PyStringData<'a> { }, Self::Ucs4(data) => match data.iter().map(|&c| std::char::from_u32(c)).collect() { Some(s) => Ok(Cow::Owned(s)), - None => Err(PyUnicodeDecodeError::new_bound( + None => Err(PyUnicodeDecodeError::new( py, ffi::c_str!("utf-32"), self.as_bytes(), From 8cafd23c2e05f7d6d8adeac15d706d5706091de5 Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Sat, 31 Aug 2024 13:34:24 +0200 Subject: [PATCH 254/495] reintroduce `Py::call` and `Py::call_method` (#4505) --- examples/decorator/src/lib.rs | 2 +- guide/src/python-from-rust/function-calls.md | 16 ++------ src/instance.rs | 43 ++++++++++++++++---- src/types/any.rs | 2 +- 4 files changed, 42 insertions(+), 21 deletions(-) diff --git a/examples/decorator/src/lib.rs b/examples/decorator/src/lib.rs index cfb09c112d5..8d257aecb2e 100644 --- a/examples/decorator/src/lib.rs +++ b/examples/decorator/src/lib.rs @@ -51,7 +51,7 @@ impl PyCounter { println!("{} has been called {} time(s).", name, new_count); // After doing something, we finally forward the call to the wrapped function - let ret = self.wraps.call_bound(py, args, kwargs)?; + let ret = self.wraps.call(py, args, kwargs)?; // We could do something with the return value of // the function before returning it diff --git a/guide/src/python-from-rust/function-calls.md b/guide/src/python-from-rust/function-calls.md index 22fcd8cacf0..12544dc02bd 100644 --- a/guide/src/python-from-rust/function-calls.md +++ b/guide/src/python-from-rust/function-calls.md @@ -91,26 +91,18 @@ fn main() -> PyResult<()> { // call object with PyDict let kwargs = [(key1, val1)].into_py_dict(py); - fun.call_bound(py, (), Some(&kwargs))?; + fun.call(py, (), Some(&kwargs))?; // pass arguments as Vec let kwargs = vec![(key1, val1), (key2, val2)]; - fun.call_bound(py, (), Some(&kwargs.into_py_dict(py)))?; + fun.call(py, (), Some(&kwargs.into_py_dict(py)))?; // pass arguments as HashMap let mut kwargs = HashMap::<&str, i32>::new(); kwargs.insert(key1, 1); - fun.call_bound(py, (), Some(&kwargs.into_py_dict(py)))?; + fun.call(py, (), Some(&kwargs.into_py_dict(py)))?; Ok(()) }) } -``` - -
- -During PyO3's [migration from "GIL Refs" to the `Bound` smart pointer](../migration.md#migrating-from-the-gil-refs-api-to-boundt), `Py::call` is temporarily named [`Py::call_bound`]({{#PYO3_DOCS_URL}}/pyo3/struct.Py.html#method.call_bound) (and `call_method` is temporarily `call_method_bound`). - -(This temporary naming is only the case for the `Py` smart pointer. The methods on the `&PyAny` GIL Ref such as `call` have not been given replacements, and the methods on the `Bound` smart pointer such as [`Bound::call`]({{#PYO3_DOCS_URL}}/pyo3/types/trait.PyAnyMethods.html#tymethod.call) already use follow the newest API conventions.) - -
+``` \ No newline at end of file diff --git a/src/instance.rs b/src/instance.rs index b0a4a38d176..c71cf89c02d 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -1449,18 +1449,30 @@ impl Py { /// Calls the object. /// /// This is equivalent to the Python expression `self(*args, **kwargs)`. - pub fn call_bound<'py, N>( + pub fn call<'py, A>( &self, py: Python<'py>, - args: N, + args: A, kwargs: Option<&Bound<'py, PyDict>>, ) -> PyResult where - N: IntoPy>, + A: IntoPy>, { self.bind(py).as_any().call(args, kwargs).map(Bound::unbind) } + /// Deprecated name for [`Py::call`]. + #[deprecated(since = "0.23.0", note = "renamed to `Py::call`")] + #[inline] + pub fn call_bound( + &self, + py: Python<'_>, + args: impl IntoPy>, + kwargs: Option<&Bound<'_, PyDict>>, + ) -> PyResult { + self.call(py, args, kwargs) + } + /// Calls the object with only positional arguments. /// /// This is equivalent to the Python expression `self(*args)`. @@ -1484,7 +1496,7 @@ impl Py { /// /// To avoid repeated temporary allocations of Python strings, the [`intern!`](crate::intern) /// macro can be used to intern `name`. - pub fn call_method_bound<'py, N, A>( + pub fn call_method<'py, N, A>( &self, py: Python<'py>, name: N, @@ -1501,6 +1513,23 @@ impl Py { .map(Bound::unbind) } + /// Deprecated name for [`Py::call_method`]. + #[deprecated(since = "0.23.0", note = "renamed to `Py::call_method`")] + #[inline] + pub fn call_method_bound( + &self, + py: Python<'_>, + name: N, + args: A, + kwargs: Option<&Bound<'_, PyDict>>, + ) -> PyResult + where + N: IntoPy>, + A: IntoPy>, + { + self.call_method(py, name.into_py(py), args, kwargs) + } + /// Calls a method on the object with only positional arguments. /// /// This is equivalent to the Python expression `self.name(*args)`. @@ -1904,11 +1933,11 @@ mod tests { assert_repr(obj.call0(py).unwrap().bind(py), "{}"); assert_repr(obj.call1(py, ()).unwrap().bind(py), "{}"); - assert_repr(obj.call_bound(py, (), None).unwrap().bind(py), "{}"); + assert_repr(obj.call(py, (), None).unwrap().bind(py), "{}"); assert_repr(obj.call1(py, ((('x', 1),),)).unwrap().bind(py), "{'x': 1}"); assert_repr( - obj.call_bound(py, (), Some(&[('x', 1)].into_py_dict(py))) + obj.call(py, (), Some(&[('x', 1)].into_py_dict(py))) .unwrap() .bind(py), "{'x': 1}", @@ -1922,7 +1951,7 @@ mod tests { let obj: PyObject = PyDict::new(py).into(); assert!(obj.call_method0(py, "asdf").is_err()); assert!(obj - .call_method_bound(py, "nonexistent_method", (1,), None) + .call_method(py, "nonexistent_method", (1,), None) .is_err()); assert!(obj.call_method0(py, "nonexistent_method").is_err()); assert!(obj.call_method1(py, "nonexistent_method", (1,)).is_err()); diff --git a/src/types/any.rs b/src/types/any.rs index be3b56deb35..4d786a2144c 100644 --- a/src/types/any.rs +++ b/src/types/any.rs @@ -1699,7 +1699,7 @@ class NonHeapNonDescriptorInt: Python::with_gil(|py| { let list = vec![3, 6, 5, 4, 7].to_object(py); let dict = vec![("reverse", true)].into_py_dict(py); - list.call_method_bound(py, "sort", (), Some(&dict)).unwrap(); + list.call_method(py, "sort", (), Some(&dict)).unwrap(); assert_eq!(list.extract::>(py).unwrap(), vec![7, 6, 5, 4, 3]); }); } From 73fc844dfd9e1ebaf5525bcc3bafbe243dc1e5ce Mon Sep 17 00:00:00 2001 From: Chayim Refael Friedman Date: Sat, 31 Aug 2024 22:41:55 +0300 Subject: [PATCH 255/495] Error at compile-time when a non-subclassable class is being subclassed (#4453) * Error at compile-time when a non-subclassable class is being subclassed Previously this crashed at runtime. * gate `invalid_base_class` ui test on non-abi3 * Fix abi3 inheritance UI test * Reword subclassing error --------- Co-authored-by: David Hewitt --- newsfragments/4453.changed.md | 1 + pyo3-macros-backend/src/pyclass.rs | 13 +++++++ src/exceptions.rs | 1 + src/impl_/pyclass.rs | 23 +++++------ src/pyclass_init.rs | 2 +- src/types/any.rs | 7 ++++ src/types/complex.rs | 2 + src/types/datetime.rs | 5 +++ src/types/dict.rs | 2 + src/types/float.rs | 2 + src/types/frozenset.rs | 2 + src/types/mod.rs | 14 +++++-- src/types/set.rs | 3 ++ src/types/weakref/reference.rs | 3 ++ tests/test_compile_error.rs | 2 + tests/test_declarative_module.rs | 27 ------------- tests/test_inheritance.rs | 23 ----------- tests/test_macros.rs | 2 +- tests/ui/abi3_inheritance.stderr | 17 ++++---- tests/ui/abi3_nativetype_inheritance.stderr | 19 ++++----- tests/ui/invalid_base_class.rs | 7 ++++ tests/ui/invalid_base_class.stderr | 43 +++++++++++++++++++++ 22 files changed, 138 insertions(+), 82 deletions(-) create mode 100644 newsfragments/4453.changed.md create mode 100644 tests/ui/invalid_base_class.rs create mode 100644 tests/ui/invalid_base_class.stderr diff --git a/newsfragments/4453.changed.md b/newsfragments/4453.changed.md new file mode 100644 index 00000000000..58a1e0ffcbd --- /dev/null +++ b/newsfragments/4453.changed.md @@ -0,0 +1 @@ +Make subclassing a class that doesn't allow that a compile-time error instead of runtime \ No newline at end of file diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index 9e3dbceaa91..9d5535f6e17 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -2244,7 +2244,20 @@ impl<'a> PyClassImplsBuilder<'a> { quote! { #pyo3_path::PyAny } }; + let pyclass_base_type_impl = attr.options.subclass.map(|subclass| { + quote_spanned! { subclass.span() => + impl #pyo3_path::impl_::pyclass::PyClassBaseType for #cls { + type LayoutAsBase = #pyo3_path::impl_::pycell::PyClassObject; + type BaseNativeType = ::BaseNativeType; + type Initializer = #pyo3_path::pyclass_init::PyClassInitializer; + type PyClassMutability = ::PyClassMutability; + } + } + }); + Ok(quote! { + #pyclass_base_type_impl + impl #pyo3_path::impl_::pyclass::PyClassImpl for #cls { const IS_BASETYPE: bool = #is_basetype; const IS_SUBCLASS: bool = #is_subclass; diff --git a/src/exceptions.rs b/src/exceptions.rs index eec7dca4070..4239fae0e41 100644 --- a/src/exceptions.rs +++ b/src/exceptions.rs @@ -276,6 +276,7 @@ macro_rules! impl_native_exception ( $crate::impl_exception_boilerplate!($name); $crate::pyobject_native_type!($name, $layout, |_py| unsafe { $crate::ffi::$exc_name as *mut $crate::ffi::PyTypeObject } $(, #checkfunction=$checkfunction)?); + $crate::pyobject_subclassable_native_type!($name, $layout); ); ($name:ident, $exc_name:ident, $doc:expr) => ( impl_native_exception!($name, $exc_name, $doc, $crate::ffi::PyBaseExceptionObject); diff --git a/src/impl_/pyclass.rs b/src/impl_/pyclass.rs index 1c47d01f9a2..44070ec30e4 100644 --- a/src/impl_/pyclass.rs +++ b/src/impl_/pyclass.rs @@ -1116,7 +1116,18 @@ impl PyClassThreadChecker for ThreadCheckerImpl { #[cfg_attr( all(diagnostic_namespace, feature = "abi3"), diagnostic::on_unimplemented( - note = "with the `abi3` feature enabled, PyO3 does not support subclassing native types" + message = "pyclass `{Self}` cannot be subclassed", + label = "required for `#[pyclass(extends={Self})]`", + note = "`{Self}` must have `#[pyclass(subclass)]` to be eligible for subclassing", + note = "with the `abi3` feature enabled, PyO3 does not support subclassing native types", + ) +)] +#[cfg_attr( + all(diagnostic_namespace, not(feature = "abi3")), + diagnostic::on_unimplemented( + message = "pyclass `{Self}` cannot be subclassed", + label = "required for `#[pyclass(extends={Self})]`", + note = "`{Self}` must have `#[pyclass(subclass)]` to be eligible for subclassing", ) )] pub trait PyClassBaseType: Sized { @@ -1126,16 +1137,6 @@ pub trait PyClassBaseType: Sized { type PyClassMutability: PyClassMutability; } -/// All mutable PyClasses can be used as a base type. -/// -/// In the future this will be extended to immutable PyClasses too. -impl PyClassBaseType for T { - type LayoutAsBase = crate::impl_::pycell::PyClassObject; - type BaseNativeType = T::BaseNativeType; - type Initializer = crate::pyclass_init::PyClassInitializer; - type PyClassMutability = T::PyClassMutability; -} - /// Implementation of tp_dealloc for pyclasses without gc pub(crate) unsafe extern "C" fn tp_dealloc(obj: *mut ffi::PyObject) { crate::impl_::trampoline::dealloc(obj, PyClassObject::::tp_dealloc) diff --git a/src/pyclass_init.rs b/src/pyclass_init.rs index 2e391f38d16..12375964a7d 100644 --- a/src/pyclass_init.rs +++ b/src/pyclass_init.rs @@ -308,7 +308,7 @@ where impl From<(S, B)> for PyClassInitializer where S: PyClass, - B: PyClass, + B: PyClass + PyClassBaseType>, B::BaseType: PyClassBaseType>, { #[track_caller] diff --git a/src/types/any.rs b/src/types/any.rs index 4d786a2144c..d9aa845106e 100644 --- a/src/types/any.rs +++ b/src/types/any.rs @@ -44,6 +44,13 @@ pyobject_native_type_info!( ); pyobject_native_type_sized!(PyAny, ffi::PyObject); +// We cannot use `pyobject_subclassable_native_type!()` because it cfgs out on `Py_LIMITED_API`. +impl crate::impl_::pyclass::PyClassBaseType for PyAny { + type LayoutAsBase = crate::impl_::pycell::PyClassObjectBase; + type BaseNativeType = PyAny; + type Initializer = crate::pyclass_init::PyNativeTypeInitializer; + type PyClassMutability = crate::pycell::impl_::ImmutableClass; +} /// This trait represents the Python APIs which are usable on all Python objects. /// diff --git a/src/types/complex.rs b/src/types/complex.rs index 131bcc09347..58651569b47 100644 --- a/src/types/complex.rs +++ b/src/types/complex.rs @@ -19,6 +19,8 @@ use std::os::raw::c_double; #[repr(transparent)] pub struct PyComplex(PyAny); +pyobject_subclassable_native_type!(PyComplex, ffi::PyComplexObject); + pyobject_native_type!( PyComplex, ffi::PyComplexObject, diff --git a/src/types/datetime.rs b/src/types/datetime.rs index 77df943fd81..2f6906064f3 100644 --- a/src/types/datetime.rs +++ b/src/types/datetime.rs @@ -199,6 +199,7 @@ pyobject_native_type!( #module=Some("datetime"), #checkfunction=PyDate_Check ); +pyobject_subclassable_native_type!(PyDate, crate::ffi::PyDateTime_Date); impl PyDate { /// Creates a new `datetime.date`. @@ -269,6 +270,7 @@ pyobject_native_type!( #module=Some("datetime"), #checkfunction=PyDateTime_Check ); +pyobject_subclassable_native_type!(PyDateTime, crate::ffi::PyDateTime_DateTime); impl PyDateTime { /// Creates a new `datetime.datetime` object. @@ -514,6 +516,7 @@ pyobject_native_type!( #module=Some("datetime"), #checkfunction=PyTime_Check ); +pyobject_subclassable_native_type!(PyTime, crate::ffi::PyDateTime_Time); impl PyTime { /// Creates a new `datetime.time` object. @@ -669,6 +672,7 @@ pyobject_native_type!( #module=Some("datetime"), #checkfunction=PyTZInfo_Check ); +pyobject_subclassable_native_type!(PyTzInfo, crate::ffi::PyObject); /// Equivalent to `datetime.timezone.utc` pub fn timezone_utc(py: Python<'_>) -> Bound<'_, PyTzInfo> { @@ -720,6 +724,7 @@ pyobject_native_type!( #module=Some("datetime"), #checkfunction=PyDelta_Check ); +pyobject_subclassable_native_type!(PyDelta, crate::ffi::PyDateTime_Delta); impl PyDelta { /// Creates a new `timedelta`. diff --git a/src/types/dict.rs b/src/types/dict.rs index e589b1cb884..6e29e6edcaa 100644 --- a/src/types/dict.rs +++ b/src/types/dict.rs @@ -18,6 +18,8 @@ use crate::{ffi, Python, ToPyObject}; #[repr(transparent)] pub struct PyDict(PyAny); +pyobject_subclassable_native_type!(PyDict, crate::ffi::PyDictObject); + pyobject_native_type!( PyDict, ffi::PyDictObject, diff --git a/src/types/float.rs b/src/types/float.rs index 5e637af3b62..58fdba609af 100644 --- a/src/types/float.rs +++ b/src/types/float.rs @@ -23,6 +23,8 @@ use std::os::raw::c_double; #[repr(transparent)] pub struct PyFloat(PyAny); +pyobject_subclassable_native_type!(PyFloat, crate::ffi::PyFloatObject); + pyobject_native_type!( PyFloat, ffi::PyFloatObject, diff --git a/src/types/frozenset.rs b/src/types/frozenset.rs index 6a0cdca89d5..2e41864872d 100644 --- a/src/types/frozenset.rs +++ b/src/types/frozenset.rs @@ -61,6 +61,8 @@ impl<'py> PyFrozenSetBuilder<'py> { #[repr(transparent)] pub struct PyFrozenSet(PyAny); +#[cfg(not(any(PyPy, GraalPy)))] +pyobject_subclassable_native_type!(PyFrozenSet, crate::ffi::PySetObject); #[cfg(not(any(PyPy, GraalPy)))] pyobject_native_type!( PyFrozenSet, diff --git a/src/types/mod.rs b/src/types/mod.rs index d11af8598fe..d1020931d76 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -195,10 +195,9 @@ macro_rules! pyobject_native_type_core { #[doc(hidden)] #[macro_export] -macro_rules! pyobject_native_type_sized { +macro_rules! pyobject_subclassable_native_type { ($name:ty, $layout:path $(;$generics:ident)*) => { - unsafe impl $crate::type_object::PyLayout<$name> for $layout {} - impl $crate::type_object::PySizedLayout<$name> for $layout {} + #[cfg(not(Py_LIMITED_API))] impl<$($generics,)*> $crate::impl_::pyclass::PyClassBaseType for $name { type LayoutAsBase = $crate::impl_::pycell::PyClassObjectBase<$layout>; type BaseNativeType = $name; @@ -208,6 +207,15 @@ macro_rules! pyobject_native_type_sized { } } +#[doc(hidden)] +#[macro_export] +macro_rules! pyobject_native_type_sized { + ($name:ty, $layout:path $(;$generics:ident)*) => { + unsafe impl $crate::type_object::PyLayout<$name> for $layout {} + impl $crate::type_object::PySizedLayout<$name> for $layout {} + }; +} + /// Declares all of the boilerplate for Python types which can be inherited from (because the exact /// Python layout is known). #[doc(hidden)] diff --git a/src/types/set.rs b/src/types/set.rs index 8292c199d7d..42b827a9ee0 100644 --- a/src/types/set.rs +++ b/src/types/set.rs @@ -19,6 +19,9 @@ use std::ptr; #[repr(transparent)] pub struct PySet(PyAny); +#[cfg(not(any(PyPy, GraalPy)))] +pyobject_subclassable_native_type!(PySet, crate::ffi::PySetObject); + #[cfg(not(any(PyPy, GraalPy)))] pyobject_native_type!( PySet, diff --git a/src/types/weakref/reference.rs b/src/types/weakref/reference.rs index b86be515b5b..deb62465ccc 100644 --- a/src/types/weakref/reference.rs +++ b/src/types/weakref/reference.rs @@ -15,6 +15,9 @@ use super::PyWeakrefMethods; #[repr(transparent)] pub struct PyWeakrefReference(PyAny); +#[cfg(not(any(PyPy, GraalPy, Py_LIMITED_API)))] +pyobject_subclassable_native_type!(PyWeakrefReference, crate::ffi::PyWeakReference); + #[cfg(not(any(PyPy, GraalPy, Py_LIMITED_API)))] pyobject_native_type!( PyWeakrefReference, diff --git a/tests/test_compile_error.rs b/tests/test_compile_error.rs index b1fcdc09fb7..5ae4e550733 100644 --- a/tests/test_compile_error.rs +++ b/tests/test_compile_error.rs @@ -62,4 +62,6 @@ fn test_compile_errors() { #[cfg(all(Py_LIMITED_API, not(Py_3_9)))] t.compile_fail("tests/ui/abi3_dict.rs"); t.compile_fail("tests/ui/duplicate_pymodule_submodule.rs"); + #[cfg(all(not(Py_LIMITED_API), Py_3_11))] + t.compile_fail("tests/ui/invalid_base_class.rs"); } diff --git a/tests/test_declarative_module.rs b/tests/test_declarative_module.rs index 9d3250d79ef..a911702ce20 100644 --- a/tests/test_declarative_module.rs +++ b/tests/test_declarative_module.rs @@ -4,8 +4,6 @@ use pyo3::create_exception; use pyo3::exceptions::PyException; use pyo3::prelude::*; use pyo3::sync::GILOnceCell; -#[cfg(not(Py_LIMITED_API))] -use pyo3::types::PyBool; #[path = "../src/tests/common.rs"] mod common; @@ -186,31 +184,6 @@ fn test_declarative_module() { }) } -#[cfg(not(Py_LIMITED_API))] -#[pyclass(extends = PyBool)] -struct ExtendsBool; - -#[cfg(not(Py_LIMITED_API))] -#[pymodule] -mod class_initialization_module { - #[pymodule_export] - use super::ExtendsBool; -} - -#[test] -#[cfg(not(Py_LIMITED_API))] -fn test_class_initialization_fails() { - Python::with_gil(|py| { - let err = class_initialization_module::_PYO3_DEF - .make_module(py) - .unwrap_err(); - assert_eq!( - err.to_string(), - "RuntimeError: An error occurred while initializing class ExtendsBool" - ); - }) -} - #[pymodule] mod r#type { #[pymodule_export] diff --git a/tests/test_inheritance.rs b/tests/test_inheritance.rs index a43ab57b6c1..d3980152120 100644 --- a/tests/test_inheritance.rs +++ b/tests/test_inheritance.rs @@ -345,26 +345,3 @@ fn test_subclass_ref_counts() { ); }) } - -#[test] -#[cfg(not(Py_LIMITED_API))] -fn module_add_class_inherit_bool_fails() { - use pyo3::types::PyBool; - - #[pyclass(extends = PyBool)] - struct ExtendsBool; - - Python::with_gil(|py| { - let m = PyModule::new(py, "test_module").unwrap(); - - let err = m.add_class::().unwrap_err(); - assert_eq!( - err.to_string(), - "RuntimeError: An error occurred while initializing class ExtendsBool" - ); - assert_eq!( - err.cause(py).unwrap().to_string(), - "TypeError: type 'bool' is not an acceptable base type" - ); - }) -} diff --git a/tests/test_macros.rs b/tests/test_macros.rs index 40fd4847679..4b5feddf3f9 100644 --- a/tests/test_macros.rs +++ b/tests/test_macros.rs @@ -12,7 +12,7 @@ macro_rules! make_struct_using_macro { // Ensure that one doesn't need to fall back on the escape type: tt // in order to macro create pyclass. ($class_name:ident, $py_name:literal) => { - #[pyclass(name=$py_name)] + #[pyclass(name=$py_name, subclass)] struct $class_name {} }; } diff --git a/tests/ui/abi3_inheritance.stderr b/tests/ui/abi3_inheritance.stderr index 756df2eb75e..309b67a633d 100644 --- a/tests/ui/abi3_inheritance.stderr +++ b/tests/ui/abi3_inheritance.stderr @@ -1,24 +1,27 @@ -error[E0277]: the trait bound `PyException: PyClassBaseType` is not satisfied +error[E0277]: pyclass `PyException` cannot be subclassed --> tests/ui/abi3_inheritance.rs:4:19 | 4 | #[pyclass(extends=PyException)] - | ^^^^^^^^^^^ the trait `PyClass` is not implemented for `PyException`, which is required by `PyException: PyClassBaseType` + | ^^^^^^^^^^^ required for `#[pyclass(extends=PyException)]` | + = help: the trait `PyClassBaseType` is not implemented for `PyException` + = note: `PyException` must have `#[pyclass(subclass)]` to be eligible for subclassing = note: with the `abi3` feature enabled, PyO3 does not support subclassing native types = help: the trait `PyClassBaseType` is implemented for `PyAny` - = note: required for `PyException` to implement `PyClassBaseType` note: required by a bound in `PyClassImpl::BaseType` --> src/impl_/pyclass.rs | | type BaseType: PyTypeInfo + PyClassBaseType; | ^^^^^^^^^^^^^^^ required by this bound in `PyClassImpl::BaseType` -error[E0277]: the trait bound `PyException: PyClass` is not satisfied +error[E0277]: pyclass `PyException` cannot be subclassed --> tests/ui/abi3_inheritance.rs:4:1 | 4 | #[pyclass(extends=PyException)] - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `PyClass` is not implemented for `PyException`, which is required by `PyException: PyClassBaseType` + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required for `#[pyclass(extends=PyException)]` | - = help: the trait `PyClass` is implemented for `MyException` - = note: required for `PyException` to implement `PyClassBaseType` + = help: the trait `PyClassBaseType` is not implemented for `PyException` + = note: `PyException` must have `#[pyclass(subclass)]` to be eligible for subclassing + = note: with the `abi3` feature enabled, PyO3 does not support subclassing native types + = help: the trait `PyClassBaseType` is implemented for `PyAny` = note: this error originates in the attribute macro `pyclass` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/tests/ui/abi3_nativetype_inheritance.stderr b/tests/ui/abi3_nativetype_inheritance.stderr index 5cd985ccfe5..872de60b244 100644 --- a/tests/ui/abi3_nativetype_inheritance.stderr +++ b/tests/ui/abi3_nativetype_inheritance.stderr @@ -1,26 +1,27 @@ -error[E0277]: the trait bound `PyDict: PyClassBaseType` is not satisfied +error[E0277]: pyclass `PyDict` cannot be subclassed --> tests/ui/abi3_nativetype_inheritance.rs:5:19 | 5 | #[pyclass(extends=PyDict)] - | ^^^^^^ the trait `PyClass` is not implemented for `PyDict`, which is required by `PyDict: PyClassBaseType` + | ^^^^^^ required for `#[pyclass(extends=PyDict)]` | + = help: the trait `PyClassBaseType` is not implemented for `PyDict` + = note: `PyDict` must have `#[pyclass(subclass)]` to be eligible for subclassing = note: with the `abi3` feature enabled, PyO3 does not support subclassing native types = help: the trait `PyClassBaseType` is implemented for `PyAny` - = note: required for `PyDict` to implement `PyClassBaseType` note: required by a bound in `PyClassImpl::BaseType` --> src/impl_/pyclass.rs | | type BaseType: PyTypeInfo + PyClassBaseType; | ^^^^^^^^^^^^^^^ required by this bound in `PyClassImpl::BaseType` -error[E0277]: the trait bound `PyDict: PyClass` is not satisfied +error[E0277]: pyclass `PyDict` cannot be subclassed --> tests/ui/abi3_nativetype_inheritance.rs:5:1 | 5 | #[pyclass(extends=PyDict)] - | ^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `PyClass` is not implemented for `PyDict`, which is required by `PyDict: PyClassBaseType` + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ required for `#[pyclass(extends=PyDict)]` | - = help: the following other types implement trait `PyClass`: - TestClass - pyo3::coroutine::Coroutine - = note: required for `PyDict` to implement `PyClassBaseType` + = help: the trait `PyClassBaseType` is not implemented for `PyDict` + = note: `PyDict` must have `#[pyclass(subclass)]` to be eligible for subclassing + = note: with the `abi3` feature enabled, PyO3 does not support subclassing native types + = help: the trait `PyClassBaseType` is implemented for `PyAny` = note: this error originates in the attribute macro `pyclass` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/tests/ui/invalid_base_class.rs b/tests/ui/invalid_base_class.rs new file mode 100644 index 00000000000..7433dcb2b96 --- /dev/null +++ b/tests/ui/invalid_base_class.rs @@ -0,0 +1,7 @@ +use pyo3::prelude::*; +use pyo3::types::PyBool; + +#[pyclass(extends=PyBool)] +struct ExtendsBool; + +fn main() {} diff --git a/tests/ui/invalid_base_class.stderr b/tests/ui/invalid_base_class.stderr new file mode 100644 index 00000000000..c40bed9eaa6 --- /dev/null +++ b/tests/ui/invalid_base_class.stderr @@ -0,0 +1,43 @@ +error[E0277]: pyclass `PyBool` cannot be subclassed + --> tests/ui/invalid_base_class.rs:4:19 + | +4 | #[pyclass(extends=PyBool)] + | ^^^^^^ required for `#[pyclass(extends=PyBool)]` + | + = help: the trait `PyClassBaseType` is not implemented for `PyBool` + = note: `PyBool` must have `#[pyclass(subclass)]` to be eligible for subclassing + = help: the following other types implement trait `PyClassBaseType`: + PyAny + PyArithmeticError + PyAssertionError + PyAttributeError + PyBaseException + PyBaseExceptionGroup + PyBlockingIOError + PyBrokenPipeError + and $N others +note: required by a bound in `PyClassImpl::BaseType` + --> src/impl_/pyclass.rs + | + | type BaseType: PyTypeInfo + PyClassBaseType; + | ^^^^^^^^^^^^^^^ required by this bound in `PyClassImpl::BaseType` + +error[E0277]: pyclass `PyBool` cannot be subclassed + --> tests/ui/invalid_base_class.rs:4:1 + | +4 | #[pyclass(extends=PyBool)] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ required for `#[pyclass(extends=PyBool)]` + | + = help: the trait `PyClassBaseType` is not implemented for `PyBool` + = note: `PyBool` must have `#[pyclass(subclass)]` to be eligible for subclassing + = help: the following other types implement trait `PyClassBaseType`: + PyAny + PyArithmeticError + PyAssertionError + PyAttributeError + PyBaseException + PyBaseExceptionGroup + PyBlockingIOError + PyBrokenPipeError + and $N others + = note: this error originates in the attribute macro `pyclass` (in Nightly builds, run with -Z macro-backtrace for more info) From 9eab2600cd31bce1f017c294d2c20deee7289e76 Mon Sep 17 00:00:00 2001 From: Georg Brandl Date: Sun, 1 Sep 2024 12:27:22 +0200 Subject: [PATCH 256/495] Contributing: fix test command (#4482) Only "cargo test" does not run Python tests. --- Contributing.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/Contributing.md b/Contributing.md index d3c61cf2195..76af08325fb 100644 --- a/Contributing.md +++ b/Contributing.md @@ -53,7 +53,7 @@ nox -s docs -- open #### Doctests We use lots of code blocks in our docs. Run `cargo test --doc` when making changes to check that -the doctests still work, or `cargo test` to run all the tests including doctests. See +the doctests still work, or `cargo test` to run all the Rust tests including doctests. See https://doc.rust-lang.org/rustdoc/documentation-tests.html for a guide on doctests. #### Building the guide @@ -86,7 +86,7 @@ Everybody is welcome to submit comments on open PRs. Please help ensure new PyO3 Here are a few things to note when you are writing PRs. -### Continuous Integration +### Testing and Continuous Integration The PyO3 repo uses GitHub Actions. PRs are blocked from merging if CI is not successful. Formatting, linting and tests are checked for all Rust and Python code. In addition, all warnings in Rust code are disallowed (using `RUSTFLAGS="-D warnings"`). @@ -94,8 +94,7 @@ Tests run with all supported Python versions with the latest stable Rust compile If you are adding a new feature, you should add it to the `full` feature in our *Cargo.toml** so that it is tested in CI. -You can run these tests yourself with -`nox`. Use `nox -l` to list the full set of subcommands you can run. +You can run these checks yourself with `nox`. Use `nox -l` to list the full set of subcommands you can run. #### Linting Python code `nox -s ruff` @@ -110,7 +109,7 @@ You can run these tests yourself with `nox -s clippy-all` #### Tests -`cargo test --features full` +`nox -s test` or `cargo test` for Rust tests only, `nox -f pytests/noxfile.py -s test` for Python tests only #### Check all conditional compilation `nox -s check-feature-powerset` From 1a96d9782921c9e1e991e2eaf11d2ceadee58d32 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Tue, 3 Sep 2024 06:54:11 +0100 Subject: [PATCH 257/495] Add Python-ref cloning `GILOnceCell::>::clone_ref` (#4511) --- newsfragments/4511.added.md | 1 + src/sync.rs | 16 +++++++++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 newsfragments/4511.added.md diff --git a/newsfragments/4511.added.md b/newsfragments/4511.added.md new file mode 100644 index 00000000000..6572ddc7238 --- /dev/null +++ b/newsfragments/4511.added.md @@ -0,0 +1 @@ +Add Python-ref cloning `clone_ref` for `GILOnceCell>` diff --git a/src/sync.rs b/src/sync.rs index c781755c067..33d247b7856 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -197,6 +197,15 @@ impl GILOnceCell { } } +impl GILOnceCell> { + /// Create a new cell that contains a new Python reference to the same contained object. + /// + /// Returns an uninitialised cell if `self` has not yet been initialised. + pub fn clone_ref(&self, py: Python<'_>) -> Self { + Self(UnsafeCell::new(self.get(py).map(|ob| ob.clone_ref(py)))) + } +} + impl GILOnceCell> { /// Get a reference to the contained Python type, initializing it if needed. /// @@ -325,7 +334,12 @@ mod tests { assert_eq!(cell.get_or_try_init(py, || Err(5)), Ok(&2)); assert_eq!(cell.take(), Some(2)); - assert_eq!(cell.into_inner(), None) + assert_eq!(cell.into_inner(), None); + + let cell_py = GILOnceCell::new(); + assert!(cell_py.clone_ref(py).get(py).is_none()); + cell_py.get_or_init(py, || py.None()); + assert!(cell_py.clone_ref(py).get(py).unwrap().is_none(py)); }) } } From 454529d7142db84003eef86664379ed4c755726f Mon Sep 17 00:00:00 2001 From: Andre Brisco <91817010+abrisco@users.noreply.github.com> Date: Mon, 2 Sep 2024 23:06:42 -0700 Subject: [PATCH 258/495] docs: updated docs with additional Bazel example (#4508) * docs: updated docs with additional Bazel example * fixed link --- guide/src/building-and-distribution.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/guide/src/building-and-distribution.md b/guide/src/building-and-distribution.md index c137a1a3995..b93ceea921e 100644 --- a/guide/src/building-and-distribution.md +++ b/guide/src/building-and-distribution.md @@ -92,8 +92,9 @@ If you're packaging your library for redistribution, you should indicated the Py To use PyO3 with bazel one needs to manually configure PyO3, PyO3-ffi and PyO3-macros. In particular, one needs to make sure that it is compiled with the right python flags for the version you intend to use. For example see: -1. https://github.com/OliverFM/pytorch_with_gazelle -- for a minimal example of a repo that can use PyO3, PyTorch and Gazelle to generate python Build files. -2. https://github.com/TheButlah/rules_pyo3 -- which has more extensive support, but is outdated. +1. https://github.com/abrisco/rules_pyo3 -- General rules for building extension modules. +2. https://github.com/OliverFM/pytorch_with_gazelle -- for a minimal example of a repo that can use PyO3, PyTorch and Gazelle to generate python Build files. +3. https://github.com/TheButlah/rules_pyo3 -- is somewhat dated. #### Platform tags From d0faf5384aa4c0825181913f3fba51da45233789 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Tue, 3 Sep 2024 15:02:56 +0200 Subject: [PATCH 259/495] ci: fix nightly build (#4517) --- pyo3-build-config/src/impl_.rs | 5 +++-- pyo3-macros-backend/src/pymethod.rs | 2 +- src/version.rs | 8 ++++---- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index e254a26cd27..7b9ae3ea9fc 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -1059,8 +1059,9 @@ impl FromStr for BuildFlag { } } -/// A list of python interpreter compile-time preprocessor defines that -/// we will pick up and pass to rustc via `--cfg=py_sys_config={varname}`; +/// A list of python interpreter compile-time preprocessor defines. +/// +/// PyO3 will pick these up and pass to rustc via `--cfg=py_sys_config={varname}`; /// this allows using them conditional cfg attributes in the .rs files, so /// /// ```rust diff --git a/pyo3-macros-backend/src/pymethod.rs b/pyo3-macros-backend/src/pymethod.rs index 2abc008899c..aa0f3cedfcb 100644 --- a/pyo3-macros-backend/src/pymethod.rs +++ b/pyo3-macros-backend/src/pymethod.rs @@ -848,7 +848,7 @@ pub fn impl_py_getter_def( } /// Split an argument of pyo3::Python from the front of the arg list, if present -fn split_off_python_arg<'a>(args: &'a [FnArg<'a>]) -> (Option<&PyArg<'_>>, &[FnArg<'_>]) { +fn split_off_python_arg<'a, 'b>(args: &'a [FnArg<'b>]) -> (Option<&'a PyArg<'b>>, &'a [FnArg<'b>]) { match args { [FnArg::Py(py), args @ ..] => (Some(py), args), args => (None, args), diff --git a/src/version.rs b/src/version.rs index ea9e9d406d7..e241c02c92c 100644 --- a/src/version.rs +++ b/src/version.rs @@ -15,7 +15,7 @@ /// /// [`Python::version`]: crate::marker::Python::version #[derive(Debug)] -pub struct PythonVersionInfo<'py> { +pub struct PythonVersionInfo<'a> { /// Python major version (e.g. `3`). pub major: u8, /// Python minor version (e.g. `11`). @@ -23,12 +23,12 @@ pub struct PythonVersionInfo<'py> { /// Python patch version (e.g. `0`). pub patch: u8, /// Python version suffix, if applicable (e.g. `a0`). - pub suffix: Option<&'py str>, + pub suffix: Option<&'a str>, } -impl<'py> PythonVersionInfo<'py> { +impl<'a> PythonVersionInfo<'a> { /// Parses a hard-coded Python interpreter version string (e.g. 3.9.0a4+). - pub(crate) fn from_str(version_number_str: &'py str) -> Result { + pub(crate) fn from_str(version_number_str: &'a str) -> Result, &'a str> { fn split_and_parse_number(version_part: &str) -> (u8, Option<&str>) { match version_part.find(|c: char| !c.is_ascii_digit()) { None => (version_part.parse().unwrap(), None), From 3cfa04fa9179647fb78e54a32e9488617f6fbbe9 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Tue, 3 Sep 2024 18:10:12 +0200 Subject: [PATCH 260/495] docs: suggest cloning `Arc>` to replace `py-clone` (#4519) --- guide/src/migration.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/guide/src/migration.md b/guide/src/migration.md index b1dfc9e30c4..e366c825a3b 100644 --- a/guide/src/migration.md +++ b/guide/src/migration.md @@ -139,7 +139,7 @@ This is purely additional and should just extend the possible return types. PyO3 0.23 introduces a new unified `IntoPyObject` trait to convert Rust types into Python objects. Notable features of this new trait: -- conversions can now return an error +- conversions can now return an error - compared to `IntoPy` the generic `T` moved into an associated type, so - there is now only one way to convert a given type - the output type is stronger typed and may return any Python type instead of just `PyAny` @@ -150,7 +150,7 @@ All PyO3 provided types as well as `#[pyclass]`es already implement `IntoPyObjec need to adapt an implementation of `IntoPyObject` to stay compatible with the Python APIs. -Before: +Before: ```rust # use pyo3::prelude::*; # #[allow(dead_code)] @@ -248,6 +248,8 @@ If you rely on `impl Clone for Py` to fulfil trait requirements imposed by However, take care to note that the behaviour is different from previous versions. If `Clone` was called without the GIL being held, we tried to delay the application of these reference count increments until PyO3-based code would re-acquire it. This turned out to be impossible to implement in a sound manner and hence was removed. Now, if `Clone` is called without the GIL being held, we panic instead for which calling code might not be prepared. +It is advised to migrate off the `py-clone` feature. The simplest way to remove dependency on `impl Clone for Py` is to wrap `Py` as `Arc>` and use cloning of the arc. + Related to this, we also added a `pyo3_disable_reference_pool` conditional compilation flag which removes the infrastructure necessary to apply delayed reference count decrements implied by `impl Drop for Py`. They do not appear to be a soundness hazard as they should lead to memory leaks in the worst case. However, the global synchronization adds significant overhead to cross the Python-Rust boundary. Enabling this feature will remove these costs and make the `Drop` implementation abort the process if called without the GIL being held instead. From 3496f491a1539356632b3a3d7affd031fe2e155d Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Wed, 4 Sep 2024 10:08:00 +0200 Subject: [PATCH 261/495] update `complexobject.rs` for 3.13 (#4521) --- newsfragments/4521.removed.md | 1 + pyo3-ffi/src/complexobject.rs | 36 ++------------------------- pyo3-ffi/src/cpython/complexobject.rs | 31 +++++++++++++++++++++++ pyo3-ffi/src/cpython/mod.rs | 2 ++ 4 files changed, 36 insertions(+), 34 deletions(-) create mode 100644 newsfragments/4521.removed.md create mode 100644 pyo3-ffi/src/cpython/complexobject.rs diff --git a/newsfragments/4521.removed.md b/newsfragments/4521.removed.md new file mode 100644 index 00000000000..3ef52c5515d --- /dev/null +++ b/newsfragments/4521.removed.md @@ -0,0 +1 @@ +Remove private FFI definitions `_Py_c_sum`, `_Py_c_diff`, `_Py_c_neg`, `_Py_c_prod`, `_Py_c_quot`, `_Py_c_pow`, `_Py_c_abs`. diff --git a/pyo3-ffi/src/complexobject.rs b/pyo3-ffi/src/complexobject.rs index 78bb9ccaaaf..283bacf6e84 100644 --- a/pyo3-ffi/src/complexobject.rs +++ b/pyo3-ffi/src/complexobject.rs @@ -2,37 +2,6 @@ use crate::object::*; use std::os::raw::{c_double, c_int}; use std::ptr::addr_of_mut; -#[repr(C)] -#[derive(Copy, Clone)] -// non-limited -pub struct Py_complex { - pub real: c_double, - pub imag: c_double, -} - -#[cfg(not(Py_LIMITED_API))] -extern "C" { - pub fn _Py_c_sum(left: Py_complex, right: Py_complex) -> Py_complex; - pub fn _Py_c_diff(left: Py_complex, right: Py_complex) -> Py_complex; - pub fn _Py_c_neg(complex: Py_complex) -> Py_complex; - pub fn _Py_c_prod(left: Py_complex, right: Py_complex) -> Py_complex; - pub fn _Py_c_quot(dividend: Py_complex, divisor: Py_complex) -> Py_complex; - pub fn _Py_c_pow(num: Py_complex, exp: Py_complex) -> Py_complex; - pub fn _Py_c_abs(arg: Py_complex) -> c_double; - #[cfg_attr(PyPy, link_name = "PyPyComplex_FromCComplex")] - pub fn PyComplex_FromCComplex(v: Py_complex) -> *mut PyObject; - #[cfg_attr(PyPy, link_name = "PyPyComplex_AsCComplex")] - pub fn PyComplex_AsCComplex(op: *mut PyObject) -> Py_complex; -} - -#[repr(C)] -// non-limited -pub struct PyComplexObject { - pub ob_base: PyObject, - #[cfg(not(GraalPy))] - pub cval: Py_complex, -} - #[cfg_attr(windows, link(name = "pythonXY"))] extern "C" { #[cfg_attr(PyPy, link_name = "PyPyComplex_Type")] @@ -46,17 +15,16 @@ pub unsafe fn PyComplex_Check(op: *mut PyObject) -> c_int { #[inline] pub unsafe fn PyComplex_CheckExact(op: *mut PyObject) -> c_int { - (Py_TYPE(op) == addr_of_mut!(PyComplex_Type)) as c_int + Py_IS_TYPE(op, addr_of_mut!(PyComplex_Type)) } extern "C" { // skipped non-limited PyComplex_FromCComplex #[cfg_attr(PyPy, link_name = "PyPyComplex_FromDoubles")] pub fn PyComplex_FromDoubles(real: c_double, imag: c_double) -> *mut PyObject; + #[cfg_attr(PyPy, link_name = "PyPyComplex_RealAsDouble")] pub fn PyComplex_RealAsDouble(op: *mut PyObject) -> c_double; #[cfg_attr(PyPy, link_name = "PyPyComplex_ImagAsDouble")] pub fn PyComplex_ImagAsDouble(op: *mut PyObject) -> c_double; - // skipped non-limited PyComplex_AsCComplex - // skipped non-limited _PyComplex_FormatAdvancedWriter } diff --git a/pyo3-ffi/src/cpython/complexobject.rs b/pyo3-ffi/src/cpython/complexobject.rs new file mode 100644 index 00000000000..255f9c27034 --- /dev/null +++ b/pyo3-ffi/src/cpython/complexobject.rs @@ -0,0 +1,31 @@ +use crate::PyObject; +use std::os::raw::c_double; + +#[repr(C)] +#[derive(Copy, Clone)] +pub struct Py_complex { + pub real: c_double, + pub imag: c_double, +} + +// skipped private function _Py_c_sum +// skipped private function _Py_c_diff +// skipped private function _Py_c_neg +// skipped private function _Py_c_prod +// skipped private function _Py_c_quot +// skipped private function _Py_c_pow +// skipped private function _Py_c_abs + +#[repr(C)] +pub struct PyComplexObject { + pub ob_base: PyObject, + #[cfg(not(GraalPy))] + pub cval: Py_complex, +} + +extern "C" { + #[cfg_attr(PyPy, link_name = "PyPyComplex_FromCComplex")] + pub fn PyComplex_FromCComplex(v: Py_complex) -> *mut PyObject; + #[cfg_attr(PyPy, link_name = "PyPyComplex_AsCComplex")] + pub fn PyComplex_AsCComplex(op: *mut PyObject) -> Py_complex; +} diff --git a/pyo3-ffi/src/cpython/mod.rs b/pyo3-ffi/src/cpython/mod.rs index fad9cd72f66..fe909f0ceeb 100644 --- a/pyo3-ffi/src/cpython/mod.rs +++ b/pyo3-ffi/src/cpython/mod.rs @@ -5,6 +5,7 @@ pub(crate) mod bytesobject; pub(crate) mod ceval; pub(crate) mod code; pub(crate) mod compile; +pub(crate) mod complexobject; #[cfg(Py_3_13)] pub(crate) mod critical_section; pub(crate) mod descrobject; @@ -47,6 +48,7 @@ pub use self::bytesobject::*; pub use self::ceval::*; pub use self::code::*; pub use self::compile::*; +pub use self::complexobject::*; #[cfg(Py_3_13)] pub use self::critical_section::*; pub use self::descrobject::*; From 3d5d4087cff2b4f4bca91d048849d190c9c462e1 Mon Sep 17 00:00:00 2001 From: Georg Brandl Date: Wed, 4 Sep 2024 12:34:46 +0200 Subject: [PATCH 262/495] type_object: fix new clippy complaint about length of doc comment (#4527) * type_object: fix new clippy complaint about length of doc comment * all: replace minor version specific links to CPython docs --- guide/src/python-from-rust/calling-existing-code.md | 4 ++-- src/type_object.rs | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/guide/src/python-from-rust/calling-existing-code.md b/guide/src/python-from-rust/calling-existing-code.md index a372ece2808..0e986562e19 100644 --- a/guide/src/python-from-rust/calling-existing-code.md +++ b/guide/src/python-from-rust/calling-existing-code.md @@ -27,7 +27,7 @@ fn main() -> PyResult<()> { ## Want to run just an expression? Then use `eval_bound`. [`Python::eval_bound`]({{#PYO3_DOCS_URL}}/pyo3/marker/struct.Python.html#method.eval_bound) is -a method to execute a [Python expression](https://docs.python.org/3.7/reference/expressions.html) +a method to execute a [Python expression](https://docs.python.org/3/reference/expressions.html) and return the evaluated value as a `Bound<'py, PyAny>` object. ```rust @@ -51,7 +51,7 @@ Python::with_gil(|py| { ## Want to run statements? Then use `run_bound`. [`Python::run_bound`] is a method to execute one or more -[Python statements](https://docs.python.org/3.7/reference/simple_stmts.html). +[Python statements](https://docs.python.org/3/reference/simple_stmts.html). This method returns nothing (like any Python statement), but you can get access to manipulated objects via the `locals` dict. diff --git a/src/type_object.rs b/src/type_object.rs index 72a87b2805b..df359227365 100644 --- a/src/type_object.rs +++ b/src/type_object.rs @@ -17,7 +17,8 @@ use crate::{ffi, Bound, Python}; pub unsafe trait PyLayout {} /// `T: PySizedLayout` represents that `T` is not a instance of -/// [`PyVarObject`](https://docs.python.org/3.8/c-api/structures.html?highlight=pyvarobject#c.PyVarObject). +/// [`PyVarObject`](https://docs.python.org/3/c-api/structures.html#c.PyVarObject). +/// /// In addition, that `T` is a concrete representation of `U`. pub trait PySizedLayout: PyLayout + Sized {} From 286ddab5df2bb1dc2977f5e17636433310cc0332 Mon Sep 17 00:00:00 2001 From: Georg Brandl Date: Wed, 4 Sep 2024 14:59:06 +0200 Subject: [PATCH 263/495] examples/plugin: fix to use Bound (#4525) --- examples/plugin/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/plugin/src/main.rs b/examples/plugin/src/main.rs index b50b54548e5..59442549e6d 100644 --- a/examples/plugin/src/main.rs +++ b/examples/plugin/src/main.rs @@ -13,7 +13,7 @@ fn main() -> Result<(), Box> { //do useful work Python::with_gil(|py| { //add the current directory to import path of Python (do not use this in production!) - let syspath: &PyList = py.import("sys")?.getattr("path")?.extract()?; + let syspath: Bound = py.import("sys")?.getattr("path")?.extract()?; syspath.insert(0, &path)?; println!("Import path is: {:?}", syspath); From 8ee5510b8a1384f99514f1ec91bff5b651bc0073 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Wed, 4 Sep 2024 11:26:49 -0600 Subject: [PATCH 264/495] Remove GILProtected on free-threaded build (#4504) * do not define GILProtected if Py_GIL_DISABLED is set * add stub for migration guide on free-threaded support * remove internal uses of GILProtected on gil-enabled builds as well * add newsfragment * flesh out documentation * fixup migration guide examples * simplify migration guide example --- guide/src/migration.md | 74 +++++++++++++++++++++++++++ newsfragments/4504.changed.md | 2 + src/impl_/pyclass/lazy_type_object.rs | 24 +++++---- src/sync.rs | 10 +++- 4 files changed, 98 insertions(+), 12 deletions(-) create mode 100644 newsfragments/4504.changed.md diff --git a/guide/src/migration.md b/guide/src/migration.md index e366c825a3b..fb212743702 100644 --- a/guide/src/migration.md +++ b/guide/src/migration.md @@ -198,6 +198,80 @@ impl<'a, 'py> IntoPyObject<'py> for &'a MyPyObjectWrapper { ``` +### Free-threaded Python Support +
+Click to expand + +PyO3 0.23 introduces preliminary support for the new free-threaded build of +CPython 3.13. PyO3 features that implicitly assumed the existence of the GIL +are not exposed in the free-threaded build, since they are no longer safe. + +If you make use of these features then you will need to account for the +unavailability of this API in the free-threaded build. One way to handle it is +via conditional compilation -- extensions built for the free-threaded build will +have the `Py_GIL_DISABLED` attribute defined. + +### `GILProtected` + +`GILProtected` allows mutable access to static data by leveraging the GIL to +lock concurrent access from other threads. In free-threaded python there is no +GIL, so you will need to replace this type with some other form of locking. In +many cases, `std::sync::Atomic` or `std::sync::Mutex` will be sufficient. If the +locks do not guard the execution of arbitrary Python code or use of the CPython +C API then conditional compilation is likely unnecessary since `GILProtected` +was not needed in the first place. + +Before: + +```rust +# fn main() { +# #[cfg(not(Py_GIL_DISABLED))] { +# use pyo3::prelude::*; +use pyo3::sync::GILProtected; +use pyo3::types::{PyDict, PyNone}; +use std::cell::RefCell; + +static OBJECTS: GILProtected>>> = + GILProtected::new(RefCell::new(Vec::new())); + +Python::with_gil(|py| { + // stand-in for something that executes arbitrary python code + let d = PyDict::new(py); + d.set_item(PyNone::get(py), PyNone::get(py)).unwrap(); + OBJECTS.get(py).borrow_mut().push(d.unbind()); +}); +# }} +``` + +After: + +```rust +# use pyo3::prelude::*; +# fn main() { +use pyo3::types::{PyDict, PyNone}; +use std::sync::Mutex; + +static OBJECTS: Mutex>> = Mutex::new(Vec::new()); + +Python::with_gil(|py| { + // stand-in for something that executes arbitrary python code + let d = PyDict::new(py); + d.set_item(PyNone::get(py), PyNone::get(py)).unwrap(); + // we're not executing python code while holding the lock, so GILProtected + // was never needed + OBJECTS.lock().unwrap().push(d.unbind()); +}); +# } +``` + +If you are executing arbitrary Python code while holding the lock, then you will +need to use conditional compilation to use `GILProtected` on GIL-enabled python +builds and mutexes otherwise. Python 3.13 introduces `PyMutex`, which releases +the GIL while the lock is held, so that is another option if you only need to +support newer Python versions. + +
+ ## from 0.21.* to 0.22 ### Deprecation of `gil-refs` feature continues diff --git a/newsfragments/4504.changed.md b/newsfragments/4504.changed.md new file mode 100644 index 00000000000..94d056dcef9 --- /dev/null +++ b/newsfragments/4504.changed.md @@ -0,0 +1,2 @@ +* The `GILProtected` struct is not available on the free-threaded build of + Python 3.13. diff --git a/src/impl_/pyclass/lazy_type_object.rs b/src/impl_/pyclass/lazy_type_object.rs index be383a272f3..d3bede7b2f3 100644 --- a/src/impl_/pyclass/lazy_type_object.rs +++ b/src/impl_/pyclass/lazy_type_object.rs @@ -1,5 +1,4 @@ use std::{ - cell::RefCell, ffi::CStr, marker::PhantomData, thread::{self, ThreadId}, @@ -11,11 +10,13 @@ use crate::{ impl_::pyclass::MaybeRuntimePyMethodDef, impl_::pymethods::PyMethodDefType, pyclass::{create_type_object, PyClassTypeObject}, - sync::{GILOnceCell, GILProtected}, + sync::GILOnceCell, types::PyType, Bound, PyClass, PyErr, PyObject, PyResult, Python, }; +use std::sync::Mutex; + use super::PyClassItemsIter; /// Lazy type object for PyClass. @@ -27,7 +28,7 @@ struct LazyTypeObjectInner { value: GILOnceCell, // Threads which have begun initialization of the `tp_dict`. Used for // reentrant initialization detection. - initializing_threads: GILProtected>>, + initializing_threads: Mutex>, tp_dict_filled: GILOnceCell<()>, } @@ -38,7 +39,7 @@ impl LazyTypeObject { LazyTypeObject( LazyTypeObjectInner { value: GILOnceCell::new(), - initializing_threads: GILProtected::new(RefCell::new(Vec::new())), + initializing_threads: Mutex::new(Vec::new()), tp_dict_filled: GILOnceCell::new(), }, PhantomData, @@ -117,7 +118,7 @@ impl LazyTypeObjectInner { let thread_id = thread::current().id(); { - let mut threads = self.initializing_threads.get(py).borrow_mut(); + let mut threads = self.initializing_threads.lock().unwrap(); if threads.contains(&thread_id) { // Reentrant call: just return the type object, even if the // `tp_dict` is not filled yet. @@ -127,20 +128,18 @@ impl LazyTypeObjectInner { } struct InitializationGuard<'a> { - initializing_threads: &'a GILProtected>>, - py: Python<'a>, + initializing_threads: &'a Mutex>, thread_id: ThreadId, } impl Drop for InitializationGuard<'_> { fn drop(&mut self) { - let mut threads = self.initializing_threads.get(self.py).borrow_mut(); + let mut threads = self.initializing_threads.lock().unwrap(); threads.retain(|id| *id != self.thread_id); } } let guard = InitializationGuard { initializing_threads: &self.initializing_threads, - py, thread_id, }; @@ -185,8 +184,11 @@ impl LazyTypeObjectInner { // Initialization successfully complete, can clear the thread list. // (No further calls to get_or_init() will try to init, on any thread.) - std::mem::forget(guard); - self.initializing_threads.get(py).replace(Vec::new()); + let mut threads = { + drop(guard); + self.initializing_threads.lock().unwrap() + }; + threads.clear(); result }); diff --git a/src/sync.rs b/src/sync.rs index 33d247b7856..59f669f2627 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -6,16 +6,21 @@ //! [PEP 703]: https://peps.python.org/pep-703/ use crate::{ types::{any::PyAnyMethods, PyString, PyType}, - Bound, Py, PyResult, PyVisit, Python, + Bound, Py, PyResult, Python, }; use std::cell::UnsafeCell; +#[cfg(not(Py_GIL_DISABLED))] +use crate::PyVisit; + /// Value with concurrent access protected by the GIL. /// /// This is a synchronization primitive based on Python's global interpreter lock (GIL). /// It ensures that only one thread at a time can access the inner value via shared references. /// It can be combined with interior mutability to obtain mutable references. /// +/// This type is not defined for extensions built against the free-threaded CPython ABI. +/// /// # Example /// /// Combining `GILProtected` with `RefCell` enables mutable access to static data: @@ -31,10 +36,12 @@ use std::cell::UnsafeCell; /// NUMBERS.get(py).borrow_mut().push(42); /// }); /// ``` +#[cfg(not(Py_GIL_DISABLED))] pub struct GILProtected { value: T, } +#[cfg(not(Py_GIL_DISABLED))] impl GILProtected { /// Place the given value under the protection of the GIL. pub const fn new(value: T) -> Self { @@ -52,6 +59,7 @@ impl GILProtected { } } +#[cfg(not(Py_GIL_DISABLED))] unsafe impl Sync for GILProtected where T: Send {} /// A write-once cell similar to [`once_cell::OnceCell`](https://docs.rs/once_cell/latest/once_cell/). From 699de421b430c791551dbeae26b1b718476410b2 Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Thu, 5 Sep 2024 21:05:41 +0200 Subject: [PATCH 265/495] ci: updates for Rust 1.81 (#4533) --- src/conversions/anyhow.rs | 7 +++---- tests/ui/invalid_cancel_handle.stderr | 6 +----- tests/ui/invalid_intern_arg.stderr | 11 +++++++---- tests/ui/invalid_property_args.stderr | 10 ++++++++++ tests/ui/invalid_pyfunctions.stderr | 12 ++++++------ tests/ui/invalid_pymethod_receiver.stderr | 10 +++++----- tests/ui/invalid_pymethods.stderr | 10 +++++----- tests/ui/invalid_result_conversion.stderr | 16 ++++++++-------- 8 files changed, 45 insertions(+), 37 deletions(-) diff --git a/src/conversions/anyhow.rs b/src/conversions/anyhow.rs index de2c5c9dc90..9e27985c152 100644 --- a/src/conversions/anyhow.rs +++ b/src/conversions/anyhow.rs @@ -1,9 +1,6 @@ #![cfg(feature = "anyhow")] -//! A conversion from -//! [anyhow](https://docs.rs/anyhow/ "A trait object based error system for easy idiomatic error handling in Rust applications.")’s -//! [`Error`](https://docs.rs/anyhow/latest/anyhow/struct.Error.html "Anyhows `Error` type, a wrapper around a dynamic error type") -//! type to [`PyErr`]. +//! A conversion from [anyhow]’s [`Error`][anyhow-error] type to [`PyErr`]. //! //! Use of an error handling library like [anyhow] is common in application code and when you just //! want error handling to be easy. If you are writing a library or you need more control over your @@ -99,6 +96,8 @@ //! } //! ``` //! +//! [anyhow]: https://docs.rs/anyhow/ "A trait object based error system for easy idiomatic error handling in Rust applications." +//! [anyhow-error]: https://docs.rs/anyhow/latest/anyhow/struct.Error.html "Anyhows `Error` type, a wrapper around a dynamic error type" //! [`RuntimeError`]: https://docs.python.org/3/library/exceptions.html#RuntimeError "Built-in Exceptions — Python documentation" //! [Error handling]: https://doc.rust-lang.org/book/ch09-02-recoverable-errors-with-result.html "Recoverable Errors with Result - The Rust Programming Language" diff --git a/tests/ui/invalid_cancel_handle.stderr b/tests/ui/invalid_cancel_handle.stderr index feb07d60161..bd2b588df32 100644 --- a/tests/ui/invalid_cancel_handle.stderr +++ b/tests/ui/invalid_cancel_handle.stderr @@ -44,11 +44,7 @@ error[E0277]: the trait bound `CancelHandle: PyFunctionArgument<'_, '_>` is not 20 | async fn missing_cancel_handle_attribute(_param: pyo3::coroutine::CancelHandle) {} | ^^^^ the trait `PyClass` is not implemented for `CancelHandle`, which is required by `CancelHandle: PyFunctionArgument<'_, '_>` | - = help: the following other types implement trait `PyFunctionArgument<'a, 'py>`: - &'a mut pyo3::coroutine::Coroutine - &'a pyo3::Bound<'py, T> - &'a pyo3::coroutine::Coroutine - Option<&'a pyo3::Bound<'py, T>> + = help: the trait `PyClass` is implemented for `pyo3::coroutine::Coroutine` = note: required for `CancelHandle` to implement `FromPyObject<'_>` = note: required for `CancelHandle` to implement `FromPyObjectBound<'_, '_>` = note: required for `CancelHandle` to implement `PyFunctionArgument<'_, '_>` diff --git a/tests/ui/invalid_intern_arg.stderr b/tests/ui/invalid_intern_arg.stderr index 7b02b72a214..ab4268310ac 100644 --- a/tests/ui/invalid_intern_arg.stderr +++ b/tests/ui/invalid_intern_arg.stderr @@ -2,10 +2,13 @@ error[E0435]: attempt to use a non-constant value in a constant --> tests/ui/invalid_intern_arg.rs:5:55 | 5 | Python::with_gil(|py| py.import(pyo3::intern!(py, _foo)).unwrap()); - | ------------------^^^^- - | | | - | | non-constant value - | help: consider using `let` instead of `static`: `let INTERNED` + | ^^^^ non-constant value + | +help: consider using `let` instead of `static` + --> src/sync.rs + | + | let INTERNED: $crate::sync::Interned = $crate::sync::Interned::new($text); + | ~~~ error: lifetime may not live long enough --> tests/ui/invalid_intern_arg.rs:5:27 diff --git a/tests/ui/invalid_property_args.stderr b/tests/ui/invalid_property_args.stderr index 0a03969fda8..70f3dd4e8f8 100644 --- a/tests/ui/invalid_property_args.stderr +++ b/tests/ui/invalid_property_args.stderr @@ -54,6 +54,16 @@ error[E0277]: `PhantomData` cannot be converted to a Python object | = help: the trait `IntoPyObject<'_>` is not implemented for `PhantomData`, which is required by `for<'py> PhantomData: PyO3GetField<'py>` = note: implement `IntoPyObject` for `&PhantomData` or `IntoPyObject + Clone` for `PhantomData` to define the conversion + = help: the following other types implement trait `IntoPyObject<'py>`: + &&str + &'a BTreeMap + &'a BTreeSet + &'a Cell + &'a HashMap + &'a HashSet + &'a Option + &'a Py + and $N others = note: required for `PhantomData` to implement `for<'py> PyO3GetField<'py>` note: required by a bound in `PyClassGetterGenerator::::generate` --> src/impl_/pyclass.rs diff --git a/tests/ui/invalid_pyfunctions.stderr b/tests/ui/invalid_pyfunctions.stderr index 9a42c59366e..ab35b086b94 100644 --- a/tests/ui/invalid_pyfunctions.stderr +++ b/tests/ui/invalid_pyfunctions.stderr @@ -54,10 +54,10 @@ error[E0277]: the trait bound `&str: From>` is not implemented for `&str`, which is required by `BoundRef<'_, '_, pyo3::types::PyModule>: Into<_>` | = help: the following other types implement trait `From`: - > - > - > - >> - >> - > + `String` implements `From<&String>` + `String` implements `From<&mut str>` + `String` implements `From<&str>` + `String` implements `From>` + `String` implements `From>` + `String` implements `From` = note: required for `BoundRef<'_, '_, pyo3::types::PyModule>` to implement `Into<&str>` diff --git a/tests/ui/invalid_pymethod_receiver.stderr b/tests/ui/invalid_pymethod_receiver.stderr index 3a8356c4b76..9c998403194 100644 --- a/tests/ui/invalid_pymethod_receiver.stderr +++ b/tests/ui/invalid_pymethod_receiver.stderr @@ -5,10 +5,10 @@ error[E0277]: the trait bound `i32: TryFrom>` is not s | ^^^ the trait `From>` is not implemented for `i32`, which is required by `i32: TryFrom>` | = help: the following other types implement trait `From`: - > - > - > - > - > + `i32` implements `From` + `i32` implements `From` + `i32` implements `From` + `i32` implements `From` + `i32` implements `From` = note: required for `BoundRef<'_, '_, MyClass>` to implement `Into` = note: required for `i32` to implement `TryFrom>` diff --git a/tests/ui/invalid_pymethods.stderr b/tests/ui/invalid_pymethods.stderr index 879cd3c7ece..845b79ed59a 100644 --- a/tests/ui/invalid_pymethods.stderr +++ b/tests/ui/invalid_pymethods.stderr @@ -186,9 +186,9 @@ error[E0277]: the trait bound `i32: From>` is not satis | ^^^ the trait `From>` is not implemented for `i32`, which is required by `BoundRef<'_, '_, PyType>: Into<_>` | = help: the following other types implement trait `From`: - > - > - > - > - > + `i32` implements `From` + `i32` implements `From` + `i32` implements `From` + `i32` implements `From` + `i32` implements `From` = note: required for `BoundRef<'_, '_, PyType>` to implement `Into` diff --git a/tests/ui/invalid_result_conversion.stderr b/tests/ui/invalid_result_conversion.stderr index b34c6396f00..18667138954 100644 --- a/tests/ui/invalid_result_conversion.stderr +++ b/tests/ui/invalid_result_conversion.stderr @@ -5,13 +5,13 @@ error[E0277]: the trait bound `PyErr: From` is not satisfied | ^^^^^^ the trait `From` is not implemented for `PyErr`, which is required by `MyError: Into` | = help: the following other types implement trait `From`: - > - > - >> - >> - > - > - > - >> + `PyErr` implements `From` + `PyErr` implements `From` + `PyErr` implements `From>` + `PyErr` implements `From>` + `PyErr` implements `From` + `PyErr` implements `From` + `PyErr` implements `From` + `PyErr` implements `From>` and $N others = note: required for `MyError` to implement `Into` From e2a1da08c37bdacbd93fe92e17f73742aeedfe03 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Fri, 6 Sep 2024 06:56:42 +0200 Subject: [PATCH 266/495] add `Borrowed::as_ptr` (#4520) --- newsfragments/4520.added.md | 1 + src/conversions/chrono.rs | 2 +- src/coroutine.rs | 5 +-- src/instance.rs | 19 +++++++--- src/types/any.rs | 72 ++++++++++++++++++------------------- src/types/dict.rs | 12 ++----- src/types/mapping.rs | 2 +- src/types/sequence.rs | 2 +- tests/test_coroutine.rs | 5 +-- 9 files changed, 57 insertions(+), 63 deletions(-) create mode 100644 newsfragments/4520.added.md diff --git a/newsfragments/4520.added.md b/newsfragments/4520.added.md new file mode 100644 index 00000000000..d9952934de5 --- /dev/null +++ b/newsfragments/4520.added.md @@ -0,0 +1 @@ +Add `Borrowed::as_ptr`. diff --git a/src/conversions/chrono.rs b/src/conversions/chrono.rs index e33c12b93e7..ddd4e39d9e3 100644 --- a/src/conversions/chrono.rs +++ b/src/conversions/chrono.rs @@ -710,7 +710,7 @@ fn warn_truncated_leap_second(obj: &Bound<'_, PyAny>) { ffi::c_str!("ignored leap-second, `datetime` does not support leap-seconds"), 0, ) { - e.write_unraisable(py, Some(&obj.as_borrowed())) + e.write_unraisable(py, Some(obj)) }; } diff --git a/src/coroutine.rs b/src/coroutine.rs index 22aad47f5c6..82f5460f03e 100644 --- a/src/coroutine.rs +++ b/src/coroutine.rs @@ -107,10 +107,7 @@ impl Coroutine { if let Some(future) = self.waker.as_ref().unwrap().initialize_future(py)? { // `asyncio.Future` must be awaited; fortunately, it implements `__iter__ = __await__` // and will yield itself if its result has not been set in polling above - if let Some(future) = PyIterator::from_object(&future.as_borrowed()) - .unwrap() - .next() - { + if let Some(future) = PyIterator::from_object(future).unwrap().next() { // future has not been leaked into Python for now, and Rust code can only call // `set_result(None)` in `Wake` implementation, so it's safe to unwrap return Ok(future.unwrap().into()); diff --git a/src/instance.rs b/src/instance.rs index c71cf89c02d..26c6b14ffa0 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -659,6 +659,19 @@ impl<'a, 'py, T> Borrowed<'a, 'py, T> { (*self).clone() } + /// Returns the raw FFI pointer represented by self. + /// + /// # Safety + /// + /// Callers are responsible for ensuring that the pointer does not outlive self. + /// + /// The reference is borrowed; callers should not decrease the reference count + /// when they are finished with the pointer. + #[inline] + pub fn as_ptr(self) -> *mut ffi::PyObject { + self.0.as_ptr() + } + pub(crate) fn to_any(self) -> Borrowed<'a, 'py, PyAny> { Borrowed(self.0, PhantomData, self.2) } @@ -2062,11 +2075,7 @@ a = A() #[test] fn test_py2_into_py_object() { Python::with_gil(|py| { - let instance = py - .eval(ffi::c_str!("object()"), None, None) - .unwrap() - .as_borrowed() - .to_owned(); + let instance = py.eval(ffi::c_str!("object()"), None, None).unwrap(); let ptr = instance.as_ptr(); let instance: PyObject = instance.clone().unbind(); assert_eq!(instance.as_ptr(), ptr); diff --git a/src/types/any.rs b/src/types/any.rs index d9aa845106e..5e40a0fdc4b 100644 --- a/src/types/any.rs +++ b/src/types/any.rs @@ -10,7 +10,7 @@ use crate::type_object::{PyTypeCheck, PyTypeInfo}; #[cfg(not(any(PyPy, GraalPy)))] use crate::types::PySuper; use crate::types::{PyDict, PyIterator, PyList, PyString, PyTuple, PyType}; -use crate::{err, ffi, BoundObject, Py, Python}; +use crate::{err, ffi, Borrowed, BoundObject, Py, Python}; use std::cell::UnsafeCell; use std::cmp::Ordering; use std::os::raw::c_int; @@ -887,7 +887,7 @@ macro_rules! implement_binop { { fn inner<'py>( any: &Bound<'py, PyAny>, - other: &Bound<'_, PyAny>, + other: Borrowed<'_, 'py, PyAny>, ) -> PyResult> { unsafe { ffi::$c_api(any.as_ptr(), other.as_ptr()).assume_owned_or_err(any.py()) } } @@ -895,7 +895,7 @@ macro_rules! implement_binop { let py = self.py(); inner( self, - &other + other .into_pyobject(py) .map_err(Into::into)? .into_any() @@ -934,7 +934,7 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { { fn inner<'py>( any: &Bound<'py, PyAny>, - attr_name: &Bound<'_, PyString>, + attr_name: Borrowed<'_, '_, PyString>, ) -> PyResult> { unsafe { ffi::PyObject_GetAttr(any.as_ptr(), attr_name.as_ptr()) @@ -944,7 +944,7 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { inner( self, - &attr_name + attr_name .into_pyobject(self.py()) .map_err(Into::into)? .as_borrowed(), @@ -958,8 +958,8 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { { fn inner( any: &Bound<'_, PyAny>, - attr_name: &Bound<'_, PyString>, - value: &Bound<'_, PyAny>, + attr_name: Borrowed<'_, '_, PyString>, + value: Borrowed<'_, '_, PyAny>, ) -> PyResult<()> { err::error_on_minusone(any.py(), unsafe { ffi::PyObject_SetAttr(any.as_ptr(), attr_name.as_ptr(), value.as_ptr()) @@ -969,11 +969,11 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { let py = self.py(); inner( self, - &attr_name + attr_name .into_pyobject(py) .map_err(Into::into)? .as_borrowed(), - &value + value .into_pyobject(py) .map_err(Into::into)? .into_any() @@ -985,7 +985,7 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { where N: IntoPyObject<'py, Target = PyString>, { - fn inner(any: &Bound<'_, PyAny>, attr_name: &Bound<'_, PyString>) -> PyResult<()> { + fn inner(any: &Bound<'_, PyAny>, attr_name: Borrowed<'_, '_, PyString>) -> PyResult<()> { err::error_on_minusone(any.py(), unsafe { ffi::PyObject_DelAttr(any.as_ptr(), attr_name.as_ptr()) }) @@ -994,7 +994,7 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { let py = self.py(); inner( self, - &attr_name + attr_name .into_pyobject(py) .map_err(Into::into)? .as_borrowed(), @@ -1005,7 +1005,7 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { where O: IntoPyObject<'py>, { - fn inner(any: &Bound<'_, PyAny>, other: &Bound<'_, PyAny>) -> PyResult { + fn inner(any: &Bound<'_, PyAny>, other: Borrowed<'_, '_, PyAny>) -> PyResult { let other = other.as_ptr(); // Almost the same as ffi::PyObject_RichCompareBool, but this one doesn't try self == other. // See https://github.com/PyO3/pyo3/issues/985 for more. @@ -1030,7 +1030,7 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { let py = self.py(); inner( self, - &other + other .into_pyobject(py) .map_err(Into::into)? .into_any() @@ -1044,7 +1044,7 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { { fn inner<'py>( any: &Bound<'py, PyAny>, - other: &Bound<'_, PyAny>, + other: Borrowed<'_, 'py, PyAny>, compare_op: CompareOp, ) -> PyResult> { unsafe { @@ -1056,7 +1056,7 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { let py = self.py(); inner( self, - &other + other .into_pyobject(py) .map_err(Into::into)? .into_any() @@ -1161,7 +1161,7 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { { fn inner<'py>( any: &Bound<'py, PyAny>, - other: &Bound<'_, PyAny>, + other: Borrowed<'_, 'py, PyAny>, ) -> PyResult> { unsafe { ffi::PyNumber_Divmod(any.as_ptr(), other.as_ptr()).assume_owned_or_err(any.py()) @@ -1171,7 +1171,7 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { let py = self.py(); inner( self, - &other + other .into_pyobject(py) .map_err(Into::into)? .into_any() @@ -1188,8 +1188,8 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { { fn inner<'py>( any: &Bound<'py, PyAny>, - other: &Bound<'_, PyAny>, - modulus: &Bound<'_, PyAny>, + other: Borrowed<'_, 'py, PyAny>, + modulus: Borrowed<'_, 'py, PyAny>, ) -> PyResult> { unsafe { ffi::PyNumber_Power(any.as_ptr(), other.as_ptr(), modulus.as_ptr()) @@ -1200,12 +1200,12 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { let py = self.py(); inner( self, - &other + other .into_pyobject(py) .map_err(Into::into)? .into_any() .as_borrowed(), - &modulus + modulus .into_pyobject(py) .map_err(Into::into)? .into_any() @@ -1314,7 +1314,7 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { { fn inner<'py>( any: &Bound<'py, PyAny>, - key: &Bound<'_, PyAny>, + key: Borrowed<'_, 'py, PyAny>, ) -> PyResult> { unsafe { ffi::PyObject_GetItem(any.as_ptr(), key.as_ptr()).assume_owned_or_err(any.py()) @@ -1324,7 +1324,7 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { let py = self.py(); inner( self, - &key.into_pyobject(py) + key.into_pyobject(py) .map_err(Into::into)? .into_any() .as_borrowed(), @@ -1338,8 +1338,8 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { { fn inner( any: &Bound<'_, PyAny>, - key: &Bound<'_, PyAny>, - value: &Bound<'_, PyAny>, + key: Borrowed<'_, '_, PyAny>, + value: Borrowed<'_, '_, PyAny>, ) -> PyResult<()> { err::error_on_minusone(any.py(), unsafe { ffi::PyObject_SetItem(any.as_ptr(), key.as_ptr(), value.as_ptr()) @@ -1349,11 +1349,11 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { let py = self.py(); inner( self, - &key.into_pyobject(py) + key.into_pyobject(py) .map_err(Into::into)? .into_any() .as_borrowed(), - &value + value .into_pyobject(py) .map_err(Into::into)? .into_any() @@ -1365,7 +1365,7 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { where K: IntoPyObject<'py>, { - fn inner(any: &Bound<'_, PyAny>, key: &Bound<'_, PyAny>) -> PyResult<()> { + fn inner(any: &Bound<'_, PyAny>, key: Borrowed<'_, '_, PyAny>) -> PyResult<()> { err::error_on_minusone(any.py(), unsafe { ffi::PyObject_DelItem(any.as_ptr(), key.as_ptr()) }) @@ -1374,7 +1374,7 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { let py = self.py(); inner( self, - &key.into_pyobject(py) + key.into_pyobject(py) .map_err(Into::into)? .into_any() .as_borrowed(), @@ -1529,7 +1529,7 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { where V: IntoPyObject<'py>, { - fn inner(any: &Bound<'_, PyAny>, value: &Bound<'_, PyAny>) -> PyResult { + fn inner(any: &Bound<'_, PyAny>, value: Borrowed<'_, '_, PyAny>) -> PyResult { match unsafe { ffi::PySequence_Contains(any.as_ptr(), value.as_ptr()) } { 0 => Ok(false), 1 => Ok(true), @@ -1540,7 +1540,7 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { let py = self.py(); inner( self, - &value + value .into_pyobject(py) .map_err(Into::into)? .into_any() @@ -1593,11 +1593,7 @@ impl<'py> Bound<'py, PyAny> { let ret = descr_get(attr.as_ptr(), self.as_ptr(), self_type.as_ptr()); ret.assume_owned_or_err(py).map(Some) } - } else if let Ok(descr_get) = attr - .get_type() - .as_borrowed() - .getattr(crate::intern!(py, "__get__")) - { + } else if let Ok(descr_get) = attr.get_type().getattr(crate::intern!(py, "__get__")) { descr_get.call1((attr, self, self_type)).map(Some) } else { Ok(Some(attr)) @@ -1670,7 +1666,7 @@ class NonHeapNonDescriptorInt: let no_descriptor = module.getattr("NoDescriptorInt").unwrap().call0().unwrap(); assert_eq!(eval_int(no_descriptor).unwrap(), 1); let missing = module.getattr("NoInt").unwrap().call0().unwrap(); - assert!(missing.as_borrowed().lookup_special(int).unwrap().is_none()); + assert!(missing.lookup_special(int).unwrap().is_none()); // Note the instance override should _not_ call the instance method that returns 2, // because that's not how special lookups are meant to work. let instance_override = module.getattr("instance_override").unwrap(); @@ -1680,7 +1676,7 @@ class NonHeapNonDescriptorInt: .unwrap() .call0() .unwrap(); - assert!(descriptor_error.as_borrowed().lookup_special(int).is_err()); + assert!(descriptor_error.lookup_special(int).is_err()); let nonheap_nondescriptor = module .getattr("NonHeapNonDescriptorInt") .unwrap() diff --git a/src/types/dict.rs b/src/types/dict.rs index 6e29e6edcaa..1be0ed22362 100644 --- a/src/types/dict.rs +++ b/src/types/dict.rs @@ -1189,9 +1189,7 @@ mod tests { Python::with_gil(|py| { let dict = abc_dict(py); let keys = dict.call_method0("keys").unwrap(); - assert!(keys - .is_instance(&py.get_type::().as_borrowed()) - .unwrap()); + assert!(keys.is_instance(&py.get_type::()).unwrap()); }) } @@ -1201,9 +1199,7 @@ mod tests { Python::with_gil(|py| { let dict = abc_dict(py); let values = dict.call_method0("values").unwrap(); - assert!(values - .is_instance(&py.get_type::().as_borrowed()) - .unwrap()); + assert!(values.is_instance(&py.get_type::()).unwrap()); }) } @@ -1213,9 +1209,7 @@ mod tests { Python::with_gil(|py| { let dict = abc_dict(py); let items = dict.call_method0("items").unwrap(); - assert!(items - .is_instance(&py.get_type::().as_borrowed()) - .unwrap()); + assert!(items.is_instance(&py.get_type::()).unwrap()); }) } diff --git a/src/types/mapping.rs b/src/types/mapping.rs index 009c5c0e5e2..0d1467c27bf 100644 --- a/src/types/mapping.rs +++ b/src/types/mapping.rs @@ -177,7 +177,7 @@ impl PyTypeCheck for PyMapping { || get_mapping_abc(object.py()) .and_then(|abc| object.is_instance(abc)) .unwrap_or_else(|err| { - err.write_unraisable(object.py(), Some(&object.as_borrowed())); + err.write_unraisable(object.py(), Some(object)); false }) } diff --git a/src/types/sequence.rs b/src/types/sequence.rs index 4fa72f1219f..4dbfedc378b 100644 --- a/src/types/sequence.rs +++ b/src/types/sequence.rs @@ -367,7 +367,7 @@ impl PyTypeCheck for PySequence { || get_sequence_abc(object.py()) .and_then(|abc| object.is_instance(abc)) .unwrap_or_else(|err| { - err.write_unraisable(object.py(), Some(&object.as_borrowed())); + err.write_unraisable(object.py(), Some(object)); false }) } diff --git a/tests/test_coroutine.rs b/tests/test_coroutine.rs index bb6889f4c0d..cdf01b8891e 100644 --- a/tests/test_coroutine.rs +++ b/tests/test_coroutine.rs @@ -69,10 +69,7 @@ fn test_coroutine_qualname() { assert coro.__name__ == name and coro.__qualname__ == qualname "#; let locals = [ - ( - "my_fn", - wrap_pyfunction!(my_fn, gil).unwrap().as_borrowed().as_any(), - ), + ("my_fn", wrap_pyfunction!(my_fn, gil).unwrap().as_any()), ("MyClass", gil.get_type::().as_any()), ] .into_py_dict(gil); From cf5df4aa2e9facaf796b5e49de52b64141efde02 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Thu, 5 Sep 2024 22:57:08 -0600 Subject: [PATCH 267/495] fix typo and missed configs in FFI bindings (#4532) --- pyo3-ffi/src/cpython/lock.rs | 2 +- pyo3-ffi/src/dictobject.rs | 1 + pyo3-ffi/src/listobject.rs | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/pyo3-ffi/src/cpython/lock.rs b/pyo3-ffi/src/cpython/lock.rs index 05778dfe573..6c80b00d3c1 100644 --- a/pyo3-ffi/src/cpython/lock.rs +++ b/pyo3-ffi/src/cpython/lock.rs @@ -10,5 +10,5 @@ pub struct PyMutex { extern "C" { pub fn PyMutex_Lock(m: *mut PyMutex); - pub fn PyMutex_UnLock(m: *mut PyMutex); + pub fn PyMutex_Unlock(m: *mut PyMutex); } diff --git a/pyo3-ffi/src/dictobject.rs b/pyo3-ffi/src/dictobject.rs index 4d8315d441e..710be80243f 100644 --- a/pyo3-ffi/src/dictobject.rs +++ b/pyo3-ffi/src/dictobject.rs @@ -67,6 +67,7 @@ extern "C" { #[cfg_attr(PyPy, link_name = "PyPyDict_DelItemString")] pub fn PyDict_DelItemString(dp: *mut PyObject, key: *const c_char) -> c_int; #[cfg(Py_3_13)] + #[cfg_attr(PyPy, link_name = "PyPyDict_GetItemRef")] pub fn PyDict_GetItemRef( dp: *mut PyObject, key: *mut PyObject, diff --git a/pyo3-ffi/src/listobject.rs b/pyo3-ffi/src/listobject.rs index 1096f2fe0c8..9d8b7ed6a58 100644 --- a/pyo3-ffi/src/listobject.rs +++ b/pyo3-ffi/src/listobject.rs @@ -29,6 +29,7 @@ extern "C" { #[cfg_attr(PyPy, link_name = "PyPyList_GetItem")] pub fn PyList_GetItem(arg1: *mut PyObject, arg2: Py_ssize_t) -> *mut PyObject; #[cfg(Py_3_13)] + #[cfg_attr(PyPy, link_name = "PyPyList_GetItemRef")] pub fn PyList_GetItemRef(arg1: *mut PyObject, arg2: Py_ssize_t) -> *mut PyObject; #[cfg_attr(PyPy, link_name = "PyPyList_SetItem")] pub fn PyList_SetItem(arg1: *mut PyObject, arg2: Py_ssize_t, arg3: *mut PyObject) -> c_int; From 4935033e562b726b9a1d9a69469e4f5ce566b4a4 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Thu, 5 Sep 2024 23:25:57 -0600 Subject: [PATCH 268/495] Add bindings for PyImport_AddModuleRef and use it in Python::run_code (#4529) * add bindings for PyImport_AddModuleRef and use it in Python::run_code * fix compiler error on older pythons * refactor run_code implementation to use smart pointers * fix check-guide * add changelog entry * refactor to only use unsafe where it's needed --- newsfragments/4529.added.md | 1 + pyo3-ffi/src/compat/py_3_13.rs | 13 ++++++++ pyo3-ffi/src/import.rs | 3 ++ src/marker.rs | 56 +++++++++++++++++++--------------- 4 files changed, 49 insertions(+), 24 deletions(-) create mode 100644 newsfragments/4529.added.md diff --git a/newsfragments/4529.added.md b/newsfragments/4529.added.md new file mode 100644 index 00000000000..8a82a942eb6 --- /dev/null +++ b/newsfragments/4529.added.md @@ -0,0 +1 @@ +* Added FFI bindings for `PyImport_AddModuleRef`. diff --git a/pyo3-ffi/src/compat/py_3_13.rs b/pyo3-ffi/src/compat/py_3_13.rs index 75c4cd101ae..94778802987 100644 --- a/pyo3-ffi/src/compat/py_3_13.rs +++ b/pyo3-ffi/src/compat/py_3_13.rs @@ -37,3 +37,16 @@ compat_function!( item } ); + +compat_function!( + originally_defined_for(Py_3_13); + + #[inline] + pub unsafe fn PyImport_AddModuleRef( + name: *const std::os::raw::c_char, + ) -> *mut crate::PyObject { + use crate::{compat::Py_XNewRef, PyImport_AddModule}; + + Py_XNewRef(PyImport_AddModule(name)) + } +); diff --git a/pyo3-ffi/src/import.rs b/pyo3-ffi/src/import.rs index e00843466e8..e15a37b0a72 100644 --- a/pyo3-ffi/src/import.rs +++ b/pyo3-ffi/src/import.rs @@ -30,6 +30,9 @@ extern "C" { pub fn PyImport_AddModuleObject(name: *mut PyObject) -> *mut PyObject; #[cfg_attr(PyPy, link_name = "PyPyImport_AddModule")] pub fn PyImport_AddModule(name: *const c_char) -> *mut PyObject; + #[cfg(Py_3_13)] + #[cfg_attr(PyPy, link_name = "PyPyImport_AddModuleRef")] + pub fn PyImport_AddModuleRef(name: *const c_char) -> *mut PyObject; #[cfg_attr(PyPy, link_name = "PyPyImport_ImportModule")] pub fn PyImport_ImportModule(name: *const c_char) -> *mut PyObject; #[cfg_attr(PyPy, link_name = "PyPyImport_ImportModuleNoBlock")] diff --git a/src/marker.rs b/src/marker.rs index 0df9b69f8ae..8af307621a8 100644 --- a/src/marker.rs +++ b/src/marker.rs @@ -116,7 +116,9 @@ //! [`SendWrapper`]: https://docs.rs/send_wrapper/latest/send_wrapper/struct.SendWrapper.html //! [`Rc`]: std::rc::Rc //! [`Py`]: crate::Py -use crate::err::{self, PyErr, PyResult}; +#[cfg(any(doc, not(Py_3_10)))] +use crate::err::PyErr; +use crate::err::{self, PyResult}; use crate::ffi_ptr_ext::FfiPtrExt; use crate::gil::{GILGuard, SuspendGIL}; use crate::impl_::not_send::NotSend; @@ -633,17 +635,19 @@ impl<'py> Python<'py> { globals: Option<&Bound<'py, PyDict>>, locals: Option<&Bound<'py, PyDict>>, ) -> PyResult> { - unsafe { - let mptr = ffi::PyImport_AddModule(ffi::c_str!("__main__").as_ptr()); - if mptr.is_null() { - return Err(PyErr::fetch(self)); - } - - let globals = globals - .map(|dict| dict.as_ptr()) - .unwrap_or_else(|| ffi::PyModule_GetDict(mptr)); - let locals = locals.map(|dict| dict.as_ptr()).unwrap_or(globals); - + let mptr = unsafe { + ffi::compat::PyImport_AddModuleRef(ffi::c_str!("__main__").as_ptr()) + .assume_owned_or_err(self)? + }; + let attr = mptr.getattr(crate::intern!(self, "__dict__"))?; + let globals = match globals { + Some(globals) => globals, + None => attr.downcast::()?, + }; + let locals = locals.unwrap_or(globals); + + #[cfg(not(Py_3_10))] + { // If `globals` don't provide `__builtins__`, most of the code will fail if Python // version is <3.10. That's probably not what user intended, so insert `__builtins__` // for them. @@ -652,30 +656,31 @@ impl<'py> Python<'py> { // - https://github.com/python/cpython/pull/24564 (the same fix in CPython 3.10) // - https://github.com/PyO3/pyo3/issues/3370 let builtins_s = crate::intern!(self, "__builtins__").as_ptr(); - let has_builtins = ffi::PyDict_Contains(globals, builtins_s); + let has_builtins = unsafe { ffi::PyDict_Contains(globals.as_ptr(), builtins_s) }; if has_builtins == -1 { return Err(PyErr::fetch(self)); } if has_builtins == 0 { // Inherit current builtins. - let builtins = ffi::PyEval_GetBuiltins(); + let builtins = unsafe { ffi::PyEval_GetBuiltins() }; // `PyDict_SetItem` doesn't take ownership of `builtins`, but `PyEval_GetBuiltins` // seems to return a borrowed reference, so no leak here. - if ffi::PyDict_SetItem(globals, builtins_s, builtins) == -1 { + if unsafe { ffi::PyDict_SetItem(globals.as_ptr(), builtins_s, builtins) } == -1 { return Err(PyErr::fetch(self)); } } + } - let code_obj = - ffi::Py_CompileString(code.as_ptr(), ffi::c_str!("").as_ptr(), start); - if code_obj.is_null() { - return Err(PyErr::fetch(self)); - } - let res_ptr = ffi::PyEval_EvalCode(code_obj, globals, locals); - ffi::Py_DECREF(code_obj); + let code_obj = unsafe { + ffi::Py_CompileString(code.as_ptr(), ffi::c_str!("").as_ptr(), start) + .assume_owned_or_err(self)? + }; - res_ptr.assume_owned_or_err(self).downcast_into_unchecked() + unsafe { + ffi::PyEval_EvalCode(code_obj.as_ptr(), globals.as_ptr(), locals.as_ptr()) + .assume_owned_or_err(self) + .downcast_into_unchecked() } } @@ -997,12 +1002,15 @@ mod tests { Python::with_gil(|py| { let namespace = PyDict::new(py); py.run( - ffi::c_str!("class Foo: pass"), + ffi::c_str!("class Foo: pass\na = int(3)"), Some(&namespace), Some(&namespace), ) .unwrap(); assert!(matches!(namespace.get_item("Foo"), Ok(Some(..)))); + assert!(matches!(namespace.get_item("a"), Ok(Some(..)))); + // 3.9 and older did not automatically insert __builtins__ if it wasn't inserted "by hand" + #[cfg(not(Py_3_10))] assert!(matches!(namespace.get_item("__builtins__"), Ok(Some(..)))); }) } From bf1623831673a5b0cd0113c13927ea0832e17c5f Mon Sep 17 00:00:00 2001 From: Georg Brandl Date: Fri, 6 Sep 2024 14:59:39 +0200 Subject: [PATCH 269/495] LICENSE: fix up Apache license (#4535) - remove unneeded indentation - remove header that summarizes the license, text follows anyway - remove "how to apply" footer --- LICENSE-APACHE | 367 ++++++++++++++++++++++++------------------------- 1 file changed, 178 insertions(+), 189 deletions(-) diff --git a/LICENSE-APACHE b/LICENSE-APACHE index fca31990733..72207b851d3 100644 --- a/LICENSE-APACHE +++ b/LICENSE-APACHE @@ -1,189 +1,178 @@ - Copyright (c) 2017-present PyO3 Project and Contributors. https://github.com/PyO3 - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. +Copyright (c) 2017-present PyO3 Project and Contributors. https://github.com/PyO3 + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS From ae2fb49f5f3e12e891042cf827f38080c1f26578 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Fri, 6 Sep 2024 17:28:23 -0600 Subject: [PATCH 270/495] update FFI bindings to reflect deprecated and removed items (#4534) * update FFI bindings to reflect deprecated and removed items * Update ceval.rs Co-authored-by: Lily Foote * fix ffi-check * add changelog entries --------- Co-authored-by: Lily Foote --- newsfragments/4534.changed.md | 3 ++ newsfragments/4534.removed.md | 1 + pyo3-ffi-check/src/main.rs | 3 +- pyo3-ffi/src/ceval.rs | 17 +++++++++++ pyo3-ffi/src/cpython/bytesobject.rs | 4 +++ pyo3-ffi/src/cpython/dictobject.rs | 4 +++ pyo3-ffi/src/cpython/pyerrors.rs | 5 +--- pyo3-ffi/src/cpython/unicodeobject.rs | 42 ++++++++++++--------------- pyo3-ffi/src/methodobject.rs | 1 + pyo3-ffi/src/pylifecycle.rs | 41 ++++++++++++++++++++++++-- pyo3-ffi/src/sysmodule.rs | 18 ++++++++++++ pyo3-ffi/src/unicodeobject.rs | 4 +++ 12 files changed, 112 insertions(+), 31 deletions(-) create mode 100644 newsfragments/4534.changed.md create mode 100644 newsfragments/4534.removed.md diff --git a/newsfragments/4534.changed.md b/newsfragments/4534.changed.md new file mode 100644 index 00000000000..c6ff877976d --- /dev/null +++ b/newsfragments/4534.changed.md @@ -0,0 +1,3 @@ +* Updated the FFI bindings for functions and struct fields that have been + deprecated or removed. You may see new deprecation warnings if you are using + functions or fields exposed by the C API that are deprecated. diff --git a/newsfragments/4534.removed.md b/newsfragments/4534.removed.md new file mode 100644 index 00000000000..3ecd27f5eb9 --- /dev/null +++ b/newsfragments/4534.removed.md @@ -0,0 +1 @@ +* Removed the bindings for the private function `_PyErr_ChainExceptions`. diff --git a/pyo3-ffi-check/src/main.rs b/pyo3-ffi-check/src/main.rs index 99713524702..0407a2ffa39 100644 --- a/pyo3-ffi-check/src/main.rs +++ b/pyo3-ffi-check/src/main.rs @@ -48,7 +48,8 @@ fn main() { macro_rules! check_field { ($struct_name:ident, $field:ident, $bindgen_field:ident) => {{ - #[allow(clippy::used_underscore_binding)] + // some struct fields are deprecated but still present in the ABI + #[allow(clippy::used_underscore_binding, deprecated)] let pyo3_ffi_offset = memoffset::offset_of!(pyo3_ffi::$struct_name, $field); #[allow(clippy::used_underscore_binding)] let bindgen_offset = memoffset::offset_of!(bindings::$struct_name, $bindgen_field); diff --git a/pyo3-ffi/src/ceval.rs b/pyo3-ffi/src/ceval.rs index 7aae25f8c3e..d1839a108cb 100644 --- a/pyo3-ffi/src/ceval.rs +++ b/pyo3-ffi/src/ceval.rs @@ -24,6 +24,7 @@ extern "C" { closure: *mut PyObject, ) -> *mut PyObject; + #[cfg(not(Py_3_13))] #[cfg_attr(Py_3_9, deprecated(note = "Python 3.9"))] #[cfg_attr(PyPy, link_name = "PyPyEval_CallObjectWithKeywords")] pub fn PyEval_CallObjectWithKeywords( @@ -33,6 +34,7 @@ extern "C" { ) -> *mut PyObject; } +#[cfg(not(Py_3_13))] #[cfg_attr(Py_3_9, deprecated(note = "Python 3.9"))] #[inline] pub unsafe fn PyEval_CallObject(func: *mut PyObject, arg: *mut PyObject) -> *mut PyObject { @@ -41,9 +43,11 @@ pub unsafe fn PyEval_CallObject(func: *mut PyObject, arg: *mut PyObject) -> *mut } extern "C" { + #[cfg(not(Py_3_13))] #[cfg_attr(Py_3_9, deprecated(note = "Python 3.9"))] #[cfg_attr(PyPy, link_name = "PyPyEval_CallFunction")] pub fn PyEval_CallFunction(obj: *mut PyObject, format: *const c_char, ...) -> *mut PyObject; + #[cfg(not(Py_3_13))] #[cfg_attr(Py_3_9, deprecated(note = "Python 3.9"))] #[cfg_attr(PyPy, link_name = "PyPyEval_CallMethod")] pub fn PyEval_CallMethod( @@ -95,9 +99,22 @@ extern "C" { } extern "C" { + #[cfg(not(Py_3_13))] #[cfg_attr(PyPy, link_name = "PyPyEval_ThreadsInitialized")] + #[cfg_attr( + Py_3_9, + deprecated( + note = "Deprecated in Python 3.9, this function always returns true in Python 3.7 or newer." + ) + )] pub fn PyEval_ThreadsInitialized() -> c_int; #[cfg_attr(PyPy, link_name = "PyPyEval_InitThreads")] + #[cfg_attr( + Py_3_9, + deprecated( + note = "Deprecated in Python 3.9, this function does nothing in Python 3.7 or newer." + ) + )] pub fn PyEval_InitThreads(); pub fn PyEval_AcquireLock(); pub fn PyEval_ReleaseLock(); diff --git a/pyo3-ffi/src/cpython/bytesobject.rs b/pyo3-ffi/src/cpython/bytesobject.rs index d0ac5b9c30e..306702de25e 100644 --- a/pyo3-ffi/src/cpython/bytesobject.rs +++ b/pyo3-ffi/src/cpython/bytesobject.rs @@ -8,6 +8,10 @@ use std::os::raw::c_int; #[repr(C)] pub struct PyBytesObject { pub ob_base: PyVarObject, + #[cfg_attr( + Py_3_11, + deprecated(note = "Deprecated in Python 3.11 and will be removed in a future version.") + )] pub ob_shash: crate::Py_hash_t, pub ob_sval: [c_char; 1], } diff --git a/pyo3-ffi/src/cpython/dictobject.rs b/pyo3-ffi/src/cpython/dictobject.rs index 74b970ebac2..79dcbfdb62e 100644 --- a/pyo3-ffi/src/cpython/dictobject.rs +++ b/pyo3-ffi/src/cpython/dictobject.rs @@ -13,6 +13,10 @@ opaque_struct!(PyDictValues); pub struct PyDictObject { pub ob_base: PyObject, pub ma_used: Py_ssize_t, + #[cfg_attr( + Py_3_12, + deprecated(note = "Deprecated in Python 3.12 and will be removed in the future.") + )] pub ma_version_tag: u64, pub ma_keys: *mut PyDictKeysObject, #[cfg(not(Py_3_11))] diff --git a/pyo3-ffi/src/cpython/pyerrors.rs b/pyo3-ffi/src/cpython/pyerrors.rs index 6d17ebc8124..ca08b44a95c 100644 --- a/pyo3-ffi/src/cpython/pyerrors.rs +++ b/pyo3-ffi/src/cpython/pyerrors.rs @@ -152,10 +152,7 @@ pub struct PyStopIterationObject { pub value: *mut PyObject, } -extern "C" { - #[cfg(not(any(PyPy, GraalPy)))] - pub fn _PyErr_ChainExceptions(typ: *mut PyObject, val: *mut PyObject, tb: *mut PyObject); -} +// skipped _PyErr_ChainExceptions // skipped PyNameErrorObject // skipped PyAttributeErrorObject diff --git a/pyo3-ffi/src/cpython/unicodeobject.rs b/pyo3-ffi/src/cpython/unicodeobject.rs index feb78cf0c82..1414b4ceb38 100644 --- a/pyo3-ffi/src/cpython/unicodeobject.rs +++ b/pyo3-ffi/src/cpython/unicodeobject.rs @@ -1,7 +1,6 @@ #[cfg(not(any(PyPy, GraalPy)))] use crate::Py_hash_t; -use crate::{PyObject, Py_UCS1, Py_UCS2, Py_UCS4, Py_UNICODE, Py_ssize_t}; -#[cfg(not(any(Py_3_12, GraalPy)))] +use crate::{PyObject, Py_UCS1, Py_UCS2, Py_UCS4, Py_ssize_t}; use libc::wchar_t; use std::os::raw::{c_char, c_int, c_uint, c_void}; @@ -588,7 +587,7 @@ extern "C" { #[cfg(not(Py_3_12))] #[deprecated] #[cfg_attr(PyPy, link_name = "PyPyUnicode_FromUnicode")] - pub fn PyUnicode_FromUnicode(u: *const Py_UNICODE, size: Py_ssize_t) -> *mut PyObject; + pub fn PyUnicode_FromUnicode(u: *const wchar_t, size: Py_ssize_t) -> *mut PyObject; #[cfg_attr(PyPy, link_name = "PyPyUnicode_FromKindAndData")] pub fn PyUnicode_FromKindAndData( @@ -603,7 +602,7 @@ extern "C" { #[cfg(not(Py_3_12))] #[deprecated] #[cfg_attr(PyPy, link_name = "PyPyUnicode_AsUnicode")] - pub fn PyUnicode_AsUnicode(unicode: *mut PyObject) -> *mut Py_UNICODE; + pub fn PyUnicode_AsUnicode(unicode: *mut PyObject) -> *mut wchar_t; // skipped _PyUnicode_AsUnicode @@ -613,7 +612,7 @@ extern "C" { pub fn PyUnicode_AsUnicodeAndSize( unicode: *mut PyObject, size: *mut Py_ssize_t, - ) -> *mut Py_UNICODE; + ) -> *mut wchar_t; // skipped PyUnicode_GetMax } @@ -642,14 +641,14 @@ extern "C" { // skipped _PyUnicode_AsString pub fn PyUnicode_Encode( - s: *const Py_UNICODE, + s: *const wchar_t, size: Py_ssize_t, encoding: *const c_char, errors: *const c_char, ) -> *mut PyObject; pub fn PyUnicode_EncodeUTF7( - data: *const Py_UNICODE, + data: *const wchar_t, length: Py_ssize_t, base64SetO: c_int, base64WhiteSpace: c_int, @@ -661,13 +660,13 @@ extern "C" { #[cfg_attr(PyPy, link_name = "PyPyUnicode_EncodeUTF8")] pub fn PyUnicode_EncodeUTF8( - data: *const Py_UNICODE, + data: *const wchar_t, length: Py_ssize_t, errors: *const c_char, ) -> *mut PyObject; pub fn PyUnicode_EncodeUTF32( - data: *const Py_UNICODE, + data: *const wchar_t, length: Py_ssize_t, errors: *const c_char, byteorder: c_int, @@ -676,7 +675,7 @@ extern "C" { // skipped _PyUnicode_EncodeUTF32 pub fn PyUnicode_EncodeUTF16( - data: *const Py_UNICODE, + data: *const wchar_t, length: Py_ssize_t, errors: *const c_char, byteorder: c_int, @@ -685,13 +684,11 @@ extern "C" { // skipped _PyUnicode_EncodeUTF16 // skipped _PyUnicode_DecodeUnicodeEscape - pub fn PyUnicode_EncodeUnicodeEscape( - data: *const Py_UNICODE, - length: Py_ssize_t, - ) -> *mut PyObject; + pub fn PyUnicode_EncodeUnicodeEscape(data: *const wchar_t, length: Py_ssize_t) + -> *mut PyObject; pub fn PyUnicode_EncodeRawUnicodeEscape( - data: *const Py_UNICODE, + data: *const wchar_t, length: Py_ssize_t, ) -> *mut PyObject; @@ -699,7 +696,7 @@ extern "C" { #[cfg_attr(PyPy, link_name = "PyPyUnicode_EncodeLatin1")] pub fn PyUnicode_EncodeLatin1( - data: *const Py_UNICODE, + data: *const wchar_t, length: Py_ssize_t, errors: *const c_char, ) -> *mut PyObject; @@ -708,13 +705,13 @@ extern "C" { #[cfg_attr(PyPy, link_name = "PyPyUnicode_EncodeASCII")] pub fn PyUnicode_EncodeASCII( - data: *const Py_UNICODE, + data: *const wchar_t, length: Py_ssize_t, errors: *const c_char, ) -> *mut PyObject; pub fn PyUnicode_EncodeCharmap( - data: *const Py_UNICODE, + data: *const wchar_t, length: Py_ssize_t, mapping: *mut PyObject, errors: *const c_char, @@ -723,7 +720,7 @@ extern "C" { // skipped _PyUnicode_EncodeCharmap pub fn PyUnicode_TranslateCharmap( - data: *const Py_UNICODE, + data: *const wchar_t, length: Py_ssize_t, table: *mut PyObject, errors: *const c_char, @@ -733,17 +730,14 @@ extern "C" { #[cfg_attr(PyPy, link_name = "PyPyUnicode_EncodeDecimal")] pub fn PyUnicode_EncodeDecimal( - s: *mut Py_UNICODE, + s: *mut wchar_t, length: Py_ssize_t, output: *mut c_char, errors: *const c_char, ) -> c_int; #[cfg_attr(PyPy, link_name = "PyPyUnicode_TransformDecimalToASCII")] - pub fn PyUnicode_TransformDecimalToASCII( - s: *mut Py_UNICODE, - length: Py_ssize_t, - ) -> *mut PyObject; + pub fn PyUnicode_TransformDecimalToASCII(s: *mut wchar_t, length: Py_ssize_t) -> *mut PyObject; // skipped _PyUnicode_TransformDecimalAndSpaceToASCII } diff --git a/pyo3-ffi/src/methodobject.rs b/pyo3-ffi/src/methodobject.rs index 8af41eda817..3dfbbb5a208 100644 --- a/pyo3-ffi/src/methodobject.rs +++ b/pyo3-ffi/src/methodobject.rs @@ -85,6 +85,7 @@ extern "C" { pub fn PyCFunction_GetFunction(f: *mut PyObject) -> Option; pub fn PyCFunction_GetSelf(f: *mut PyObject) -> *mut PyObject; pub fn PyCFunction_GetFlags(f: *mut PyObject) -> c_int; + #[cfg(not(Py_3_13))] #[cfg_attr(Py_3_9, deprecated(note = "Python 3.9"))] pub fn PyCFunction_Call( f: *mut PyObject, diff --git a/pyo3-ffi/src/pylifecycle.rs b/pyo3-ffi/src/pylifecycle.rs index 7f73e3f0e9b..3f051c54f7c 100644 --- a/pyo3-ffi/src/pylifecycle.rs +++ b/pyo3-ffi/src/pylifecycle.rs @@ -23,18 +23,55 @@ extern "C" { pub fn Py_Main(argc: c_int, argv: *mut *mut wchar_t) -> c_int; pub fn Py_BytesMain(argc: c_int, argv: *mut *mut c_char) -> c_int; + #[cfg_attr( + Py_3_11, + deprecated(note = "Deprecated since Python 3.11. Use `PyConfig.program_name` instead.") + )] pub fn Py_SetProgramName(arg1: *const wchar_t); #[cfg_attr(PyPy, link_name = "PyPy_GetProgramName")] + #[cfg_attr( + Py_3_13, + deprecated(note = "Deprecated since Python 3.13. Use `sys.executable` instead.") + )] pub fn Py_GetProgramName() -> *mut wchar_t; + #[cfg_attr( + Py_3_11, + deprecated(note = "Deprecated since Python 3.11. Use `PyConfig.home` instead.") + )] pub fn Py_SetPythonHome(arg1: *const wchar_t); + #[cfg_attr( + Py_3_13, + deprecated( + note = "Deprecated since Python 3.13. Use `PyConfig.home` or the value of the `PYTHONHOME` environment variable instead." + ) + )] pub fn Py_GetPythonHome() -> *mut wchar_t; - + #[cfg_attr( + Py_3_13, + deprecated(note = "Deprecated since Python 3.13. Use `sys.executable` instead.") + )] pub fn Py_GetProgramFullPath() -> *mut wchar_t; - + #[cfg_attr( + Py_3_13, + deprecated(note = "Deprecated since Python 3.13. Use `sys.prefix` instead.") + )] pub fn Py_GetPrefix() -> *mut wchar_t; + #[cfg_attr( + Py_3_13, + deprecated(note = "Deprecated since Python 3.13. Use `sys.exec_prefix` instead.") + )] pub fn Py_GetExecPrefix() -> *mut wchar_t; + #[cfg_attr( + Py_3_13, + deprecated(note = "Deprecated since Python 3.13. Use `sys.path` instead.") + )] pub fn Py_GetPath() -> *mut wchar_t; + #[cfg(not(Py_3_13))] + #[cfg_attr( + Py_3_11, + deprecated(note = "Deprecated since Python 3.11. Use `sys.path` instead.") + )] pub fn Py_SetPath(arg1: *const wchar_t); // skipped _Py_CheckPython3 diff --git a/pyo3-ffi/src/sysmodule.rs b/pyo3-ffi/src/sysmodule.rs index 3c552254244..6f402197ece 100644 --- a/pyo3-ffi/src/sysmodule.rs +++ b/pyo3-ffi/src/sysmodule.rs @@ -8,7 +8,19 @@ extern "C" { #[cfg_attr(PyPy, link_name = "PyPySys_SetObject")] pub fn PySys_SetObject(arg1: *const c_char, arg2: *mut PyObject) -> c_int; + #[cfg_attr( + Py_3_11, + deprecated( + note = "Deprecated in Python 3.11, use `PyConfig.argv` and `PyConfig.parse_argv` instead" + ) + )] pub fn PySys_SetArgv(arg1: c_int, arg2: *mut *mut wchar_t); + #[cfg_attr( + Py_3_11, + deprecated( + note = "Deprecated in Python 3.11, use `PyConfig.argv` and `PyConfig.parse_argv` instead" + ) + )] pub fn PySys_SetArgvEx(arg1: c_int, arg2: *mut *mut wchar_t, arg3: c_int); pub fn PySys_SetPath(arg1: *const wchar_t); @@ -19,6 +31,12 @@ extern "C" { pub fn PySys_FormatStdout(format: *const c_char, ...); pub fn PySys_FormatStderr(format: *const c_char, ...); + #[cfg_attr( + Py_3_13, + deprecated( + note = "Deprecated since Python 3.13. Clear sys.warnoptions and warnings.filters instead." + ) + )] pub fn PySys_ResetWarnOptions(); #[cfg_attr(Py_3_11, deprecated(note = "Python 3.11"))] pub fn PySys_AddWarnOption(arg1: *const wchar_t); diff --git a/pyo3-ffi/src/unicodeobject.rs b/pyo3-ffi/src/unicodeobject.rs index 519bbf261f9..1e0425ce2a2 100644 --- a/pyo3-ffi/src/unicodeobject.rs +++ b/pyo3-ffi/src/unicodeobject.rs @@ -6,6 +6,10 @@ use std::os::raw::{c_char, c_int, c_void}; use std::ptr::addr_of_mut; #[cfg(not(Py_LIMITED_API))] +#[cfg_attr( + Py_3_13, + deprecated(note = "Deprecated since Python 3.13. Use `libc::wchar_t` instead.") +)] pub type Py_UNICODE = wchar_t; pub type Py_UCS4 = u32; From 349f4c0e012bd2869296f237976f809dc5e5a8fc Mon Sep 17 00:00:00 2001 From: Andre Brisco <91817010+abrisco@users.noreply.github.com> Date: Fri, 6 Sep 2024 19:08:15 -0700 Subject: [PATCH 271/495] docs: convert text urls to actual links. (#4530) --- guide/src/building-and-distribution.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/guide/src/building-and-distribution.md b/guide/src/building-and-distribution.md index b93ceea921e..699f6561828 100644 --- a/guide/src/building-and-distribution.md +++ b/guide/src/building-and-distribution.md @@ -92,9 +92,10 @@ If you're packaging your library for redistribution, you should indicated the Py To use PyO3 with bazel one needs to manually configure PyO3, PyO3-ffi and PyO3-macros. In particular, one needs to make sure that it is compiled with the right python flags for the version you intend to use. For example see: -1. https://github.com/abrisco/rules_pyo3 -- General rules for building extension modules. -2. https://github.com/OliverFM/pytorch_with_gazelle -- for a minimal example of a repo that can use PyO3, PyTorch and Gazelle to generate python Build files. -3. https://github.com/TheButlah/rules_pyo3 -- is somewhat dated. + +1. [github.com/abrisco/rules_pyo3](https://github.com/abrisco/rules_pyo3) -- General rules for building extension modules. +2. [github.com/OliverFM/pytorch_with_gazelle](https://github.com/OliverFM/pytorch_with_gazelle) -- for a minimal example of a repo that can use PyO3, PyTorch and Gazelle to generate python Build files. +3. [github.com/TheButlah/rules_pyo3](https://github.com/TheButlah/rules_pyo3) -- is somewhat dated. #### Platform tags From 540e9eda5ede191880a095db3e3344e17184bfce Mon Sep 17 00:00:00 2001 From: Henrik Zenkert <34012616+YesSeri@users.noreply.github.com> Date: Sun, 8 Sep 2024 14:09:07 +0200 Subject: [PATCH 272/495] fixed spelling mistake "clases" -> "classes" (#4538) --- 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 02f8a2a194c..45718f5b667 100644 --- a/guide/src/class.md +++ b/guide/src/class.md @@ -330,7 +330,7 @@ or [`PyRefMut`] instead of `&mut self`. Then you can access a parent class by `self_.as_super()` as `&PyRef`, or by `self_.into_super()` as `PyRef` (and similar for the `PyRefMut` case). For convenience, `self_.as_ref()` can also be used to get `&Self::BaseClass` -directly; however, this approach does not let you access base clases higher in the +directly; however, this approach does not let you access base classes higher in the inheritance hierarchy, for which you would need to chain multiple `as_super` or `into_super` calls. From ef8ee64f54640c98acb695cdd6263412bd3ce608 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Sep 2024 22:48:05 +0100 Subject: [PATCH 273/495] Bump deadsnakes/action from 3.1.0 to 3.2.0 (#4540) Bumps [deadsnakes/action](https://github.com/deadsnakes/action) from 3.1.0 to 3.2.0. - [Release notes](https://github.com/deadsnakes/action/releases) - [Commits](https://github.com/deadsnakes/action/compare/v3.1.0...v3.2.0) --- updated-dependencies: - dependency-name: deadsnakes/action dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .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 c6b31cdc378..8b4c3a55dcf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -513,7 +513,7 @@ jobs: with: components: rust-src # TODO: replace with setup-python when there is support - - uses: deadsnakes/action@v3.1.0 + - uses: deadsnakes/action@v3.2.0 with: python-version: '3.13-dev' nogil: true From 672efdc3ed19ea575b915daa33333de664331f47 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Sep 2024 21:50:49 +0000 Subject: [PATCH 274/495] Update chrono-tz requirement from >= 0.6, < 0.10 to >= 0.6, < 0.11 (#4541) Updates the requirements on [chrono-tz](https://github.com/chronotope/chrono-tz) to permit the latest version. - [Release notes](https://github.com/chronotope/chrono-tz/releases) - [Commits](https://github.com/chronotope/chrono-tz/compare/v0.6.0...v0.10.0) --- updated-dependencies: - dependency-name: chrono-tz dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 0259b1bae9e..28d341f5e75 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,7 +34,7 @@ inventory = { version = "0.3.0", optional = true } # crate integrations that can be added using the eponymous features anyhow = { version = "1.0.1", optional = true } chrono = { version = "0.4.25", default-features = false, optional = true } -chrono-tz = { version = ">= 0.6, < 0.10", default-features = false, optional = true } +chrono-tz = { version = ">= 0.6, < 0.11", 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 } @@ -52,7 +52,7 @@ portable-atomic = "1.0" [dev-dependencies] assert_approx_eq = "1.1.0" chrono = "0.4.25" -chrono-tz = ">= 0.6, < 0.10" +chrono-tz = ">= 0.6, < 0.11" # Required for "and $N others" normalization trybuild = ">=1.0.70" proptest = { version = "1.0", default-features = false, features = ["std"] } From 7d1425ad4aa26cf05685f15492cc84272314e7ae Mon Sep 17 00:00:00 2001 From: Shehab Amin <11789402+shehabgamin@users.noreply.github.com> Date: Thu, 12 Sep 2024 10:00:02 -0700 Subject: [PATCH 275/495] Update README.md (#4547) --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 6cc9be4d6a9..0c20a6d08bc 100644 --- a/README.md +++ b/README.md @@ -219,6 +219,7 @@ about this topic. - [river](https://github.com/online-ml/river) _Online machine learning in python, the computationally heavy statistics algorithms are implemented in Rust._ - [robyn](https://github.com/sparckles/Robyn) A Super Fast Async Python Web Framework with a Rust runtime. - [rust-python-coverage](https://github.com/cjermain/rust-python-coverage) _Example PyO3 project with automated test coverage for Rust and Python._ +- [sail](https://github.com/lakehq/sail) _Unifying stream, batch, and AI workloads with Apache Spark compatibility._ - [tiktoken](https://github.com/openai/tiktoken) _A fast BPE tokeniser for use with OpenAI's models._ - [tokenizers](https://github.com/huggingface/tokenizers/tree/main/bindings/python) _Python bindings to the Hugging Face tokenizers (NLP) written in Rust._ - [tzfpy](http://github.com/ringsaturn/tzfpy) _A fast package to convert longitude/latitude to timezone name._ From d14bfd0c4285a5b99822edf61c75423bef5c9fd9 Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Fri, 13 Sep 2024 09:42:21 +0200 Subject: [PATCH 276/495] fix beta/nightly ci (#4549) --- src/impl_/pyclass.rs | 3 ++- src/tests/common.rs | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/impl_/pyclass.rs b/src/impl_/pyclass.rs index 44070ec30e4..f116e608d2f 100644 --- a/src/impl_/pyclass.rs +++ b/src/impl_/pyclass.rs @@ -990,8 +990,9 @@ unsafe fn bpo_35810_workaround(py: Python<'_>, ty: *mut ffi::PyTypeObject) { ffi::Py_INCREF(ty as *mut ffi::PyObject); } -/// Implementation detail. Only to be used through our proc macro code. /// Method storage for `#[pyclass]`. +/// +/// Implementation detail. Only to be used through our proc macro code. /// Allows arbitrary `#[pymethod]` blocks to submit their methods, /// which are eventually collected by `#[pyclass]`. #[cfg(feature = "multiple-pymethods")] diff --git a/src/tests/common.rs b/src/tests/common.rs index efe0d4c89f1..4e4d7fe98ee 100644 --- a/src/tests/common.rs +++ b/src/tests/common.rs @@ -3,6 +3,7 @@ /// Common macros and helpers for tests #[allow(dead_code)] // many tests do not use the complete set of functionality offered here +#[allow(missing_docs)] // only used in tests #[macro_use] mod inner { From a32afdd7db685637aa63f60b5cc30d1ab6930147 Mon Sep 17 00:00:00 2001 From: Jeff Date: Mon, 16 Sep 2024 17:22:53 +0100 Subject: [PATCH 277/495] cfg features for enum variants (#4509) * handle feature gated simple enum variants * test case for feature gated enum variants * add towncrier newsfragment to pull request * rename `attrs` to `cfg_attrs` * generate a compiler error if cfg attributes disable all variants of enum * test compiler error when all variants of enum disabled * spanned compiler error when cfg features disable enum variants --- newsfragments/4509.fixed.md | 1 + pyo3-macros-backend/src/pyclass.rs | 87 +++++++++++++++++++++++++--- pyo3-macros-backend/src/pyimpl.rs | 2 +- tests/test_field_cfg.rs | 24 ++++++++ tests/ui/invalid_pyclass_enum.rs | 9 +++ tests/ui/invalid_pyclass_enum.stderr | 6 ++ 6 files changed, 119 insertions(+), 10 deletions(-) create mode 100644 newsfragments/4509.fixed.md diff --git a/newsfragments/4509.fixed.md b/newsfragments/4509.fixed.md new file mode 100644 index 00000000000..3684b3e617d --- /dev/null +++ b/newsfragments/4509.fixed.md @@ -0,0 +1 @@ +Fix compile failure when using `#[cfg]` attributes for simple enum variants. diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index 9d5535f6e17..291aeb0125a 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -17,7 +17,7 @@ use crate::attributes::{ use crate::konst::{ConstAttributes, ConstSpec}; use crate::method::{FnArg, FnSpec, PyArg, RegularArg}; use crate::pyfunction::ConstructorAttribute; -use crate::pyimpl::{gen_py_const, PyClassMethodsType}; +use crate::pyimpl::{gen_py_const, get_cfg_attributes, PyClassMethodsType}; use crate::pymethod::{ impl_py_getter_def, impl_py_setter_def, MethodAndMethodDef, MethodAndSlotDef, PropertyType, SlotDef, __GETITEM__, __HASH__, __INT__, __LEN__, __REPR__, __RICHCMP__, __STR__, @@ -533,7 +533,12 @@ impl<'a> PyClassSimpleEnum<'a> { _ => bail_spanned!(variant.span() => "Must be a unit variant."), }; let options = EnumVariantPyO3Options::take_pyo3_options(&mut variant.attrs)?; - Ok(PyClassEnumUnitVariant { ident, options }) + let cfg_attrs = get_cfg_attributes(&variant.attrs); + Ok(PyClassEnumUnitVariant { + ident, + options, + cfg_attrs, + }) } let ident = &enum_.ident; @@ -693,6 +698,7 @@ impl<'a> EnumVariant for PyClassEnumVariant<'a> { struct PyClassEnumUnitVariant<'a> { ident: &'a syn::Ident, options: EnumVariantPyO3Options, + cfg_attrs: Vec<&'a syn::Attribute>, } impl<'a> EnumVariant for PyClassEnumUnitVariant<'a> { @@ -877,20 +883,23 @@ fn impl_simple_enum( ensure_spanned!(variant.options.constructor.is_none(), variant.options.constructor.span() => "`constructor` can't be used on a simple enum variant"); } + let variant_cfg_check = generate_cfg_check(&variants, cls); + let (default_repr, default_repr_slot) = { let variants_repr = variants.iter().map(|variant| { let variant_name = variant.ident; + let cfg_attrs = &variant.cfg_attrs; // Assuming all variants are unit variants because they are the only type we support. let repr = format!( "{}.{}", get_class_python_name(cls, args), variant.get_python_name(args), ); - quote! { #cls::#variant_name => #repr, } + quote! { #(#cfg_attrs)* #cls::#variant_name => #repr, } }); let mut repr_impl: syn::ImplItemFn = syn::parse_quote! { fn __pyo3__repr__(&self) -> &'static str { - match self { + match *self { #(#variants_repr)* } } @@ -908,11 +917,12 @@ fn impl_simple_enum( // This implementation allows us to convert &T to #repr_type without implementing `Copy` let variants_to_int = variants.iter().map(|variant| { let variant_name = variant.ident; - quote! { #cls::#variant_name => #cls::#variant_name as #repr_type, } + let cfg_attrs = &variant.cfg_attrs; + quote! { #(#cfg_attrs)* #cls::#variant_name => #cls::#variant_name as #repr_type, } }); let mut int_impl: syn::ImplItemFn = syn::parse_quote! { fn __pyo3__int__(&self) -> #repr_type { - match self { + match *self { #(#variants_to_int)* } } @@ -936,7 +946,9 @@ fn impl_simple_enum( methods_type, simple_enum_default_methods( cls, - variants.iter().map(|v| (v.ident, v.get_python_name(args))), + variants + .iter() + .map(|v| (v.ident, v.get_python_name(args), &v.cfg_attrs)), ctx, ), default_slots, @@ -945,6 +957,8 @@ fn impl_simple_enum( .impl_all(ctx)?; Ok(quote! { + #variant_cfg_check + #pytypeinfo #pyclass_impls @@ -1474,7 +1488,13 @@ fn generate_default_protocol_slot( fn simple_enum_default_methods<'a>( cls: &'a syn::Ident, - unit_variant_names: impl IntoIterator)>, + unit_variant_names: impl IntoIterator< + Item = ( + &'a syn::Ident, + Cow<'a, syn::Ident>, + &'a Vec<&'a syn::Attribute>, + ), + >, ctx: &Ctx, ) -> Vec { let cls_type = syn::parse_quote!(#cls); @@ -1490,7 +1510,25 @@ fn simple_enum_default_methods<'a>( }; unit_variant_names .into_iter() - .map(|(var, py_name)| gen_py_const(&cls_type, &variant_to_attribute(var, &py_name), ctx)) + .map(|(var, py_name, attrs)| { + let method = gen_py_const(&cls_type, &variant_to_attribute(var, &py_name), ctx); + let associated_method_tokens = method.associated_method; + let method_def_tokens = method.method_def; + + let associated_method = quote! { + #(#attrs)* + #associated_method_tokens + }; + let method_def = quote! { + #(#attrs)* + #method_def_tokens + }; + + MethodAndMethodDef { + associated_method, + method_def, + } + }) .collect() } @@ -2395,6 +2433,37 @@ fn define_inventory_class(inventory_class_name: &syn::Ident, ctx: &Ctx) -> Token } } +fn generate_cfg_check(variants: &[PyClassEnumUnitVariant<'_>], cls: &syn::Ident) -> TokenStream { + if variants.is_empty() { + return quote! {}; + } + + let mut conditions = Vec::new(); + + for variant in variants { + let cfg_attrs = &variant.cfg_attrs; + + if cfg_attrs.is_empty() { + // There's at least one variant of the enum without cfg attributes, + // so the check is not necessary + return quote! {}; + } + + for attr in cfg_attrs { + if let syn::Meta::List(meta) = &attr.meta { + let cfg_tokens = &meta.tokens; + conditions.push(quote! { not(#cfg_tokens) }); + } + } + } + + quote_spanned! { + cls.span() => + #[cfg(all(#(#conditions),*))] + ::core::compile_error!(concat!("#[pyclass] can't be used on enums without any variants - all variants of enum `", stringify!(#cls), "` have been configured out by cfg attributes")); + } +} + const UNIQUE_GET: &str = "`get` may only be specified once"; const UNIQUE_SET: &str = "`set` may only be specified once"; const UNIQUE_NAME: &str = "`name` may only be specified once"; diff --git a/pyo3-macros-backend/src/pyimpl.rs b/pyo3-macros-backend/src/pyimpl.rs index 69b7bb04f48..1c3d7f766c2 100644 --- a/pyo3-macros-backend/src/pyimpl.rs +++ b/pyo3-macros-backend/src/pyimpl.rs @@ -330,7 +330,7 @@ fn submit_methods_inventory( } } -fn get_cfg_attributes(attrs: &[syn::Attribute]) -> Vec<&syn::Attribute> { +pub(crate) fn get_cfg_attributes(attrs: &[syn::Attribute]) -> Vec<&syn::Attribute> { attrs .iter() .filter(|attr| attr.path().is_ident("cfg")) diff --git a/tests/test_field_cfg.rs b/tests/test_field_cfg.rs index c5fc4958dbf..1766cadb3d5 100644 --- a/tests/test_field_cfg.rs +++ b/tests/test_field_cfg.rs @@ -17,6 +17,15 @@ struct CfgClass { pub b: u32, } +#[pyclass(eq, eq_int)] +#[derive(PartialEq)] +enum CfgSimpleEnum { + #[cfg(any())] + DisabledVariant, + #[cfg(not(any()))] + EnabledVariant, +} + #[test] fn test_cfg() { Python::with_gil(|py| { @@ -27,3 +36,18 @@ fn test_cfg() { assert_eq!(b, 3); }); } + +#[test] +fn test_cfg_simple_enum() { + Python::with_gil(|py| { + let simple = py.get_type::(); + pyo3::py_run!( + py, + simple, + r#" + assert hasattr(simple, "EnabledVariant") + assert not hasattr(simple, "DisabledVariant") + "# + ); + }) +} diff --git a/tests/ui/invalid_pyclass_enum.rs b/tests/ui/invalid_pyclass_enum.rs index c490f9291e3..102659e822e 100644 --- a/tests/ui/invalid_pyclass_enum.rs +++ b/tests/ui/invalid_pyclass_enum.rs @@ -93,4 +93,13 @@ enum InvalidOrderedComplexEnum2 { VariantB { msg: String } } +#[pyclass(eq)] +#[derive(PartialEq)] +enum AllEnumVariantsDisabled { + #[cfg(any())] + DisabledA, + #[cfg(not(all()))] + DisabledB, +} + fn main() {} diff --git a/tests/ui/invalid_pyclass_enum.stderr b/tests/ui/invalid_pyclass_enum.stderr index 98ca2d77bfa..80dc9539748 100644 --- a/tests/ui/invalid_pyclass_enum.stderr +++ b/tests/ui/invalid_pyclass_enum.stderr @@ -66,6 +66,12 @@ error: The `ord` option requires the `eq` option. 83 | #[pyclass(ord)] | ^^^ +error: #[pyclass] can't be used on enums without any variants - all variants of enum `AllEnumVariantsDisabled` have been configured out by cfg attributes + --> tests/ui/invalid_pyclass_enum.rs:98:6 + | +98 | enum AllEnumVariantsDisabled { + | ^^^^^^^^^^^^^^^^^^^^^^^ + error[E0369]: binary operation `==` cannot be applied to type `&SimpleEqOptRequiresPartialEq` --> tests/ui/invalid_pyclass_enum.rs:31:11 | From 3b39a833fea33163f051c10bb20e009b20657cc4 Mon Sep 17 00:00:00 2001 From: Predrag Gruevski <2348618+obi1kenobi@users.noreply.github.com> Date: Wed, 18 Sep 2024 14:08:47 -0400 Subject: [PATCH 278/495] Fix broken links in python-from-rust.md (#4564) These links are currently broken in the docs: https://pyo3.rs/v0.22.3/python-from-rust#the-py-lifetime I'm not particularly familiar with the docs setup for this repo, so this PR is my best-effort attempt at fixing the problem with a minimal fix in the spirit of the existing code. --- guide/src/python-from-rust.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/guide/src/python-from-rust.md b/guide/src/python-from-rust.md index ee618f3fa47..ebb7fa1f4da 100644 --- a/guide/src/python-from-rust.md +++ b/guide/src/python-from-rust.md @@ -44,3 +44,7 @@ Because of the lack of exclusive `&mut` references, PyO3's APIs for Python objec [smart-pointers]: https://doc.rust-lang.org/book/ch15-00-smart-pointers.html [obtaining-py]: {{#PYO3_DOCS_URL}}/pyo3/marker/struct.Python.html#obtaining-a-python-token [`pyo3::sync`]: {{#PYO3_DOCS_URL}}/pyo3/sync/index.html +[eval]: {{#PYO3_DOCS_URL}}/pyo3/marker/struct.Python.html#method.eval +[import]: {{#PYO3_DOCS_URL}}/pyo3/marker/struct.Python.html#method.import_bound +[clone_ref]: {{#PYO3_DOCS_URL}}/pyo3/prelude/struct.Py.html#method.clone_ref +[Bound]: {{#PYO3_DOCS_URL}}/pyo3/struct.Bound.html From 6b3f887bd2359f987e9cb3c3fd059cc61e3574d8 Mon Sep 17 00:00:00 2001 From: Gabriel Levcovitz Date: Sat, 21 Sep 2024 05:22:41 -0300 Subject: [PATCH 279/495] make `GILOnceCell::get_or_try_init_type_ref` public (#4542) * make `GILOnceCell::get_or_try_init_type_ref` public * change `GILOnceCell::get_or_try_init_type_ref` to `GILOnceCell::import` * update doc Co-authored-by: Bruno Kolenbrander <59372212+mejrs@users.noreply.github.com> * update doc Co-authored-by: Bruno Kolenbrander <59372212+mejrs@users.noreply.github.com> * update docs --------- Co-authored-by: Bruno Kolenbrander <59372212+mejrs@users.noreply.github.com> --- .../python-from-rust/calling-existing-code.md | 17 +++++--- newsfragments/4542.changed.md | 1 + src/conversions/chrono_tz.rs | 2 +- src/conversions/num_rational.rs | 2 +- src/conversions/rust_decimal.rs | 2 +- src/conversions/std/ipaddr.rs | 4 +- src/conversions/std/time.rs | 2 +- src/impl_/exceptions.rs | 2 +- src/sync.rs | 43 ++++++++++++++++--- src/types/mapping.rs | 2 +- src/types/module.rs | 3 ++ src/types/sequence.rs | 2 +- 12 files changed, 60 insertions(+), 22 deletions(-) create mode 100644 newsfragments/4542.changed.md diff --git a/guide/src/python-from-rust/calling-existing-code.md b/guide/src/python-from-rust/calling-existing-code.md index 0e986562e19..aae4317ae2c 100644 --- a/guide/src/python-from-rust/calling-existing-code.md +++ b/guide/src/python-from-rust/calling-existing-code.md @@ -4,8 +4,7 @@ If you already have some existing Python code that you need to execute from Rust ## Want to access Python APIs? Then use `PyModule::import`. -[`PyModule::import`]({{#PYO3_DOCS_URL}}/pyo3/types/struct.PyModule.html#method.import) can -be used to get handle to a Python module from Rust. You can use this to import and use any Python +[`PyModule::import`] can be used to get handle to a Python module from Rust. You can use this to import and use any Python module available in your environment. ```rust @@ -24,9 +23,11 @@ fn main() -> PyResult<()> { } ``` -## Want to run just an expression? Then use `eval_bound`. +[`PyModule::import`]: {{#PYO3_DOCS_URL}}/pyo3/types/struct.PyModule.html#method.import -[`Python::eval_bound`]({{#PYO3_DOCS_URL}}/pyo3/marker/struct.Python.html#method.eval_bound) is +## Want to run just an expression? Then use `eval`. + +[`Python::eval`]({{#PYO3_DOCS_URL}}/pyo3/marker/struct.Python.html#method.eval) is a method to execute a [Python expression](https://docs.python.org/3/reference/expressions.html) and return the evaluated value as a `Bound<'py, PyAny>` object. @@ -48,17 +49,19 @@ Python::with_gil(|py| { # } ``` -## Want to run statements? Then use `run_bound`. +## Want to run statements? Then use `run`. -[`Python::run_bound`] is a method to execute one or more +[`Python::run`] is a method to execute one or more [Python statements](https://docs.python.org/3/reference/simple_stmts.html). This method returns nothing (like any Python statement), but you can get access to manipulated objects via the `locals` dict. -You can also use the [`py_run!`] macro, which is a shorthand for [`Python::run_bound`]. +You can also use the [`py_run!`] macro, which is a shorthand for [`Python::run`]. Since [`py_run!`] panics on exceptions, we recommend you use this macro only for quickly testing your Python extensions. +[`Python::run`]: {{#PYO3_DOCS_URL}}/pyo3/marker/struct.Python.html#method.run + ```rust use pyo3::prelude::*; use pyo3::py_run; diff --git a/newsfragments/4542.changed.md b/newsfragments/4542.changed.md new file mode 100644 index 00000000000..1f983a5e344 --- /dev/null +++ b/newsfragments/4542.changed.md @@ -0,0 +1 @@ +Change `GILOnceCell::get_or_try_init_type_ref` to `GILOnceCell::import` and make it public API. diff --git a/src/conversions/chrono_tz.rs b/src/conversions/chrono_tz.rs index 52c156eaba2..91428638f1e 100644 --- a/src/conversions/chrono_tz.rs +++ b/src/conversions/chrono_tz.rs @@ -66,7 +66,7 @@ impl<'py> IntoPyObject<'py> for Tz { fn into_pyobject(self, py: Python<'py>) -> Result { static ZONE_INFO: GILOnceCell> = GILOnceCell::new(); ZONE_INFO - .get_or_try_init_type_ref(py, "zoneinfo", "ZoneInfo") + .import(py, "zoneinfo", "ZoneInfo") .and_then(|obj| obj.call1((self.name(),))) } } diff --git a/src/conversions/num_rational.rs b/src/conversions/num_rational.rs index 146e9973119..2448ad3701e 100644 --- a/src/conversions/num_rational.rs +++ b/src/conversions/num_rational.rs @@ -59,7 +59,7 @@ use num_rational::Ratio; static FRACTION_CLS: GILOnceCell> = GILOnceCell::new(); fn get_fraction_cls(py: Python<'_>) -> PyResult<&Bound<'_, PyType>> { - FRACTION_CLS.get_or_try_init_type_ref(py, "fractions", "Fraction") + FRACTION_CLS.import(py, "fractions", "Fraction") } macro_rules! rational_conversion { diff --git a/src/conversions/rust_decimal.rs b/src/conversions/rust_decimal.rs index 8dbecad9fa5..c353eb91d8a 100644 --- a/src/conversions/rust_decimal.rs +++ b/src/conversions/rust_decimal.rs @@ -79,7 +79,7 @@ impl FromPyObject<'_> for Decimal { static DECIMAL_CLS: GILOnceCell> = GILOnceCell::new(); fn get_decimal_cls(py: Python<'_>) -> PyResult<&Bound<'_, PyType>> { - DECIMAL_CLS.get_or_try_init_type_ref(py, "decimal", "Decimal") + DECIMAL_CLS.import(py, "decimal", "Decimal") } impl ToPyObject for Decimal { diff --git a/src/conversions/std/ipaddr.rs b/src/conversions/std/ipaddr.rs index 8c308ead3b8..bff81ad1a1f 100755 --- a/src/conversions/std/ipaddr.rs +++ b/src/conversions/std/ipaddr.rs @@ -46,7 +46,7 @@ impl<'py> IntoPyObject<'py> for Ipv4Addr { fn into_pyobject(self, py: Python<'py>) -> Result { static IPV4_ADDRESS: GILOnceCell> = GILOnceCell::new(); IPV4_ADDRESS - .get_or_try_init_type_ref(py, "ipaddress", "IPv4Address")? + .import(py, "ipaddress", "IPv4Address")? .call1((u32::from_be_bytes(self.octets()),)) } } @@ -77,7 +77,7 @@ impl<'py> IntoPyObject<'py> for Ipv6Addr { fn into_pyobject(self, py: Python<'py>) -> Result { static IPV6_ADDRESS: GILOnceCell> = GILOnceCell::new(); IPV6_ADDRESS - .get_or_try_init_type_ref(py, "ipaddress", "IPv6Address")? + .import(py, "ipaddress", "IPv6Address")? .call1((u128::from_be_bytes(self.octets()),)) } } diff --git a/src/conversions/std/time.rs b/src/conversions/std/time.rs index 50ec524d86d..f8345b12e61 100755 --- a/src/conversions/std/time.rs +++ b/src/conversions/std/time.rs @@ -93,7 +93,7 @@ impl<'py> IntoPyObject<'py> for Duration { { static TIMEDELTA: GILOnceCell> = GILOnceCell::new(); TIMEDELTA - .get_or_try_init_type_ref(py, "datetime", "timedelta")? + .import(py, "datetime", "timedelta")? .call1((days, seconds, microseconds)) } } diff --git a/src/impl_/exceptions.rs b/src/impl_/exceptions.rs index eafac1edfa2..15b6f53bbe2 100644 --- a/src/impl_/exceptions.rs +++ b/src/impl_/exceptions.rs @@ -17,7 +17,7 @@ impl ImportedExceptionTypeObject { pub fn get<'py>(&self, py: Python<'py>) -> &Bound<'py, PyType> { self.imported_value - .get_or_try_init_type_ref(py, self.module, self.name) + .import(py, self.module, self.name) .unwrap_or_else(|e| { panic!( "failed to import exception {}.{}: {}", diff --git a/src/sync.rs b/src/sync.rs index 59f669f2627..b02b21def93 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -5,8 +5,8 @@ //! //! [PEP 703]: https://peps.python.org/pep-703/ use crate::{ - types::{any::PyAnyMethods, PyString, PyType}, - Bound, Py, PyResult, Python, + types::{any::PyAnyMethods, PyString}, + Bound, Py, PyResult, PyTypeCheck, Python, }; use std::cell::UnsafeCell; @@ -214,16 +214,47 @@ impl GILOnceCell> { } } -impl GILOnceCell> { - /// Get a reference to the contained Python type, initializing it if needed. +impl GILOnceCell> +where + T: PyTypeCheck, +{ + /// Get a reference to the contained Python type, initializing the cell if needed. /// /// This is a shorthand method for `get_or_init` which imports the type from Python on init. - pub(crate) fn get_or_try_init_type_ref<'py>( + /// + /// # Example: Using `GILOnceCell` to store a class in a static variable. + /// + /// `GILOnceCell` can be used to avoid importing a class multiple times: + /// ``` + /// # use pyo3::prelude::*; + /// # use pyo3::sync::GILOnceCell; + /// # use pyo3::types::{PyDict, PyType}; + /// # use pyo3::intern; + /// # + /// #[pyfunction] + /// fn create_ordered_dict<'py>(py: Python<'py>, dict: Bound<'py, PyDict>) -> PyResult> { + /// // Even if this function is called multiple times, + /// // the `OrderedDict` class will be imported only once. + /// static ORDERED_DICT: GILOnceCell> = GILOnceCell::new(); + /// ORDERED_DICT + /// .import(py, "collections", "OrderedDict")? + /// .call1((dict,)) + /// } + /// + /// # Python::with_gil(|py| { + /// # let dict = PyDict::new(py); + /// # dict.set_item(intern!(py, "foo"), 42).unwrap(); + /// # let fun = wrap_pyfunction!(create_ordered_dict, py).unwrap(); + /// # let ordered_dict = fun.call1((&dict,)).unwrap(); + /// # assert!(dict.eq(ordered_dict).unwrap()); + /// # }); + /// ``` + pub fn import<'py>( &self, py: Python<'py>, module_name: &str, attr_name: &str, - ) -> PyResult<&Bound<'py, PyType>> { + ) -> PyResult<&Bound<'py, T>> { self.get_or_try_init(py, || { let type_object = py .import(module_name)? diff --git a/src/types/mapping.rs b/src/types/mapping.rs index 0d1467c27bf..5ad1aa1b3c1 100644 --- a/src/types/mapping.rs +++ b/src/types/mapping.rs @@ -163,7 +163,7 @@ impl<'py> PyMappingMethods<'py> for Bound<'py, PyMapping> { fn get_mapping_abc(py: Python<'_>) -> PyResult<&Bound<'_, PyType>> { static MAPPING_ABC: GILOnceCell> = GILOnceCell::new(); - MAPPING_ABC.get_or_try_init_type_ref(py, "collections.abc", "Mapping") + MAPPING_ABC.import(py, "collections.abc", "Mapping") } impl PyTypeCheck for PyMapping { diff --git a/src/types/module.rs b/src/types/module.rs index 7307dfd4c2d..aec3ea0c179 100644 --- a/src/types/module.rs +++ b/src/types/module.rs @@ -79,6 +79,9 @@ impl PyModule { /// ```python /// import antigravity /// ``` + /// + /// If you want to import a class, you can store a reference to it with + /// [`GILOnceCell::import`][crate::sync::GILOnceCell#method.import]. pub fn import(py: Python<'_>, name: N) -> PyResult> where N: IntoPy>, diff --git a/src/types/sequence.rs b/src/types/sequence.rs index 4dbfedc378b..2e8ef4e53b8 100644 --- a/src/types/sequence.rs +++ b/src/types/sequence.rs @@ -352,7 +352,7 @@ where fn get_sequence_abc(py: Python<'_>) -> PyResult<&Bound<'_, PyType>> { static SEQUENCE_ABC: GILOnceCell> = GILOnceCell::new(); - SEQUENCE_ABC.get_or_try_init_type_ref(py, "collections.abc", "Sequence") + SEQUENCE_ABC.import(py, "collections.abc", "Sequence") } impl PyTypeCheck for PySequence { From df0710013c42b91267de3b3b736f4a9014d49f0d Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Sat, 21 Sep 2024 04:48:08 -0400 Subject: [PATCH 280/495] Replace a use of `libc::c_void` with the `std` version (#4572) --- src/types/capsule.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/types/capsule.rs b/src/types/capsule.rs index a6a2ba30c7b..9d9e6e4eb72 100644 --- a/src/types/capsule.rs +++ b/src/types/capsule.rs @@ -194,8 +194,8 @@ pub trait PyCapsuleMethods<'py>: crate::sealed::Sealed { /// # Example /// /// ``` + /// use std::os::raw::c_void; /// use std::sync::mpsc::{channel, Sender}; - /// use libc::c_void; /// use pyo3::{prelude::*, types::PyCapsule}; /// /// let (tx, rx) = channel::(); @@ -357,13 +357,12 @@ fn name_ptr_ignore_error(slf: &Bound<'_, PyCapsule>) -> *const c_char { #[cfg(test)] mod tests { - use libc::c_void; - use crate::prelude::PyModule; use crate::types::capsule::PyCapsuleMethods; use crate::types::module::PyModuleMethods; use crate::{types::PyCapsule, Py, PyResult, Python}; use std::ffi::CString; + use std::os::raw::c_void; use std::sync::mpsc::{channel, Sender}; #[test] From 0079003c49c2b7ac1e5c66b3f73f2ef11621476f Mon Sep 17 00:00:00 2001 From: Brogolem35 Date: Sat, 21 Sep 2024 16:15:31 +0300 Subject: [PATCH 281/495] Fix typo (#4573) --- guide/src/rust-from-python.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/guide/src/rust-from-python.md b/guide/src/rust-from-python.md index cbf846981ed..3b525d399df 100644 --- a/guide/src/rust-from-python.md +++ b/guide/src/rust-from-python.md @@ -8,6 +8,6 @@ PyO3 can create three types of Python objects: - Python modules, via the `#[pymodule]` macro - Python functions, via the `#[pyfunction]` macro -- Python classes, via the `#[pyclass]` macro (plus `#[pymethods]` to define methods for those clases) +- Python classes, via the `#[pyclass]` macro (plus `#[pymethods]` to define methods for those classes) The following subchapters go through each of these in turn. From eccff5859f91d06d141219be83184d77b7c448cc Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Mon, 23 Sep 2024 17:00:59 -0600 Subject: [PATCH 282/495] remove continue-on-error from free-threaded CI job (#4546) * remove continue-on-error from free-threaded CI job * fix diagnostic message `cfg_attrs` * disable assert that may not be true on free-threaded build * Update ci.yml Co-authored-by: David Hewitt --------- Co-authored-by: David Hewitt --- .github/workflows/ci.yml | 7 +------ src/impl_/pyclass.rs | 4 ++-- tests/test_gc.rs | 4 ++++ 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8b4c3a55dcf..9ceb4dd64c7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -520,12 +520,7 @@ jobs: - run: python3 -m sysconfig - run: python3 -m pip install --upgrade pip && pip install nox - run: nox -s ffi-check - - name: Run default nox sessions that should pass - run: nox -s clippy docs rustfmt ruff - - name: Run PyO3 tests with free-threaded Python (can fail) - # TODO fix the test crashes so we can unset this - continue-on-error: true - run: nox -s test + - run: nox test-version-limits: needs: [fmt] diff --git a/src/impl_/pyclass.rs b/src/impl_/pyclass.rs index f116e608d2f..b0362174d25 100644 --- a/src/impl_/pyclass.rs +++ b/src/impl_/pyclass.rs @@ -1115,7 +1115,7 @@ impl PyClassThreadChecker for ThreadCheckerImpl { /// Trait denoting that this class is suitable to be used as a base type for PyClass. #[cfg_attr( - all(diagnostic_namespace, feature = "abi3"), + all(diagnostic_namespace, Py_LIMITED_API), diagnostic::on_unimplemented( message = "pyclass `{Self}` cannot be subclassed", label = "required for `#[pyclass(extends={Self})]`", @@ -1124,7 +1124,7 @@ impl PyClassThreadChecker for ThreadCheckerImpl { ) )] #[cfg_attr( - all(diagnostic_namespace, not(feature = "abi3")), + all(diagnostic_namespace, not(Py_LIMITED_API)), diagnostic::on_unimplemented( message = "pyclass `{Self}` cannot be subclassed", label = "required for `#[pyclass(extends={Self})]`", diff --git a/tests/test_gc.rs b/tests/test_gc.rs index b37901930be..a9380c29f7d 100644 --- a/tests/test_gc.rs +++ b/tests/test_gc.rs @@ -120,6 +120,10 @@ fn gc_integration() { Python::with_gil(|py| { py.run(ffi::c_str!("import gc; gc.collect()"), None, None) .unwrap(); + // threads are resumed before tp_clear() calls finish, so drop might not + // necessarily be called when we get here see + // https://peps.python.org/pep-0703/#stop-the-world + #[cfg(not(Py_GIL_DISABLED))] assert!(drop_called.load(Ordering::Relaxed)); }); } From caf00a7409b0e84508088bb94551f7bf2aa2b54b Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Tue, 24 Sep 2024 18:41:12 +0100 Subject: [PATCH 283/495] release: 0.22.3 (#4558) --- CHANGELOG.md | 37 ++++++++++++++++++- README.md | 4 +- 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/4328.fixed.md | 1 - newsfragments/4349.fixed.md | 1 - newsfragments/4355.added.md | 10 ----- newsfragments/4355.fixed.md | 2 - newsfragments/4359.fixed.md | 1 - newsfragments/4382.fixed.md | 1 - newsfragments/4396.fixed.md | 1 - newsfragments/4407.fixed.md | 1 - newsfragments/4410.added.md | 1 - newsfragments/4410.fixed.md | 3 -- newsfragments/4420.fixed.md | 1 - newsfragments/4420.removed.md | 1 - newsfragments/4445.added.md | 1 - newsfragments/4445.removed.md | 1 - newsfragments/4454.fixed.md | 1 - newsfragments/4456.changed.md | 1 - newsfragments/4461.added.md | 1 - newsfragments/4479.fixed.md | 1 - newsfragments/4481.changed.md | 1 - newsfragments/4511.added.md | 1 - 27 files changed, 43 insertions(+), 40 deletions(-) delete mode 100644 newsfragments/4328.fixed.md delete mode 100644 newsfragments/4349.fixed.md delete mode 100644 newsfragments/4355.added.md delete mode 100644 newsfragments/4355.fixed.md delete mode 100644 newsfragments/4359.fixed.md delete mode 100644 newsfragments/4382.fixed.md delete mode 100644 newsfragments/4396.fixed.md delete mode 100644 newsfragments/4407.fixed.md delete mode 100644 newsfragments/4410.added.md delete mode 100644 newsfragments/4410.fixed.md delete mode 100644 newsfragments/4420.fixed.md delete mode 100644 newsfragments/4420.removed.md delete mode 100644 newsfragments/4445.added.md delete mode 100644 newsfragments/4445.removed.md delete mode 100644 newsfragments/4454.fixed.md delete mode 100644 newsfragments/4456.changed.md delete mode 100644 newsfragments/4461.added.md delete mode 100644 newsfragments/4479.fixed.md delete mode 100644 newsfragments/4481.changed.md delete mode 100644 newsfragments/4511.added.md diff --git a/CHANGELOG.md b/CHANGELOG.md index caabd04129c..0c9afeed6df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,40 @@ To see unreleased changes, please see the [CHANGELOG on the main branch guide](h +## [0.22.3] - 2024-09-15 + +### Added + +- Add `pyo3::ffi::compat` namespace with compatibility shims for C API functions added in recent versions of Python. +- Add FFI definition `PyDict_GetItemRef` on Python 3.13 and newer, and `compat::PyDict_GetItemRef` for all versions. [#4355](https://github.com/PyO3/pyo3/pull/4355) +- Add FFI definition `PyList_GetItemRef` on Python 3.13 and newer, and `pyo3_ffi::compat::PyList_GetItemRef` for all versions. [#4410](https://github.com/PyO3/pyo3/pull/4410) +- Add FFI definitions `compat::Py_NewRef` and `compat::Py_XNewRef`. [#4445](https://github.com/PyO3/pyo3/pull/4445) +- Add FFI definitions `compat::PyObject_CallNoArgs` and `compat::PyObject_CallMethodNoArgs`. [#4461](https://github.com/PyO3/pyo3/pull/4461) +- Add `GilOnceCell>::clone_ref`. [#4511](https://github.com/PyO3/pyo3/pull/4511) + +### Changed + +- Improve error messages for `#[pyfunction]` defined inside `#[pymethods]`. [#4349](https://github.com/PyO3/pyo3/pull/4349) +- Improve performance of calls to Python by using the vectorcall calling convention where possible. [#4456](https://github.com/PyO3/pyo3/pull/4456) +- Mention the type name in the exception message when trying to instantiate a class with no constructor defined. [#4481](https://github.com/PyO3/pyo3/pull/4481) + +### Removed + +- Remove private FFI definition `_Py_PackageContext`. [#4420](https://github.com/PyO3/pyo3/pull/4420) + +### Fixed + +- Fix compile failure in declarative `#[pymodule]` under presence of `#![no_implicit_prelude]`. [#4328](https://github.com/PyO3/pyo3/pull/4328) +- Fix use of borrowed reference in `PyDict::get_item` (unsafe in free-threaded Python). [#4355](https://github.com/PyO3/pyo3/pull/4355) +- Fix `#[pyclass(eq)]` macro hygiene issues for structs and enums. [#4359](https://github.com/PyO3/pyo3/pull/4359) +- Fix hygiene/span issues of `'#[pyfunction]` and `#[pymethods]` generated code which affected expansion in `macro_rules` context. [#4382](https://github.com/PyO3/pyo3/pull/4382) +- Fix `unsafe_code` lint error in `#[pyclass]` generated code. [#4396](https://github.com/PyO3/pyo3/pull/4396) +- Fix async functions returning a tuple only returning the first element to Python. [#4407](https://github.com/PyO3/pyo3/pull/4407) +- Fix use of borrowed reference in `PyList::get_item` (unsafe in free-threaded Python). [#4410](https://github.com/PyO3/pyo3/pull/4410) +- Correct FFI definition `PyArg_ParseTupleAndKeywords` to take `*const *const c_char` instead of `*mut *mut c_char` on Python 3.13 and up. [#4420](https://github.com/PyO3/pyo3/pull/4420) +- Fix a soundness bug with `PyClassInitializer`: panic if adding subclass to existing instance via `PyClassInitializer::from(Py).add_subclass(SubClass)`. [#4454](https://github.com/PyO3/pyo3/pull/4454) +- Fix illegal reference counting op inside implementation of `__traverse__` handlers. [#4479](https://github.com/PyO3/pyo3/pull/4479) + ## [0.22.2] - 2024-07-17 ### Packaging @@ -1839,7 +1873,8 @@ Yanked - Initial release -[Unreleased]: https://github.com/pyo3/pyo3/compare/v0.22.2...HEAD +[Unreleased]: https://github.com/pyo3/pyo3/compare/v0.22.3...HEAD +[0.22.3]: https://github.com/pyo3/pyo3/compare/v0.22.2...v0.22.3 [0.22.2]: https://github.com/pyo3/pyo3/compare/v0.22.1...v0.22.2 [0.22.1]: https://github.com/pyo3/pyo3/compare/v0.22.0...v0.22.1 [0.22.0]: https://github.com/pyo3/pyo3/compare/v0.21.2...v0.22.0 diff --git a/README.md b/README.md index 0c20a6d08bc..28f9c0af0b6 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ name = "string_sum" crate-type = ["cdylib"] [dependencies] -pyo3 = { version = "0.22.2", features = ["extension-module"] } +pyo3 = { version = "0.22.3", 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.22.2" +version = "0.22.3" features = ["auto-initialize"] ``` diff --git a/examples/decorator/.template/pre-script.rhai b/examples/decorator/.template/pre-script.rhai index a9f0eb4a289..32b20a55d17 100644 --- a/examples/decorator/.template/pre-script.rhai +++ b/examples/decorator/.template/pre-script.rhai @@ -1,4 +1,4 @@ -variable::set("PYO3_VERSION", "0.22.2"); +variable::set("PYO3_VERSION", "0.22.3"); 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 a9f0eb4a289..32b20a55d17 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.22.2"); +variable::set("PYO3_VERSION", "0.22.3"); 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 bbc96358a23..ee37346e10d 100644 --- a/examples/plugin/.template/pre-script.rhai +++ b/examples/plugin/.template/pre-script.rhai @@ -1,4 +1,4 @@ -variable::set("PYO3_VERSION", "0.22.2"); +variable::set("PYO3_VERSION", "0.22.3"); 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 d28d9d09987..8ceb8df28bb 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.22.2"); +variable::set("PYO3_VERSION", "0.22.3"); 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 a9f0eb4a289..32b20a55d17 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.22.2"); +variable::set("PYO3_VERSION", "0.22.3"); file::rename(".template/Cargo.toml", "Cargo.toml"); file::rename(".template/pyproject.toml", "pyproject.toml"); file::delete(".template"); diff --git a/newsfragments/4328.fixed.md b/newsfragments/4328.fixed.md deleted file mode 100644 index f21fdfbcb76..00000000000 --- a/newsfragments/4328.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Fix compile failure in declarative `#[pymodule]` under presence of `#![no_implicit_prelude]`. diff --git a/newsfragments/4349.fixed.md b/newsfragments/4349.fixed.md deleted file mode 100644 index 0895ffa1ae1..00000000000 --- a/newsfragments/4349.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Improve error messages for `#[pyfunction]` defined inside `#[pymethods]` diff --git a/newsfragments/4355.added.md b/newsfragments/4355.added.md deleted file mode 100644 index 1410c0720bf..00000000000 --- a/newsfragments/4355.added.md +++ /dev/null @@ -1,10 +0,0 @@ -* Added an `ffi::compat` namespace to store compatibility shims for C API - functions added in recent versions of Python. - -* Added bindings for `PyDict_GetItemRef` on Python 3.13 and newer. Also added - `ffi::compat::PyDict_GetItemRef` which re-exports the FFI binding on Python - 3.13 or newer and defines a compatibility version on older versions of - Python. This function is inherently safer to use than `PyDict_GetItem` and has - an API that is easier to use than `PyDict_GetItemWithError`. It returns a - strong reference to value, as opposed to the two older functions which return - a possibly unsafe borrowed reference. diff --git a/newsfragments/4355.fixed.md b/newsfragments/4355.fixed.md deleted file mode 100644 index 9a141bc6b96..00000000000 --- a/newsfragments/4355.fixed.md +++ /dev/null @@ -1,2 +0,0 @@ -Avoid creating temporary borrowed reference in dict.get_item bindings. Borrowed -references like this are unsafe in the free-threading build. diff --git a/newsfragments/4359.fixed.md b/newsfragments/4359.fixed.md deleted file mode 100644 index 7174cab0a9d..00000000000 --- a/newsfragments/4359.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Fixed `pyclass` macro hygiene issues for structs and enums and added tests. \ No newline at end of file diff --git a/newsfragments/4382.fixed.md b/newsfragments/4382.fixed.md deleted file mode 100644 index 974ae23d3bf..00000000000 --- a/newsfragments/4382.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Fixed hygiene/span issues of emitted code which affected expansion in `macro_rules` context. \ No newline at end of file diff --git a/newsfragments/4396.fixed.md b/newsfragments/4396.fixed.md deleted file mode 100644 index 285358ad526..00000000000 --- a/newsfragments/4396.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Hide confusing warnings about unsafe usage in `#[pyclass]` implementation. diff --git a/newsfragments/4407.fixed.md b/newsfragments/4407.fixed.md deleted file mode 100644 index be2706bca05..00000000000 --- a/newsfragments/4407.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Fix async functions returning a tuple only returning the first element to Python. diff --git a/newsfragments/4410.added.md b/newsfragments/4410.added.md deleted file mode 100644 index 350a54531bd..00000000000 --- a/newsfragments/4410.added.md +++ /dev/null @@ -1 +0,0 @@ -Add ffi binding `PyList_GetItemRef` and `pyo3_ffi::compat::PyList_GetItemRef` diff --git a/newsfragments/4410.fixed.md b/newsfragments/4410.fixed.md deleted file mode 100644 index f4403409aea..00000000000 --- a/newsfragments/4410.fixed.md +++ /dev/null @@ -1,3 +0,0 @@ -* Avoid creating temporary borrowed reference in list.get_item - bindings. Temporary borrowed references are unsafe in the free-threaded python - build if the list is shared between threads. diff --git a/newsfragments/4420.fixed.md b/newsfragments/4420.fixed.md deleted file mode 100644 index dec974555d9..00000000000 --- a/newsfragments/4420.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Correct FFI definition `PyArg_ParseTupleAndKeywords` to take `*const *const c_char` instead of `*mut *mut c_char` on Python 3.13 and up. diff --git a/newsfragments/4420.removed.md b/newsfragments/4420.removed.md deleted file mode 100644 index 9d17c33e143..00000000000 --- a/newsfragments/4420.removed.md +++ /dev/null @@ -1 +0,0 @@ -Remove FFI definition of private variable `_Py_PackageContext`. diff --git a/newsfragments/4445.added.md b/newsfragments/4445.added.md deleted file mode 100644 index ee6af52d97e..00000000000 --- a/newsfragments/4445.added.md +++ /dev/null @@ -1 +0,0 @@ -Add FFI definitions `compat::Py_NewRef` and `compat::Py_XNewRef`. diff --git a/newsfragments/4445.removed.md b/newsfragments/4445.removed.md deleted file mode 100644 index b5b7e450331..00000000000 --- a/newsfragments/4445.removed.md +++ /dev/null @@ -1 +0,0 @@ -Remove private FFI definitions `_Py_NewRef` and `_Py_XNewRef`. diff --git a/newsfragments/4454.fixed.md b/newsfragments/4454.fixed.md deleted file mode 100644 index e7faf3e690d..00000000000 --- a/newsfragments/4454.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Fix a soundness bug with `PyClassInitializer`: from now you cannot initialize a `PyClassInitializer` with `PyClassInitializer::from(Py).add_subclass(SubClass)`. \ No newline at end of file diff --git a/newsfragments/4456.changed.md b/newsfragments/4456.changed.md deleted file mode 100644 index 094dece12a5..00000000000 --- a/newsfragments/4456.changed.md +++ /dev/null @@ -1 +0,0 @@ -Improve performance of calls to Python by using the vectorcall calling convention where possible. \ No newline at end of file diff --git a/newsfragments/4461.added.md b/newsfragments/4461.added.md deleted file mode 100644 index c151664c843..00000000000 --- a/newsfragments/4461.added.md +++ /dev/null @@ -1 +0,0 @@ -Add FFI definitions `compat::PyObject_CallNoArgs` and `compat::PyObject_CallMethodNoArgs`. diff --git a/newsfragments/4479.fixed.md b/newsfragments/4479.fixed.md deleted file mode 100644 index 15d634543af..00000000000 --- a/newsfragments/4479.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Remove illegal reference counting op inside implementation of `__traverse__` handlers. diff --git a/newsfragments/4481.changed.md b/newsfragments/4481.changed.md deleted file mode 100644 index 9ce455c923c..00000000000 --- a/newsfragments/4481.changed.md +++ /dev/null @@ -1 +0,0 @@ -Mention the type name in the exception message when trying to instantiate a class with no constructor defined. diff --git a/newsfragments/4511.added.md b/newsfragments/4511.added.md deleted file mode 100644 index 6572ddc7238..00000000000 --- a/newsfragments/4511.added.md +++ /dev/null @@ -1 +0,0 @@ -Add Python-ref cloning `clone_ref` for `GILOnceCell>` From 9b4878cb5ee11be46952d4f977a023fd5e61924e Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Wed, 25 Sep 2024 23:12:58 +0200 Subject: [PATCH 284/495] fix unintentional `unsafe_code` trigger (#4574) * Revert "Add missing #[allow(unsafe_code)] attributes (#4396)" This reverts commit 0e03b39caa18d016a6951307b297ca7e6f999e24. * fix unintentional `unsafe_code` trigger * add newsfragment --- newsfragments/4574.fixed.md | 2 ++ pyo3-macros-backend/src/pyclass.rs | 1 - pyo3-macros-backend/src/pymethod.rs | 15 +++++++++------ src/impl_/pyclass.rs | 6 +----- src/macros.rs | 1 - src/tests/hygiene/mod.rs | 1 - src/types/mod.rs | 1 - tests/test_compile_error.rs | 2 ++ tests/ui/forbid_unsafe.rs | 30 +++++++++++++++++++++++++++++ 9 files changed, 44 insertions(+), 15 deletions(-) create mode 100644 newsfragments/4574.fixed.md create mode 100644 tests/ui/forbid_unsafe.rs diff --git a/newsfragments/4574.fixed.md b/newsfragments/4574.fixed.md new file mode 100644 index 00000000000..c996e927289 --- /dev/null +++ b/newsfragments/4574.fixed.md @@ -0,0 +1,2 @@ +Fixes `#[forbid(unsafe_code)]` regression by reverting #4396. +Fixes unintentional `unsafe_code` trigger by adjusting macro hygiene. \ No newline at end of file diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index 291aeb0125a..1da9cfa20ad 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -1809,7 +1809,6 @@ fn impl_pytypeinfo(cls: &syn::Ident, attr: &PyClassArgs, ctx: &Ctx) -> TokenStre }; quote! { - #[allow(unsafe_code)] unsafe impl #pyo3_path::type_object::PyTypeInfo for #cls { const NAME: &'static str = #cls_name; const MODULE: ::std::option::Option<&'static str> = #module; diff --git a/pyo3-macros-backend/src/pymethod.rs b/pyo3-macros-backend/src/pymethod.rs index aa0f3cedfcb..567bd2f5ac8 100644 --- a/pyo3-macros-backend/src/pymethod.rs +++ b/pyo3-macros-backend/src/pymethod.rs @@ -766,14 +766,20 @@ pub fn impl_py_getter_def( // TODO: on MSRV 1.77+, we can use `::std::mem::offset_of!` here, and it should // make it possible for the `MaybeRuntimePyMethodDef` to be a `Static` variant. - let method_def = quote_spanned! {ty.span()=> + let generator = quote_spanned! { ty.span() => + #pyo3_path::impl_::pyclass::MaybeRuntimePyMethodDef::Runtime( + || GENERATOR.generate(#python_name, #doc) + ) + }; + // This is separate so that the unsafe below does not inherit the span and thus does not + // trigger the `unsafe_code` lint + let method_def = quote! { #cfg_attrs { #[allow(unused_imports)] // might not be used if all probes are positve use #pyo3_path::impl_::pyclass::Probe; struct Offset; - #[allow(unsafe_code)] unsafe impl #pyo3_path::impl_::pyclass::OffsetCalculator<#cls, #ty> for Offset { fn offset() -> usize { #pyo3_path::impl_::pyclass::class_offset::<#cls>() + @@ -781,7 +787,6 @@ pub fn impl_py_getter_def( } } - #[allow(unsafe_code)] const GENERATOR: #pyo3_path::impl_::pyclass::PyClassGetterGenerator::< #cls, #ty, @@ -792,9 +797,7 @@ pub fn impl_py_getter_def( { #pyo3_path::impl_::pyclass::IsIntoPyObjectRef::<#ty>::VALUE }, { #pyo3_path::impl_::pyclass::IsIntoPyObject::<#ty>::VALUE }, > = unsafe { #pyo3_path::impl_::pyclass::PyClassGetterGenerator::new() }; - #pyo3_path::impl_::pyclass::MaybeRuntimePyMethodDef::Runtime( - || GENERATOR.generate(#python_name, #doc) - ) + #generator } }; diff --git a/src/impl_/pyclass.rs b/src/impl_/pyclass.rs index b0362174d25..bf94503f1c0 100644 --- a/src/impl_/pyclass.rs +++ b/src/impl_/pyclass.rs @@ -351,7 +351,6 @@ slot_fragment_trait! { #[macro_export] macro_rules! generate_pyclass_getattro_slot { ($cls:ty) => {{ - #[allow(unsafe_code)] unsafe extern "C" fn __wrap( _slf: *mut $crate::ffi::PyObject, attr: *mut $crate::ffi::PyObject, @@ -435,7 +434,6 @@ macro_rules! define_pyclass_setattr_slot { #[macro_export] macro_rules! $generate_macro { ($cls:ty) => {{ - #[allow(unsafe_code)] unsafe extern "C" fn __wrap( _slf: *mut $crate::ffi::PyObject, attr: *mut $crate::ffi::PyObject, @@ -552,7 +550,6 @@ macro_rules! define_pyclass_binary_operator_slot { #[macro_export] macro_rules! $generate_macro { ($cls:ty) => {{ - #[allow(unsafe_code)] unsafe extern "C" fn __wrap( _slf: *mut $crate::ffi::PyObject, _other: *mut $crate::ffi::PyObject, @@ -745,7 +742,6 @@ slot_fragment_trait! { #[macro_export] macro_rules! generate_pyclass_pow_slot { ($cls:ty) => {{ - #[allow(unsafe_code)] unsafe extern "C" fn __wrap( _slf: *mut $crate::ffi::PyObject, _other: *mut $crate::ffi::PyObject, @@ -870,7 +866,7 @@ macro_rules! generate_pyclass_richcompare_slot { ($cls:ty) => {{ #[allow(unknown_lints, non_local_definitions)] impl $cls { - #[allow(non_snake_case, unsafe_code)] + #[allow(non_snake_case)] unsafe extern "C" fn __pymethod___richcmp____( slf: *mut $crate::ffi::PyObject, other: *mut $crate::ffi::PyObject, diff --git a/src/macros.rs b/src/macros.rs index 1a021998bcf..e4ae4c9dc16 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -184,7 +184,6 @@ macro_rules! wrap_pymodule { #[macro_export] macro_rules! append_to_inittab { ($module:ident) => { - #[allow(unsafe_code)] unsafe { if $crate::ffi::Py_IsInitialized() != 0 { ::std::panic!( diff --git a/src/tests/hygiene/mod.rs b/src/tests/hygiene/mod.rs index 9bf89161b24..c950e18da94 100644 --- a/src/tests/hygiene/mod.rs +++ b/src/tests/hygiene/mod.rs @@ -1,6 +1,5 @@ #![no_implicit_prelude] #![allow(dead_code, unused_variables, clippy::unnecessary_wraps)] -#![deny(unsafe_code)] // The modules in this test are used to check PyO3 macro expansion is hygienic. By locating the test // inside the crate the global `::pyo3` namespace is not available, so in combination with diff --git a/src/types/mod.rs b/src/types/mod.rs index d1020931d76..bd33e5a3ded 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -153,7 +153,6 @@ macro_rules! pyobject_native_static_type_object( #[macro_export] macro_rules! pyobject_native_type_info( ($name:ty, $typeobject:expr, $module:expr $(, #checkfunction=$checkfunction:path)? $(;$generics:ident)*) => { - #[allow(unsafe_code)] unsafe impl<$($generics,)*> $crate::type_object::PyTypeInfo for $name { const NAME: &'static str = stringify!($name); const MODULE: ::std::option::Option<&'static str> = $module; diff --git a/tests/test_compile_error.rs b/tests/test_compile_error.rs index 5ae4e550733..012d759a99d 100644 --- a/tests/test_compile_error.rs +++ b/tests/test_compile_error.rs @@ -54,6 +54,8 @@ fn test_compile_errors() { #[cfg(any(not(Py_LIMITED_API), Py_3_10))] // to avoid PyFunctionArgument for &str t.compile_fail("tests/ui/invalid_cancel_handle.rs"); t.pass("tests/ui/pymodule_missing_docs.rs"); + #[cfg(not(Py_LIMITED_API))] + t.pass("tests/ui/forbid_unsafe.rs"); #[cfg(all(Py_LIMITED_API, not(feature = "experimental-async")))] // output changes with async feature t.compile_fail("tests/ui/abi3_inheritance.rs"); diff --git a/tests/ui/forbid_unsafe.rs b/tests/ui/forbid_unsafe.rs new file mode 100644 index 00000000000..9b62886b650 --- /dev/null +++ b/tests/ui/forbid_unsafe.rs @@ -0,0 +1,30 @@ +#![forbid(unsafe_code)] + +use pyo3::*; + +#[allow(unexpected_cfgs)] +#[path = "../../src/tests/hygiene/mod.rs"] +mod hygiene; + +mod gh_4394 { + use pyo3::prelude::*; + + #[derive(Eq, Ord, PartialEq, PartialOrd, Clone)] + #[pyclass(get_all)] + pub struct VersionSpecifier { + pub(crate) operator: Operator, + pub(crate) version: Version, + } + + #[derive(Eq, Ord, PartialEq, PartialOrd, Debug, Hash, Clone, Copy)] + #[pyo3::pyclass(eq, eq_int)] + pub enum Operator { + Equal, + } + + #[derive(Clone, Eq, PartialEq, PartialOrd, Ord)] + #[pyclass] + pub struct Version; +} + +fn main() {} From e5e33386bea9fe523acb9a3fa5d9a014ad9d266b Mon Sep 17 00:00:00 2001 From: digitalsentinel Date: Thu, 26 Sep 2024 08:11:07 -0700 Subject: [PATCH 285/495] docs: semantic change typo (#4561) * Typo * Correct and clear --- guide/src/types.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/guide/src/types.md b/guide/src/types.md index b975e8400a7..bd47c127b60 100644 --- a/guide/src/types.md +++ b/guide/src/types.md @@ -40,7 +40,7 @@ To convert a `Py` into a `Bound<'py, T>`, the `Py::bind` and `Py::into_bound` [`Bound<'py, T>`][Bound] is the counterpart to `Py` which is also bound to the `'py` lifetime. It can be thought of as equivalent to the Rust tuple `(Python<'py>, Py)`. -By having the binding to the `'py` lifetime, `Bound<'py, T>` can offer the complete PyO3 API at maximum efficiency. This means that in almost all cases where `Py` is not necessary for lifetime reasons, `Bound<'py, T>` should be used. +By having the binding to the `'py` lifetime, `Bound<'py, T>` can offer the complete PyO3 API at maximum efficiency. This means that `Bound<'py, T>` should usually be used whenever carrying this lifetime is acceptable, and `Py` otherwise. `Bound<'py, T>` engages in Python reference counting. This means that `Bound<'py, T>` owns a Python object. Rust code which just wants to borrow a Python object should use a shared reference `&Bound<'py, T>`. Just like `std::sync::Arc`, using `.clone()` and `drop()` will cheaply increment and decrement the reference count of the object (just in this case, the reference counting is implemented by the Python interpreter itself). From cb96613d9e2fed76de75473e9582d9ac95aa432b Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Thu, 26 Sep 2024 17:20:19 +0200 Subject: [PATCH 286/495] migrate `PySequenceMethods` trait bounds (#4559) --- .../python-from-rust/calling-existing-code.md | 2 +- src/types/list.rs | 93 +++++--- src/types/sequence.rs | 202 +++++++++++------- src/types/tuple.rs | 120 ++++++----- 4 files changed, 243 insertions(+), 174 deletions(-) diff --git a/guide/src/python-from-rust/calling-existing-code.md b/guide/src/python-from-rust/calling-existing-code.md index aae4317ae2c..812fb20e7ae 100644 --- a/guide/src/python-from-rust/calling-existing-code.md +++ b/guide/src/python-from-rust/calling-existing-code.md @@ -304,7 +304,7 @@ fn main() -> PyResult<()> { .import("sys")? .getattr("path")? .downcast_into::()?; - syspath.insert(0, &path)?; + syspath.insert(0, path)?; let app: Py = PyModule::from_code(py, py_app.as_c_str(), c_str!(""), c_str!(""))? .getattr("run")? .into(); diff --git a/src/types/list.rs b/src/types/list.rs index 0fa33d41a44..9c6f8ff21c3 100644 --- a/src/types/list.rs +++ b/src/types/list.rs @@ -5,8 +5,9 @@ use crate::ffi::{self, Py_ssize_t}; use crate::ffi_ptr_ext::FfiPtrExt; use crate::internal_tricks::get_ssize_index; use crate::types::{PySequence, PyTuple}; -use crate::{Bound, PyAny, PyObject, Python, ToPyObject}; +use crate::{Borrowed, Bound, BoundObject, PyAny, PyObject, Python, ToPyObject}; +use crate::prelude::IntoPyObject; use crate::types::any::PyAnyMethods; use crate::types::sequence::PySequenceMethods; @@ -189,7 +190,7 @@ pub trait PyListMethods<'py>: crate::sealed::Sealed { /// Raises `IndexError` if the index is out of range. fn set_item(&self, index: usize, item: I) -> PyResult<()> where - I: ToPyObject; + I: IntoPyObject<'py>; /// Deletes the `index`th element of self. /// @@ -209,28 +210,28 @@ pub trait PyListMethods<'py>: crate::sealed::Sealed { /// Appends an item to the list. fn append(&self, item: I) -> PyResult<()> where - I: ToPyObject; + I: IntoPyObject<'py>; /// Inserts an item at the specified index. /// /// If `index >= self.len()`, inserts at the end. fn insert(&self, index: usize, item: I) -> PyResult<()> where - I: ToPyObject; + I: IntoPyObject<'py>; /// Determines if self contains `value`. /// /// This is equivalent to the Python expression `value in self`. fn contains(&self, value: V) -> PyResult where - V: ToPyObject; + V: IntoPyObject<'py>; /// Returns the first index `i` for which `self[i] == value`. /// /// This is equivalent to the Python expression `self.index(value)`. fn index(&self, value: V) -> PyResult where - V: ToPyObject; + V: IntoPyObject<'py>; /// Returns an iterator over this list's items. fn iter(&self) -> BoundListIterator<'py>; @@ -323,7 +324,7 @@ impl<'py> PyListMethods<'py> for Bound<'py, PyList> { /// Raises `IndexError` if the index is out of range. fn set_item(&self, index: usize, item: I) -> PyResult<()> where - I: ToPyObject, + I: IntoPyObject<'py>, { fn inner(list: &Bound<'_, PyList>, index: usize, item: Bound<'_, PyAny>) -> PyResult<()> { err::error_on_minusone(list.py(), unsafe { @@ -332,7 +333,14 @@ impl<'py> PyListMethods<'py> for Bound<'py, PyList> { } let py = self.py(); - inner(self, index, item.to_object(py).into_bound(py)) + inner( + self, + index, + item.into_pyobject(py) + .map_err(Into::into)? + .into_any() + .into_bound(), + ) } /// Deletes the `index`th element of self. @@ -369,16 +377,22 @@ impl<'py> PyListMethods<'py> for Bound<'py, PyList> { /// Appends an item to the list. fn append(&self, item: I) -> PyResult<()> where - I: ToPyObject, + I: IntoPyObject<'py>, { - fn inner(list: &Bound<'_, PyList>, item: Bound<'_, PyAny>) -> PyResult<()> { + fn inner(list: &Bound<'_, PyList>, item: Borrowed<'_, '_, PyAny>) -> PyResult<()> { err::error_on_minusone(list.py(), unsafe { ffi::PyList_Append(list.as_ptr(), item.as_ptr()) }) } let py = self.py(); - inner(self, item.to_object(py).into_bound(py)) + inner( + self, + item.into_pyobject(py) + .map_err(Into::into)? + .into_any() + .as_borrowed(), + ) } /// Inserts an item at the specified index. @@ -386,16 +400,27 @@ impl<'py> PyListMethods<'py> for Bound<'py, PyList> { /// If `index >= self.len()`, inserts at the end. fn insert(&self, index: usize, item: I) -> PyResult<()> where - I: ToPyObject, + I: IntoPyObject<'py>, { - fn inner(list: &Bound<'_, PyList>, index: usize, item: Bound<'_, PyAny>) -> PyResult<()> { + fn inner( + list: &Bound<'_, PyList>, + index: usize, + item: Borrowed<'_, '_, PyAny>, + ) -> PyResult<()> { err::error_on_minusone(list.py(), unsafe { ffi::PyList_Insert(list.as_ptr(), get_ssize_index(index), item.as_ptr()) }) } let py = self.py(); - inner(self, index, item.to_object(py).into_bound(py)) + inner( + self, + index, + item.into_pyobject(py) + .map_err(Into::into)? + .into_any() + .as_borrowed(), + ) } /// Determines if self contains `value`. @@ -404,7 +429,7 @@ impl<'py> PyListMethods<'py> for Bound<'py, PyList> { #[inline] fn contains(&self, value: V) -> PyResult where - V: ToPyObject, + V: IntoPyObject<'py>, { self.as_sequence().contains(value) } @@ -415,7 +440,7 @@ impl<'py> PyListMethods<'py> for Bound<'py, PyList> { #[inline] fn index(&self, value: V) -> PyResult where - V: ToPyObject, + V: IntoPyObject<'py>, { self.as_sequence().index(value) } @@ -544,7 +569,6 @@ mod tests { use crate::types::sequence::PySequenceMethods; use crate::types::{PyList, PyTuple}; use crate::{ffi, Python}; - use crate::{IntoPy, PyObject, ToPyObject}; #[test] fn test_new() { @@ -591,8 +615,8 @@ mod tests { fn test_set_item() { Python::with_gil(|py| { let list = PyList::new(py, [2, 3, 5, 7]); - let val = 42i32.to_object(py); - let val2 = 42i32.to_object(py); + let val = 42i32.into_pyobject(py).unwrap(); + let val2 = 42i32.into_pyobject(py).unwrap(); assert_eq!(2, list.get_item(0).unwrap().extract::().unwrap()); list.set_item(0, val).unwrap(); assert_eq!(42, list.get_item(0).unwrap().extract::().unwrap()); @@ -607,8 +631,8 @@ mod tests { let cnt; { let v = vec![2]; - let ob = v.to_object(py); - let list = ob.downcast_bound::(py).unwrap(); + let ob = v.into_pyobject(py).unwrap(); + let list = ob.downcast::().unwrap(); cnt = obj.get_refcnt(); list.set_item(0, &obj).unwrap(); } @@ -621,8 +645,8 @@ mod tests { fn test_insert() { Python::with_gil(|py| { let list = PyList::new(py, [2, 3, 5, 7]); - let val = 42i32.to_object(py); - let val2 = 43i32.to_object(py); + let val = 42i32.into_pyobject(py).unwrap(); + let val2 = 43i32.into_pyobject(py).unwrap(); assert_eq!(4, list.len()); assert_eq!(2, list.get_item(0).unwrap().extract::().unwrap()); list.insert(0, val).unwrap(); @@ -691,8 +715,8 @@ mod tests { fn test_iter_size_hint() { Python::with_gil(|py| { let v = vec![2, 3, 5, 7]; - let ob = v.to_object(py); - let list = ob.downcast_bound::(py).unwrap(); + let ob = (&v).into_pyobject(py).unwrap(); + let list = ob.downcast::().unwrap(); let mut iter = list.iter(); assert_eq!(iter.size_hint(), (v.len(), Some(v.len()))); @@ -710,8 +734,8 @@ mod tests { fn test_iter_rev() { Python::with_gil(|py| { let v = vec![2, 3, 5, 7]; - let ob = v.to_object(py); - let list = ob.downcast_bound::(py).unwrap(); + let ob = v.into_pyobject(py).unwrap(); + let list = ob.downcast::().unwrap(); let mut iter = list.iter().rev(); @@ -832,10 +856,10 @@ mod tests { } #[test] - fn test_array_into_py() { + fn test_array_into_pyobject() { Python::with_gil(|py| { - let array: PyObject = [1, 2].into_py(py); - let list = array.downcast_bound::(py).unwrap(); + let array = [1, 2].into_pyobject(py).unwrap(); + let list = array.downcast::().unwrap(); assert_eq!(1, list.get_item(0).unwrap().extract::().unwrap()); assert_eq!(2, list.get_item(1).unwrap().extract::().unwrap()); }); @@ -924,13 +948,13 @@ mod tests { let list = PyList::new(py, [1, 1, 2, 3, 5, 8]); assert_eq!(6, list.len()); - let bad_needle = 7i32.to_object(py); + let bad_needle = 7i32.into_pyobject(py).unwrap(); assert!(!list.contains(&bad_needle).unwrap()); - let good_needle = 8i32.to_object(py); + let good_needle = 8i32.into_pyobject(py).unwrap(); assert!(list.contains(&good_needle).unwrap()); - let type_coerced_needle = 8f32.to_object(py); + let type_coerced_needle = 8f32.into_pyobject(py).unwrap(); assert!(list.contains(&type_coerced_needle).unwrap()); }); } @@ -948,6 +972,7 @@ mod tests { }); } + use crate::prelude::IntoPyObject; use std::ops::Range; // An iterator that lies about its `ExactSizeIterator` implementation. @@ -1005,7 +1030,7 @@ mod tests { #[cfg(feature = "macros")] #[test] fn bad_clone_mem_leaks() { - use crate::{Py, PyAny}; + use crate::{IntoPy, Py, PyAny, ToPyObject}; use std::sync::atomic::{AtomicUsize, Ordering::SeqCst}; static NEEDS_DESTRUCTING_COUNT: AtomicUsize = AtomicUsize::new(0); diff --git a/src/types/sequence.rs b/src/types/sequence.rs index 2e8ef4e53b8..2fca2b18a51 100644 --- a/src/types/sequence.rs +++ b/src/types/sequence.rs @@ -5,11 +5,12 @@ use crate::ffi_ptr_ext::FfiPtrExt; use crate::inspect::types::TypeInfo; use crate::instance::Bound; use crate::internal_tricks::get_ssize_index; +use crate::prelude::IntoPyObject; use crate::py_result_ext::PyResultExt; use crate::sync::GILOnceCell; use crate::type_object::PyTypeInfo; use crate::types::{any::PyAnyMethods, PyAny, PyList, PyString, PyTuple, PyType}; -use crate::{ffi, FromPyObject, Py, PyTypeCheck, Python, ToPyObject}; +use crate::{ffi, Borrowed, BoundObject, FromPyObject, Py, PyTypeCheck, Python}; /// Represents a reference to a Python object supporting the sequence protocol. /// @@ -91,7 +92,7 @@ pub trait PySequenceMethods<'py>: crate::sealed::Sealed { /// This is equivalent to the Python statement `self[i] = v`. fn set_item(&self, i: usize, item: I) -> PyResult<()> where - I: ToPyObject; + I: IntoPyObject<'py>; /// Deletes the `i`th element of self. /// @@ -113,21 +114,21 @@ pub trait PySequenceMethods<'py>: crate::sealed::Sealed { #[cfg(not(PyPy))] fn count(&self, value: V) -> PyResult where - V: ToPyObject; + V: IntoPyObject<'py>; /// Determines if self contains `value`. /// /// This is equivalent to the Python expression `value in self`. fn contains(&self, value: V) -> PyResult where - V: ToPyObject; + V: IntoPyObject<'py>; /// Returns the first index `i` for which `self[i] == value`. /// /// This is equivalent to the Python expression `self.index(value)`. fn index(&self, value: V) -> PyResult where - V: ToPyObject; + V: IntoPyObject<'py>; /// Returns a fresh list based on the Sequence. fn to_list(&self) -> PyResult>; @@ -205,16 +206,27 @@ impl<'py> PySequenceMethods<'py> for Bound<'py, PySequence> { #[inline] fn set_item(&self, i: usize, item: I) -> PyResult<()> where - I: ToPyObject, + I: IntoPyObject<'py>, { - fn inner(seq: &Bound<'_, PySequence>, i: usize, item: Bound<'_, PyAny>) -> PyResult<()> { + fn inner( + seq: &Bound<'_, PySequence>, + i: usize, + item: Borrowed<'_, '_, PyAny>, + ) -> PyResult<()> { err::error_on_minusone(seq.py(), unsafe { ffi::PySequence_SetItem(seq.as_ptr(), get_ssize_index(i), item.as_ptr()) }) } let py = self.py(); - inner(self, i, item.to_object(py).into_bound(py)) + inner( + self, + i, + item.into_pyobject(py) + .map_err(Into::into)? + .into_any() + .as_borrowed(), + ) } #[inline] @@ -247,24 +259,31 @@ impl<'py> PySequenceMethods<'py> for Bound<'py, PySequence> { #[cfg(not(PyPy))] fn count(&self, value: V) -> PyResult where - V: ToPyObject, + V: IntoPyObject<'py>, { - fn inner(seq: &Bound<'_, PySequence>, value: Bound<'_, PyAny>) -> PyResult { + fn inner(seq: &Bound<'_, PySequence>, value: Borrowed<'_, '_, PyAny>) -> PyResult { let r = unsafe { ffi::PySequence_Count(seq.as_ptr(), value.as_ptr()) }; crate::err::error_on_minusone(seq.py(), r)?; Ok(r as usize) } let py = self.py(); - inner(self, value.to_object(py).into_bound(py)) + inner( + self, + value + .into_pyobject(py) + .map_err(Into::into)? + .into_any() + .as_borrowed(), + ) } #[inline] fn contains(&self, value: V) -> PyResult where - V: ToPyObject, + V: IntoPyObject<'py>, { - fn inner(seq: &Bound<'_, PySequence>, value: Bound<'_, PyAny>) -> PyResult { + fn inner(seq: &Bound<'_, PySequence>, value: Borrowed<'_, '_, PyAny>) -> PyResult { let r = unsafe { ffi::PySequence_Contains(seq.as_ptr(), value.as_ptr()) }; match r { 0 => Ok(false), @@ -274,22 +293,36 @@ impl<'py> PySequenceMethods<'py> for Bound<'py, PySequence> { } let py = self.py(); - inner(self, value.to_object(py).into_bound(py)) + inner( + self, + value + .into_pyobject(py) + .map_err(Into::into)? + .into_any() + .as_borrowed(), + ) } #[inline] fn index(&self, value: V) -> PyResult where - V: ToPyObject, + V: IntoPyObject<'py>, { - fn inner(seq: &Bound<'_, PySequence>, value: Bound<'_, PyAny>) -> PyResult { + fn inner(seq: &Bound<'_, PySequence>, value: Borrowed<'_, '_, PyAny>) -> PyResult { let r = unsafe { ffi::PySequence_Index(seq.as_ptr(), value.as_ptr()) }; crate::err::error_on_minusone(seq.py(), r)?; Ok(r as usize) } let py = self.py(); - inner(self, value.to_object(self.py()).into_bound(py)) + inner( + self, + value + .into_pyobject(py) + .map_err(Into::into)? + .into_any() + .as_borrowed(), + ) } #[inline] @@ -375,15 +408,16 @@ impl PyTypeCheck for PySequence { #[cfg(test)] mod tests { + use crate::prelude::IntoPyObject; use crate::types::{PyAnyMethods, PyList, PySequence, PySequenceMethods, PyTuple}; - use crate::{ffi, PyObject, Python, ToPyObject}; + use crate::{ffi, PyObject, Python}; fn get_object() -> PyObject { // Convenience function for getting a single unique object Python::with_gil(|py| { let obj = py.eval(ffi::c_str!("object()"), None, None).unwrap(); - obj.to_object(py) + obj.into_pyobject(py).unwrap().unbind() }) } @@ -391,7 +425,11 @@ mod tests { fn test_numbers_are_not_sequences() { Python::with_gil(|py| { let v = 42i32; - assert!(v.to_object(py).downcast_bound::(py).is_err()); + assert!(v + .into_pyobject(py) + .unwrap() + .downcast::() + .is_err()); }); } @@ -399,7 +437,11 @@ mod tests { fn test_strings_are_sequences() { Python::with_gil(|py| { let v = "London Calling"; - assert!(v.to_object(py).downcast_bound::(py).is_ok()); + assert!(v + .into_pyobject(py) + .unwrap() + .downcast::() + .is_ok()); }); } @@ -407,10 +449,10 @@ mod tests { fn test_strings_cannot_be_extracted_to_vec() { Python::with_gil(|py| { let v = "London Calling"; - let ob = v.to_object(py); + let ob = v.into_pyobject(py).unwrap(); - assert!(ob.extract::>(py).is_err()); - assert!(ob.extract::>(py).is_err()); + assert!(ob.extract::>().is_err()); + assert!(ob.extract::>().is_err()); }); } @@ -418,11 +460,11 @@ mod tests { fn test_seq_empty() { Python::with_gil(|py| { let v: Vec = vec![]; - let ob = v.to_object(py); - let seq = ob.downcast_bound::(py).unwrap(); + let ob = v.into_pyobject(py).unwrap(); + let seq = ob.downcast::().unwrap(); assert_eq!(0, seq.len().unwrap()); - let needle = 7i32.to_object(py); + let needle = 7i32.into_pyobject(py).unwrap(); assert!(!seq.contains(&needle).unwrap()); }); } @@ -430,12 +472,12 @@ mod tests { #[test] fn test_seq_is_empty() { Python::with_gil(|py| { - let list = vec![1].to_object(py); - let seq = list.downcast_bound::(py).unwrap(); + let list = vec![1].into_pyobject(py).unwrap(); + let seq = list.downcast::().unwrap(); assert!(!seq.is_empty().unwrap()); let vec: Vec = Vec::new(); - let empty_list = vec.to_object(py); - let empty_seq = empty_list.downcast_bound::(py).unwrap(); + let empty_list = vec.into_pyobject(py).unwrap(); + let empty_seq = empty_list.downcast::().unwrap(); assert!(empty_seq.is_empty().unwrap()); }); } @@ -444,17 +486,17 @@ mod tests { fn test_seq_contains() { Python::with_gil(|py| { let v: Vec = vec![1, 1, 2, 3, 5, 8]; - let ob = v.to_object(py); - let seq = ob.downcast_bound::(py).unwrap(); + let ob = v.into_pyobject(py).unwrap(); + let seq = ob.downcast::().unwrap(); assert_eq!(6, seq.len().unwrap()); - let bad_needle = 7i32.to_object(py); + let bad_needle = 7i32.into_pyobject(py).unwrap(); assert!(!seq.contains(&bad_needle).unwrap()); - let good_needle = 8i32.to_object(py); + let good_needle = 8i32.into_pyobject(py).unwrap(); assert!(seq.contains(&good_needle).unwrap()); - let type_coerced_needle = 8f32.to_object(py); + let type_coerced_needle = 8f32.into_pyobject(py).unwrap(); assert!(seq.contains(&type_coerced_needle).unwrap()); }); } @@ -463,8 +505,8 @@ mod tests { fn test_seq_get_item() { Python::with_gil(|py| { let v: Vec = vec![1, 1, 2, 3, 5, 8]; - let ob = v.to_object(py); - let seq = ob.downcast_bound::(py).unwrap(); + let ob = v.into_pyobject(py).unwrap(); + let seq = ob.downcast::().unwrap(); assert_eq!(1, seq.get_item(0).unwrap().extract::().unwrap()); assert_eq!(1, seq.get_item(1).unwrap().extract::().unwrap()); assert_eq!(2, seq.get_item(2).unwrap().extract::().unwrap()); @@ -479,8 +521,8 @@ mod tests { fn test_seq_del_item() { Python::with_gil(|py| { let v: Vec = vec![1, 1, 2, 3, 5, 8]; - let ob = v.to_object(py); - let seq = ob.downcast_bound::(py).unwrap(); + let ob = v.into_pyobject(py).unwrap(); + let seq = ob.downcast::().unwrap(); assert!(seq.del_item(10).is_err()); assert_eq!(1, seq.get_item(0).unwrap().extract::().unwrap()); assert!(seq.del_item(0).is_ok()); @@ -503,8 +545,8 @@ mod tests { fn test_seq_set_item() { Python::with_gil(|py| { let v: Vec = vec![1, 2]; - let ob = v.to_object(py); - let seq = ob.downcast_bound::(py).unwrap(); + let ob = v.into_pyobject(py).unwrap(); + let seq = ob.downcast::().unwrap(); assert_eq!(2, seq.get_item(1).unwrap().extract::().unwrap()); assert!(seq.set_item(1, 10).is_ok()); assert_eq!(10, seq.get_item(1).unwrap().extract::().unwrap()); @@ -517,8 +559,8 @@ mod tests { Python::with_gil(|py| { let v: Vec = vec![1, 2]; - let ob = v.to_object(py); - let seq = ob.downcast_bound::(py).unwrap(); + let ob = v.into_pyobject(py).unwrap(); + let seq = ob.downcast::().unwrap(); assert!(seq.set_item(1, &obj).is_ok()); assert!(seq.get_item(1).unwrap().as_ptr() == obj.as_ptr()); }); @@ -532,8 +574,8 @@ mod tests { fn test_seq_get_slice() { Python::with_gil(|py| { let v: Vec = vec![1, 1, 2, 3, 5, 8]; - let ob = v.to_object(py); - let seq = ob.downcast_bound::(py).unwrap(); + let ob = v.into_pyobject(py).unwrap(); + let seq = ob.downcast::().unwrap(); assert_eq!( [1, 2, 3], seq.get_slice(1, 4).unwrap().extract::<[i32; 3]>().unwrap() @@ -553,10 +595,10 @@ mod tests { Python::with_gil(|py| { let v: Vec = vec![1, 1, 2, 3, 5, 8]; let w: Vec = vec![7, 4]; - let ob = v.to_object(py); - let seq = ob.downcast_bound::(py).unwrap(); - let ins = w.to_object(py); - seq.set_slice(1, 4, ins.bind(py)).unwrap(); + let ob = v.into_pyobject(py).unwrap(); + let seq = ob.downcast::().unwrap(); + let ins = w.into_pyobject(py).unwrap(); + seq.set_slice(1, 4, &ins).unwrap(); assert_eq!([1, 7, 4, 5, 8], seq.extract::<[i32; 5]>().unwrap()); seq.set_slice(3, 100, &PyList::empty(py)).unwrap(); assert_eq!([1, 7, 4], seq.extract::<[i32; 3]>().unwrap()); @@ -567,8 +609,8 @@ mod tests { fn test_del_slice() { Python::with_gil(|py| { let v: Vec = vec![1, 1, 2, 3, 5, 8]; - let ob = v.to_object(py); - let seq = ob.downcast_bound::(py).unwrap(); + let ob = v.into_pyobject(py).unwrap(); + let seq = ob.downcast::().unwrap(); seq.del_slice(1, 4).unwrap(); assert_eq!([1, 5, 8], seq.extract::<[i32; 3]>().unwrap()); seq.del_slice(1, 100).unwrap(); @@ -580,8 +622,8 @@ mod tests { fn test_seq_index() { Python::with_gil(|py| { let v: Vec = vec![1, 1, 2, 3, 5, 8]; - let ob = v.to_object(py); - let seq = ob.downcast_bound::(py).unwrap(); + let ob = v.into_pyobject(py).unwrap(); + let seq = ob.downcast::().unwrap(); assert_eq!(0, seq.index(1i32).unwrap()); assert_eq!(2, seq.index(2i32).unwrap()); assert_eq!(3, seq.index(3i32).unwrap()); @@ -596,8 +638,8 @@ mod tests { fn test_seq_count() { Python::with_gil(|py| { let v: Vec = vec![1, 1, 2, 3, 5, 8]; - let ob = v.to_object(py); - let seq = ob.downcast_bound::(py).unwrap(); + let ob = v.into_pyobject(py).unwrap(); + let seq = ob.downcast::().unwrap(); assert_eq!(2, seq.count(1i32).unwrap()); assert_eq!(1, seq.count(2i32).unwrap()); assert_eq!(1, seq.count(3i32).unwrap()); @@ -611,8 +653,8 @@ mod tests { fn test_seq_iter() { Python::with_gil(|py| { let v: Vec = vec![1, 1, 2, 3, 5, 8]; - let ob = v.to_object(py); - let seq = ob.downcast_bound::(py).unwrap(); + let ob = (&v).into_pyobject(py).unwrap(); + let seq = ob.downcast::().unwrap(); let mut idx = 0; for el in seq.iter().unwrap() { assert_eq!(v[idx], el.unwrap().extract::().unwrap()); @@ -626,13 +668,13 @@ mod tests { fn test_seq_strings() { Python::with_gil(|py| { let v = vec!["It", "was", "the", "worst", "of", "times"]; - let ob = v.to_object(py); - let seq = ob.downcast_bound::(py).unwrap(); + let ob = v.into_pyobject(py).unwrap(); + let seq = ob.downcast::().unwrap(); - let bad_needle = "blurst".to_object(py); + let bad_needle = "blurst".into_pyobject(py).unwrap(); assert!(!seq.contains(bad_needle).unwrap()); - let good_needle = "worst".to_object(py); + let good_needle = "worst".into_pyobject(py).unwrap(); assert!(seq.contains(good_needle).unwrap()); }); } @@ -641,8 +683,8 @@ mod tests { fn test_seq_concat() { Python::with_gil(|py| { let v: Vec = vec![1, 2, 3]; - let ob = v.to_object(py); - let seq = ob.downcast_bound::(py).unwrap(); + let ob = v.into_pyobject(py).unwrap(); + let seq = ob.downcast::().unwrap(); let concat_seq = seq.concat(seq).unwrap(); assert_eq!(6, concat_seq.len().unwrap()); let concat_v: Vec = vec![1, 2, 3, 1, 2, 3]; @@ -656,8 +698,8 @@ mod tests { fn test_seq_concat_string() { Python::with_gil(|py| { let v = "string"; - let ob = v.to_object(py); - let seq = ob.downcast_bound::(py).unwrap(); + let ob = v.into_pyobject(py).unwrap(); + let seq = ob.downcast::().unwrap(); let concat_seq = seq.concat(seq).unwrap(); assert_eq!(12, concat_seq.len().unwrap()); let concat_v = "stringstring".to_owned(); @@ -671,8 +713,8 @@ mod tests { fn test_seq_repeat() { Python::with_gil(|py| { let v = vec!["foo", "bar"]; - let ob = v.to_object(py); - let seq = ob.downcast_bound::(py).unwrap(); + let ob = v.into_pyobject(py).unwrap(); + let seq = ob.downcast::().unwrap(); let repeat_seq = seq.repeat(3).unwrap(); assert_eq!(6, repeat_seq.len().unwrap()); let repeated = ["foo", "bar", "foo", "bar", "foo", "bar"]; @@ -686,8 +728,8 @@ mod tests { fn test_seq_inplace() { Python::with_gil(|py| { let v = vec!["foo", "bar"]; - let ob = v.to_object(py); - let seq = ob.downcast_bound::(py).unwrap(); + let ob = v.into_pyobject(py).unwrap(); + let seq = ob.downcast::().unwrap(); let rep_seq = seq.in_place_repeat(3).unwrap(); assert_eq!(6, seq.len().unwrap()); assert!(seq.is(&rep_seq)); @@ -702,8 +744,8 @@ mod tests { fn test_list_coercion() { Python::with_gil(|py| { let v = vec!["foo", "bar"]; - let ob = v.to_object(py); - let seq = ob.downcast_bound::(py).unwrap(); + let ob = (&v).into_pyobject(py).unwrap(); + let seq = ob.downcast::().unwrap(); assert!(seq.to_list().unwrap().eq(PyList::new(py, &v)).unwrap()); }); } @@ -712,8 +754,8 @@ mod tests { fn test_strings_coerce_to_lists() { Python::with_gil(|py| { let v = "foo"; - let ob = v.to_object(py); - let seq = ob.downcast_bound::(py).unwrap(); + let ob = v.into_pyobject(py).unwrap(); + let seq = ob.downcast::().unwrap(); assert!(seq .to_list() .unwrap() @@ -726,8 +768,8 @@ mod tests { fn test_tuple_coercion() { Python::with_gil(|py| { let v = ("foo", "bar"); - let ob = v.to_object(py); - let seq = ob.downcast_bound::(py).unwrap(); + let ob = v.into_pyobject(py).unwrap(); + let seq = ob.downcast::().unwrap(); assert!(seq .to_tuple() .unwrap() @@ -740,8 +782,8 @@ mod tests { fn test_lists_coerce_to_tuples() { Python::with_gil(|py| { let v = vec!["foo", "bar"]; - let ob = v.to_object(py); - let seq = ob.downcast_bound::(py).unwrap(); + let ob = (&v).into_pyobject(py).unwrap(); + let seq = ob.downcast::().unwrap(); assert!(seq.to_tuple().unwrap().eq(PyTuple::new(py, &v)).unwrap()); }); } @@ -786,8 +828,8 @@ mod tests { fn test_seq_downcast_unchecked() { Python::with_gil(|py| { let v = vec!["foo", "bar"]; - let ob = v.to_object(py); - let seq = ob.downcast_bound::(py).unwrap(); + let ob = v.into_pyobject(py).unwrap(); + let seq = ob.downcast::().unwrap(); let type_ptr = seq.as_ref(); let seq_from = unsafe { type_ptr.downcast_unchecked::() }; assert!(seq_from.to_list().is_ok()); diff --git a/src/types/tuple.rs b/src/types/tuple.rs index d54bdf79f8d..642a5c2d055 100644 --- a/src/types/tuple.rs +++ b/src/types/tuple.rs @@ -207,14 +207,14 @@ pub trait PyTupleMethods<'py>: crate::sealed::Sealed { /// This is equivalent to the Python expression `value in self`. fn contains(&self, value: V) -> PyResult where - V: ToPyObject; + V: IntoPyObject<'py>; /// Returns the first index `i` for which `self[i] == value`. /// /// This is equivalent to the Python expression `self.index(value)`. fn index(&self, value: V) -> PyResult where - V: ToPyObject; + V: IntoPyObject<'py>; /// Returns an iterator over the tuple items. fn iter(&self) -> BoundTupleIterator<'py>; @@ -290,7 +290,7 @@ impl<'py> PyTupleMethods<'py> for Bound<'py, PyTuple> { #[inline] fn contains(&self, value: V) -> PyResult where - V: ToPyObject, + V: IntoPyObject<'py>, { self.as_sequence().contains(value) } @@ -298,7 +298,7 @@ impl<'py> PyTupleMethods<'py> for Bound<'py, PyTuple> { #[inline] fn index(&self, value: V) -> PyResult where - V: ToPyObject, + V: IntoPyObject<'py>, { self.as_sequence().index(value) } @@ -799,7 +799,7 @@ tuple_conversion!( #[cfg(test)] mod tests { use crate::types::{any::PyAnyMethods, tuple::PyTupleMethods, PyList, PyTuple}; - use crate::{Python, ToPyObject}; + use crate::Python; use std::collections::HashSet; #[test] @@ -820,8 +820,8 @@ mod tests { #[test] fn test_len() { Python::with_gil(|py| { - let ob = (1, 2, 3).to_object(py); - let tuple = ob.downcast_bound::(py).unwrap(); + let ob = (1, 2, 3).into_pyobject(py).unwrap(); + let tuple = ob.downcast::().unwrap(); assert_eq!(3, tuple.len()); assert!(!tuple.is_empty()); let ob = tuple.as_any(); @@ -852,8 +852,8 @@ mod tests { #[test] fn test_iter() { Python::with_gil(|py| { - let ob = (1, 2, 3).to_object(py); - let tuple = ob.downcast_bound::(py).unwrap(); + let ob = (1, 2, 3).into_pyobject(py).unwrap(); + let tuple = ob.downcast::().unwrap(); assert_eq!(3, tuple.len()); let mut iter = tuple.iter(); @@ -876,8 +876,8 @@ mod tests { #[test] fn test_iter_rev() { Python::with_gil(|py| { - let ob = (1, 2, 3).to_object(py); - let tuple = ob.downcast_bound::(py).unwrap(); + let ob = (1, 2, 3).into_pyobject(py).unwrap(); + let tuple = ob.downcast::().unwrap(); assert_eq!(3, tuple.len()); let mut iter = tuple.iter().rev(); @@ -946,8 +946,8 @@ mod tests { #[test] fn test_into_iter() { Python::with_gil(|py| { - let ob = (1, 2, 3).to_object(py); - let tuple = ob.downcast_bound::(py).unwrap(); + let ob = (1, 2, 3).into_pyobject(py).unwrap(); + let tuple = ob.downcast::().unwrap(); assert_eq!(3, tuple.len()); for (i, item) in tuple.iter().enumerate() { @@ -958,11 +958,8 @@ mod tests { #[test] fn test_into_iter_bound() { - use crate::Bound; - Python::with_gil(|py| { - let ob = (1, 2, 3).to_object(py); - let tuple: &Bound<'_, PyTuple> = ob.downcast_bound(py).unwrap(); + let tuple = (1, 2, 3).into_pyobject(py).unwrap(); assert_eq!(3, tuple.len()); let mut items = vec![]; @@ -977,8 +974,8 @@ mod tests { #[cfg(not(any(Py_LIMITED_API, GraalPy)))] fn test_as_slice() { Python::with_gil(|py| { - let ob = (1, 2, 3).to_object(py); - let tuple = ob.downcast_bound::(py).unwrap(); + let ob = (1, 2, 3).into_pyobject(py).unwrap(); + let tuple = ob.downcast::().unwrap(); let slice = tuple.as_slice(); assert_eq!(3, slice.len()); @@ -991,61 +988,65 @@ mod tests { #[test] fn test_tuple_lengths_up_to_12() { Python::with_gil(|py| { - let t0 = (0,).to_object(py); - let t1 = (0, 1).to_object(py); - let t2 = (0, 1, 2).to_object(py); - let t3 = (0, 1, 2, 3).to_object(py); - let t4 = (0, 1, 2, 3, 4).to_object(py); - let t5 = (0, 1, 2, 3, 4, 5).to_object(py); - let t6 = (0, 1, 2, 3, 4, 5, 6).to_object(py); - let t7 = (0, 1, 2, 3, 4, 5, 6, 7).to_object(py); - let t8 = (0, 1, 2, 3, 4, 5, 6, 7, 8).to_object(py); - let t9 = (0, 1, 2, 3, 4, 5, 6, 7, 8, 9).to_object(py); - let t10 = (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10).to_object(py); - let t11 = (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11).to_object(py); - - assert_eq!(t0.extract::<(i32,)>(py).unwrap(), (0,)); - assert_eq!(t1.extract::<(i32, i32)>(py).unwrap(), (0, 1,)); - assert_eq!(t2.extract::<(i32, i32, i32)>(py).unwrap(), (0, 1, 2,)); + let t0 = (0,).into_pyobject(py).unwrap(); + let t1 = (0, 1).into_pyobject(py).unwrap(); + let t2 = (0, 1, 2).into_pyobject(py).unwrap(); + let t3 = (0, 1, 2, 3).into_pyobject(py).unwrap(); + let t4 = (0, 1, 2, 3, 4).into_pyobject(py).unwrap(); + let t5 = (0, 1, 2, 3, 4, 5).into_pyobject(py).unwrap(); + let t6 = (0, 1, 2, 3, 4, 5, 6).into_pyobject(py).unwrap(); + let t7 = (0, 1, 2, 3, 4, 5, 6, 7).into_pyobject(py).unwrap(); + let t8 = (0, 1, 2, 3, 4, 5, 6, 7, 8).into_pyobject(py).unwrap(); + let t9 = (0, 1, 2, 3, 4, 5, 6, 7, 8, 9).into_pyobject(py).unwrap(); + let t10 = (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10) + .into_pyobject(py) + .unwrap(); + let t11 = (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11) + .into_pyobject(py) + .unwrap(); + + assert_eq!(t0.extract::<(i32,)>().unwrap(), (0,)); + assert_eq!(t1.extract::<(i32, i32)>().unwrap(), (0, 1,)); + assert_eq!(t2.extract::<(i32, i32, i32)>().unwrap(), (0, 1, 2,)); assert_eq!( - t3.extract::<(i32, i32, i32, i32,)>(py).unwrap(), + t3.extract::<(i32, i32, i32, i32,)>().unwrap(), (0, 1, 2, 3,) ); assert_eq!( - t4.extract::<(i32, i32, i32, i32, i32,)>(py).unwrap(), + t4.extract::<(i32, i32, i32, i32, i32,)>().unwrap(), (0, 1, 2, 3, 4,) ); assert_eq!( - t5.extract::<(i32, i32, i32, i32, i32, i32,)>(py).unwrap(), + t5.extract::<(i32, i32, i32, i32, i32, i32,)>().unwrap(), (0, 1, 2, 3, 4, 5,) ); assert_eq!( - t6.extract::<(i32, i32, i32, i32, i32, i32, i32,)>(py) + t6.extract::<(i32, i32, i32, i32, i32, i32, i32,)>() .unwrap(), (0, 1, 2, 3, 4, 5, 6,) ); assert_eq!( - t7.extract::<(i32, i32, i32, i32, i32, i32, i32, i32,)>(py) + t7.extract::<(i32, i32, i32, i32, i32, i32, i32, i32,)>() .unwrap(), (0, 1, 2, 3, 4, 5, 6, 7,) ); assert_eq!( - t8.extract::<(i32, i32, i32, i32, i32, i32, i32, i32, i32,)>(py) + t8.extract::<(i32, i32, i32, i32, i32, i32, i32, i32, i32,)>() .unwrap(), (0, 1, 2, 3, 4, 5, 6, 7, 8,) ); assert_eq!( - t9.extract::<(i32, i32, i32, i32, i32, i32, i32, i32, i32, i32,)>(py) + t9.extract::<(i32, i32, i32, i32, i32, i32, i32, i32, i32, i32,)>() .unwrap(), (0, 1, 2, 3, 4, 5, 6, 7, 8, 9,) ); assert_eq!( - t10.extract::<(i32, i32, i32, i32, i32, i32, i32, i32, i32, i32, i32,)>(py) + t10.extract::<(i32, i32, i32, i32, i32, i32, i32, i32, i32, i32, i32,)>() .unwrap(), (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,) ); assert_eq!( - t11.extract::<(i32, i32, i32, i32, i32, i32, i32, i32, i32, i32, i32, i32,)>(py) + t11.extract::<(i32, i32, i32, i32, i32, i32, i32, i32, i32, i32, i32, i32,)>() .unwrap(), (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11,) ); @@ -1055,8 +1056,8 @@ mod tests { #[test] fn test_tuple_get_item_invalid_index() { Python::with_gil(|py| { - let ob = (1, 2, 3).to_object(py); - let tuple = ob.downcast_bound::(py).unwrap(); + let ob = (1, 2, 3).into_pyobject(py).unwrap(); + let tuple = ob.downcast::().unwrap(); let obj = tuple.get_item(5); assert!(obj.is_err()); assert_eq!( @@ -1069,8 +1070,8 @@ mod tests { #[test] fn test_tuple_get_item_sanity() { Python::with_gil(|py| { - let ob = (1, 2, 3).to_object(py); - let tuple = ob.downcast_bound::(py).unwrap(); + let ob = (1, 2, 3).into_pyobject(py).unwrap(); + let tuple = ob.downcast::().unwrap(); let obj = tuple.get_item(0); assert_eq!(obj.unwrap().extract::().unwrap(), 1); }); @@ -1080,8 +1081,8 @@ mod tests { #[test] fn test_tuple_get_item_unchecked_sanity() { Python::with_gil(|py| { - let ob = (1, 2, 3).to_object(py); - let tuple = ob.downcast_bound::(py).unwrap(); + let ob = (1, 2, 3).into_pyobject(py).unwrap(); + let tuple = ob.downcast::().unwrap(); let obj = unsafe { tuple.get_item_unchecked(0) }; assert_eq!(obj.extract::().unwrap(), 1); }); @@ -1090,17 +1091,17 @@ mod tests { #[test] fn test_tuple_contains() { Python::with_gil(|py| { - let ob = (1, 1, 2, 3, 5, 8).to_object(py); - let tuple = ob.downcast_bound::(py).unwrap(); + let ob = (1, 1, 2, 3, 5, 8).into_pyobject(py).unwrap(); + let tuple = ob.downcast::().unwrap(); assert_eq!(6, tuple.len()); - let bad_needle = 7i32.to_object(py); + let bad_needle = 7i32.into_pyobject(py).unwrap(); assert!(!tuple.contains(&bad_needle).unwrap()); - let good_needle = 8i32.to_object(py); + let good_needle = 8i32.into_pyobject(py).unwrap(); assert!(tuple.contains(&good_needle).unwrap()); - let type_coerced_needle = 8f32.to_object(py); + let type_coerced_needle = 8f32.into_pyobject(py).unwrap(); assert!(tuple.contains(&type_coerced_needle).unwrap()); }); } @@ -1108,8 +1109,8 @@ mod tests { #[test] fn test_tuple_index() { Python::with_gil(|py| { - let ob = (1, 1, 2, 3, 5, 8).to_object(py); - let tuple = ob.downcast_bound::(py).unwrap(); + let ob = (1, 1, 2, 3, 5, 8).into_pyobject(py).unwrap(); + let tuple = ob.downcast::().unwrap(); assert_eq!(0, tuple.index(1i32).unwrap()); assert_eq!(2, tuple.index(2i32).unwrap()); assert_eq!(3, tuple.index(3i32).unwrap()); @@ -1119,6 +1120,7 @@ mod tests { }); } + use crate::prelude::IntoPyObject; use std::ops::Range; // An iterator that lies about its `ExactSizeIterator` implementation. @@ -1176,7 +1178,7 @@ mod tests { #[cfg(feature = "macros")] #[test] fn bad_clone_mem_leaks() { - use crate::{IntoPy, Py, PyAny}; + use crate::{IntoPy, Py, PyAny, ToPyObject}; use std::sync::atomic::{AtomicUsize, Ordering::SeqCst}; static NEEDS_DESTRUCTING_COUNT: AtomicUsize = AtomicUsize::new(0); @@ -1244,7 +1246,7 @@ mod tests { #[cfg(feature = "macros")] #[test] fn bad_clone_mem_leaks_2() { - use crate::{IntoPy, Py, PyAny}; + use crate::{IntoPy, Py, PyAny, ToPyObject}; use std::sync::atomic::{AtomicUsize, Ordering::SeqCst}; static NEEDS_DESTRUCTING_COUNT: AtomicUsize = AtomicUsize::new(0); From 1ae08de84f1c0c68ecf71d18c4aa1e4375b1f672 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Thu, 26 Sep 2024 15:30:44 -0600 Subject: [PATCH 287/495] fix flaky tests on free-threaded build (#4576) * fix flaky tests on free-threaded build * only skip asserts when __traverse__ is in play --- tests/test_gc.rs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/tests/test_gc.rs b/tests/test_gc.rs index a9380c29f7d..1f55d91d496 100644 --- a/tests/test_gc.rs +++ b/tests/test_gc.rs @@ -34,6 +34,15 @@ fn class_with_freelist() { }); } +/// Tests that drop is eventually called on objects that are dropped when the +/// GIL is not held. +/// +/// On the free-threaded build, threads are resumed before tp_clear() calls +/// finish. Therefore, if the type needs __traverse__, drop might not necessarily +/// be called by the time the a test re-acquires a Python thread state and checks if +/// drop has been called. +/// +/// See https://peps.python.org/pep-0703/#stop-the-world struct TestDropCall { drop_called: Arc, } @@ -120,9 +129,6 @@ fn gc_integration() { Python::with_gil(|py| { py.run(ffi::c_str!("import gc; gc.collect()"), None, None) .unwrap(); - // threads are resumed before tp_clear() calls finish, so drop might not - // necessarily be called when we get here see - // https://peps.python.org/pep-0703/#stop-the-world #[cfg(not(Py_GIL_DISABLED))] assert!(drop_called.load(Ordering::Relaxed)); }); @@ -482,6 +488,7 @@ fn drop_during_traversal_with_gil() { .unwrap(); }); } + #[cfg(not(Py_GIL_DISABLED))] assert!(drop_called.load(Ordering::Relaxed)); } @@ -517,6 +524,7 @@ fn drop_during_traversal_without_gil() { .unwrap(); }); } + #[cfg(not(Py_GIL_DISABLED))] assert!(drop_called.load(Ordering::Relaxed)); } From e053dbcf9f56170019a3c36f4eb1e7d4b72016ce Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Fri, 27 Sep 2024 19:51:40 +0100 Subject: [PATCH 288/495] stop emitting Py_3_6 cfg (#4583) --- pyo3-build-config/src/impl_.rs | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index 7b9ae3ea9fc..a7ae4254bbe 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -165,9 +165,7 @@ impl InterpreterConfig { 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 { + for i in MINIMUM_SUPPORTED_VERSION.minor..=self.version.minor { out.push(format!("cargo:rustc-cfg=Py_3_{}", i)); } @@ -2708,7 +2706,6 @@ mod tests { 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(), ] @@ -2721,7 +2718,6 @@ mod tests { 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(), @@ -2748,7 +2744,6 @@ mod tests { 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(), ] @@ -2761,7 +2756,6 @@ mod tests { 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:rustc-cfg=Py_LIMITED_API".to_owned(), @@ -2793,7 +2787,6 @@ mod tests { 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=Py_3_9".to_owned(), @@ -2827,7 +2820,6 @@ mod tests { 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(), ] From 2fa97f907a2b1ecad4d1a9bb254246540cf1d1dc Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Sun, 29 Sep 2024 09:26:30 +0200 Subject: [PATCH 289/495] Migrate `PyTuple` / `PyList` constructors (#4580) * migrate `PyList::new` * migrate `PyTuple::new` * add newsfragment --- guide/src/conversions/traits.md | 6 +- guide/src/exception.md | 7 +- guide/src/python-from-rust/function-calls.md | 2 +- guide/src/trait-bounds.md | 10 +- guide/src/types.md | 6 +- newsfragments/4580.changed.md | 1 + pyo3-benches/benches/bench_list.rs | 8 +- pytests/src/datetime.rs | 12 +- src/conversion.rs | 4 +- src/conversions/smallvec.rs | 6 +- src/impl_/extract_argument.rs | 2 +- src/impl_/pymodule.rs | 2 +- src/instance.rs | 2 +- src/macros.rs | 7 +- src/marker.rs | 4 +- src/tests/hygiene/pymethods.rs | 5 +- src/types/any.rs | 2 +- src/types/datetime.rs | 2 +- src/types/dict.rs | 4 +- src/types/list.rs | 134 +++++++++--------- src/types/sequence.rs | 16 ++- src/types/tuple.rs | 135 ++++++++++--------- src/types/typeobject.rs | 5 +- tests/test_frompyobject.rs | 21 ++- tests/test_getter_setter.rs | 2 +- tests/test_methods.rs | 2 +- tests/test_proto_methods.rs | 2 + tests/test_various.rs | 5 +- tests/ui/invalid_property_args.stderr | 14 +- tests/ui/missing_intopy.stderr | 14 +- 30 files changed, 243 insertions(+), 199 deletions(-) create mode 100644 newsfragments/4580.changed.md diff --git a/guide/src/conversions/traits.md b/guide/src/conversions/traits.md index 7f61616eefe..fd9e90097f2 100644 --- a/guide/src/conversions/traits.md +++ b/guide/src/conversions/traits.md @@ -13,7 +13,7 @@ fails, so usually you will use something like # use pyo3::types::PyList; # fn main() -> PyResult<()> { # Python::with_gil(|py| { -# let list = PyList::new(py, b"foo"); +# let list = PyList::new(py, b"foo")?; let v: Vec = list.extract()?; # assert_eq!(&v, &[102, 111, 111]); # Ok(()) @@ -183,7 +183,7 @@ struct RustyTuple(String, String); # use pyo3::types::PyTuple; # fn main() -> PyResult<()> { # Python::with_gil(|py| -> PyResult<()> { -# let tuple = PyTuple::new(py, vec!["test", "test2"]); +# let tuple = PyTuple::new(py, vec!["test", "test2"])?; # # let rustytuple: RustyTuple = tuple.extract()?; # assert_eq!(rustytuple.0, "test"); @@ -206,7 +206,7 @@ struct RustyTuple((String,)); # use pyo3::types::PyTuple; # fn main() -> PyResult<()> { # Python::with_gil(|py| -> PyResult<()> { -# let tuple = PyTuple::new(py, vec!["test"]); +# let tuple = PyTuple::new(py, vec!["test"])?; # # let rustytuple: RustyTuple = tuple.extract()?; # assert_eq!((rustytuple.0).0, "test"); diff --git a/guide/src/exception.md b/guide/src/exception.md index 32a56078af5..4a1f1425d72 100644 --- a/guide/src/exception.md +++ b/guide/src/exception.md @@ -78,12 +78,15 @@ In PyO3 every object has the [`PyAny::is_instance`] and [`PyAny::is_instance_of` use pyo3::prelude::*; use pyo3::types::{PyBool, PyList}; +# fn main() -> PyResult<()> { Python::with_gil(|py| { assert!(PyBool::new(py, true).is_instance_of::()); - let list = PyList::new(py, &[1, 2, 3, 4]); + let list = PyList::new(py, &[1, 2, 3, 4])?; assert!(!list.is_instance_of::()); assert!(list.is_instance_of::()); -}); +# Ok(()) +}) +# } ``` To check the type of an exception, you can similarly do: diff --git a/guide/src/python-from-rust/function-calls.md b/guide/src/python-from-rust/function-calls.md index 12544dc02bd..f1eb8025431 100644 --- a/guide/src/python-from-rust/function-calls.md +++ b/guide/src/python-from-rust/function-calls.md @@ -50,7 +50,7 @@ fn main() -> PyResult<()> { fun.call1(py, args)?; // call object with Python tuple of positional arguments - let args = PyTuple::new(py, &[arg1, arg2, arg3]); + let args = PyTuple::new(py, &[arg1, arg2, arg3])?; fun.call1(py, args)?; Ok(()) }) diff --git a/guide/src/trait-bounds.md b/guide/src/trait-bounds.md index 434ac7dd968..e1b8e82f1db 100644 --- a/guide/src/trait-bounds.md +++ b/guide/src/trait-bounds.md @@ -84,7 +84,7 @@ impl Model for UserModel { Python::with_gil(|py| { self.model .bind(py) - .call_method("set_variables", (PyList::new(py, var),), None) + .call_method("set_variables", (PyList::new(py, var).unwrap(),), None) .unwrap(); }) } @@ -182,7 +182,7 @@ This wrapper will also perform the type conversions between Python and Rust. # println!("Rust calling Python to set the variables"); # Python::with_gil(|py| { # self.model.bind(py) -# .call_method("set_variables", (PyList::new(py, var),), None) +# .call_method("set_variables", (PyList::new(py, var).unwrap(),), None) # .unwrap(); # }) # } @@ -360,7 +360,7 @@ impl Model for UserModel { # println!("Rust calling Python to set the variables"); # Python::with_gil(|py| { # self.model.bind(py) -# .call_method("set_variables", (PyList::new(py, var),), None) +# .call_method("set_variables", (PyList::new(py, var).unwrap(),), None) # .unwrap(); # }) # } @@ -419,7 +419,7 @@ impl Model for UserModel { # println!("Rust calling Python to set the variables"); # Python::with_gil(|py| { # let py_model = self.model.bind(py) -# .call_method("set_variables", (PyList::new(py, var),), None) +# .call_method("set_variables", (PyList::new(py, var).unwrap(),), None) # .unwrap(); # }) # } @@ -517,7 +517,7 @@ impl Model for UserModel { Python::with_gil(|py| { self.model .bind(py) - .call_method("set_variables", (PyList::new(py, var),), None) + .call_method("set_variables", (PyList::new(py, var).unwrap(),), None) .unwrap(); }) } diff --git a/guide/src/types.md b/guide/src/types.md index bd47c127b60..06559d49d13 100644 --- a/guide/src/types.md +++ b/guide/src/types.md @@ -145,7 +145,7 @@ use pyo3::types::PyTuple; # fn example<'py>(py: Python<'py>) -> PyResult<()> { // Create a new tuple with the elements (0, 1, 2) -let t = PyTuple::new(py, [0, 1, 2]); +let t = PyTuple::new(py, [0, 1, 2])?; for i in 0..=2 { let entry: Borrowed<'_, 'py, PyAny> = t.get_borrowed_item(i)?; // `PyAnyMethods::extract` is available on `Borrowed` @@ -232,7 +232,7 @@ fn get_first_item<'py>(list: &Bound<'py, PyList>) -> PyResult> list.get_item(0) } # Python::with_gil(|py| { -# let l = PyList::new(py, ["hello world"]); +# let l = PyList::new(py, ["hello world"]).unwrap(); # assert!(get_first_item(&l).unwrap().eq("hello world").unwrap()); # }) ``` @@ -295,7 +295,7 @@ For example, the following snippet extracts a Rust tuple of integers from a Pyth # use pyo3::types::PyTuple; # fn example<'py>(py: Python<'py>) -> PyResult<()> { // create a new Python `tuple`, and use `.into_any()` to erase the type -let obj: Bound<'py, PyAny> = PyTuple::new(py, [1, 2, 3]).into_any(); +let obj: Bound<'py, PyAny> = PyTuple::new(py, [1, 2, 3])?.into_any(); // extracting the Python `tuple` to a rust `(i32, i32, i32)` tuple let (x, y, z) = obj.extract::<(i32, i32, i32)>()?; diff --git a/newsfragments/4580.changed.md b/newsfragments/4580.changed.md new file mode 100644 index 00000000000..72b838d4055 --- /dev/null +++ b/newsfragments/4580.changed.md @@ -0,0 +1 @@ +`PyList::new` and `PyTuple::new` are now fallible due to `IntoPyObject` migration. \ No newline at end of file diff --git a/pyo3-benches/benches/bench_list.rs b/pyo3-benches/benches/bench_list.rs index 4f6374d75eb..39829f52ce8 100644 --- a/pyo3-benches/benches/bench_list.rs +++ b/pyo3-benches/benches/bench_list.rs @@ -8,7 +8,7 @@ use pyo3::types::{PyList, PySequence}; fn iter_list(b: &mut Bencher<'_>) { Python::with_gil(|py| { const LEN: usize = 100_000; - let list = PyList::new(py, 0..LEN); + let list = PyList::new(py, 0..LEN).unwrap(); let mut sum = 0; b.iter(|| { for x in &list { @@ -29,7 +29,7 @@ fn list_new(b: &mut Bencher<'_>) { fn list_get_item(b: &mut Bencher<'_>) { Python::with_gil(|py| { const LEN: usize = 50_000; - let list = PyList::new(py, 0..LEN); + let list = PyList::new(py, 0..LEN).unwrap(); let mut sum = 0; b.iter(|| { for i in 0..LEN { @@ -43,7 +43,7 @@ fn list_get_item(b: &mut Bencher<'_>) { fn list_get_item_unchecked(b: &mut Bencher<'_>) { Python::with_gil(|py| { const LEN: usize = 50_000; - let list = PyList::new(py, 0..LEN); + let list = PyList::new(py, 0..LEN).unwrap(); let mut sum = 0; b.iter(|| { for i in 0..LEN { @@ -58,7 +58,7 @@ fn list_get_item_unchecked(b: &mut Bencher<'_>) { fn sequence_from_list(b: &mut Bencher<'_>) { Python::with_gil(|py| { const LEN: usize = 50_000; - let list = &PyList::new(py, 0..LEN); + let list = &PyList::new(py, 0..LEN).unwrap(); b.iter(|| black_box(list).downcast::().unwrap()); }); } diff --git a/pytests/src/datetime.rs b/pytests/src/datetime.rs index 744be279431..3bdf103b62e 100644 --- a/pytests/src/datetime.rs +++ b/pytests/src/datetime.rs @@ -12,7 +12,7 @@ fn make_date(py: Python<'_>, year: i32, month: u8, day: u8) -> PyResult(d: &Bound<'py, PyDate>) -> Bound<'py, PyTuple> { +fn get_date_tuple<'py>(d: &Bound<'py, PyDate>) -> PyResult> { PyTuple::new( d.py(), [d.get_year(), d.get_month() as i32, d.get_day() as i32], @@ -52,7 +52,7 @@ fn time_with_fold<'py>( } #[pyfunction] -fn get_time_tuple<'py>(dt: &Bound<'py, PyTime>) -> Bound<'py, PyTuple> { +fn get_time_tuple<'py>(dt: &Bound<'py, PyTime>) -> PyResult> { PyTuple::new( dt.py(), [ @@ -65,7 +65,7 @@ fn get_time_tuple<'py>(dt: &Bound<'py, PyTime>) -> Bound<'py, PyTuple> { } #[pyfunction] -fn get_time_tuple_fold<'py>(dt: &Bound<'py, PyTime>) -> Bound<'py, PyTuple> { +fn get_time_tuple_fold<'py>(dt: &Bound<'py, PyTime>) -> PyResult> { PyTuple::new( dt.py(), [ @@ -89,7 +89,7 @@ fn make_delta( } #[pyfunction] -fn get_delta_tuple<'py>(delta: &Bound<'py, PyDelta>) -> Bound<'py, PyTuple> { +fn get_delta_tuple<'py>(delta: &Bound<'py, PyDelta>) -> PyResult> { PyTuple::new( delta.py(), [ @@ -128,7 +128,7 @@ fn make_datetime<'py>( } #[pyfunction] -fn get_datetime_tuple<'py>(dt: &Bound<'py, PyDateTime>) -> Bound<'py, PyTuple> { +fn get_datetime_tuple<'py>(dt: &Bound<'py, PyDateTime>) -> PyResult> { PyTuple::new( dt.py(), [ @@ -144,7 +144,7 @@ fn get_datetime_tuple<'py>(dt: &Bound<'py, PyDateTime>) -> Bound<'py, PyTuple> { } #[pyfunction] -fn get_datetime_tuple_fold<'py>(dt: &Bound<'py, PyDateTime>) -> Bound<'py, PyTuple> { +fn get_datetime_tuple_fold<'py>(dt: &Bound<'py, PyDateTime>) -> PyResult> { PyTuple::new( dt.py(), [ diff --git a/src/conversion.rs b/src/conversion.rs index c74c529bbb8..3cc73072ed9 100644 --- a/src/conversion.rs +++ b/src/conversion.rs @@ -304,7 +304,7 @@ pub trait IntoPyObject<'py>: Sized { let mut iter = iter.into_iter().map(|e| { e.into_pyobject(py) .map(BoundObject::into_any) - .map(BoundObject::unbind) + .map(BoundObject::into_bound) .map_err(Into::into) }); let list = crate::types::list::try_new_from_iter(py, &mut iter); @@ -327,7 +327,7 @@ pub trait IntoPyObject<'py>: Sized { let mut iter = iter.into_iter().map(|e| { e.into_pyobject(py) .map(BoundObject::into_any) - .map(BoundObject::unbind) + .map(BoundObject::into_bound) .map_err(Into::into) }); let list = crate::types::list::try_new_from_iter(py, &mut iter); diff --git a/src/conversions/smallvec.rs b/src/conversions/smallvec.rs index 8c13c7e8299..be90113344e 100644 --- a/src/conversions/smallvec.rs +++ b/src/conversions/smallvec.rs @@ -140,7 +140,7 @@ mod tests { Python::with_gil(|py| { let sv: SmallVec<[u64; 8]> = [1, 2, 3, 4, 5].iter().cloned().collect(); let hso: PyObject = sv.clone().into_py(py); - let l = PyList::new(py, [1, 2, 3, 4, 5]); + let l = PyList::new(py, [1, 2, 3, 4, 5]).unwrap(); assert!(l.eq(hso).unwrap()); }); } @@ -148,7 +148,7 @@ mod tests { #[test] fn test_smallvec_from_py_object() { Python::with_gil(|py| { - let l = PyList::new(py, [1, 2, 3, 4, 5]); + let l = PyList::new(py, [1, 2, 3, 4, 5]).unwrap(); let sv: SmallVec<[u64; 8]> = l.extract().unwrap(); assert_eq!(sv.as_slice(), [1, 2, 3, 4, 5]); }); @@ -171,7 +171,7 @@ mod tests { Python::with_gil(|py| { let sv: SmallVec<[u64; 8]> = [1, 2, 3, 4, 5].iter().cloned().collect(); let hso: PyObject = sv.to_object(py); - let l = PyList::new(py, [1, 2, 3, 4, 5]); + let l = PyList::new(py, [1, 2, 3, 4, 5]).unwrap(); assert!(l.eq(hso).unwrap()); }); } diff --git a/src/impl_/extract_argument.rs b/src/impl_/extract_argument.rs index bb31f96d8b7..098722060c6 100644 --- a/src/impl_/extract_argument.rs +++ b/src/impl_/extract_argument.rs @@ -703,7 +703,7 @@ impl<'py> VarargsHandler<'py> for TupleVarargs { varargs: &[Option>], _function_description: &FunctionDescription, ) -> PyResult { - Ok(PyTuple::new(py, varargs)) + PyTuple::new(py, varargs) } #[inline] diff --git a/src/impl_/pymodule.rs b/src/impl_/pymodule.rs index 270d32f15a9..ab38bc49e8e 100644 --- a/src/impl_/pymodule.rs +++ b/src/impl_/pymodule.rs @@ -97,7 +97,7 @@ impl ModuleDef { .import("sys")? .getattr("implementation")? .getattr("version")?; - if version.lt(crate::types::PyTuple::new(py, PYPY_GOOD_VERSION))? { + if version.lt(crate::types::PyTuple::new(py, PYPY_GOOD_VERSION)?)? { let warn = py.import("warnings")?.getattr("warn")?; warn.call1(( "PyPy 3.7 versions older than 7.3.8 are known to have binary \ diff --git a/src/instance.rs b/src/instance.rs index 26c6b14ffa0..1ff18f82466 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -640,7 +640,7 @@ impl<'a, 'py, T> Borrowed<'a, 'py, T> { /// /// # fn main() -> PyResult<()> { /// Python::with_gil(|py| -> PyResult<()> { - /// let tuple = PyTuple::new(py, [1, 2, 3]); + /// let tuple = PyTuple::new(py, [1, 2, 3])?; /// /// // borrows from `tuple`, so can only be /// // used while `tuple` stays alive diff --git a/src/macros.rs b/src/macros.rs index e4ae4c9dc16..6148d9662c5 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -11,10 +11,13 @@ /// ``` /// use pyo3::{prelude::*, py_run, types::PyList}; /// +/// # fn main() -> PyResult<()> { /// Python::with_gil(|py| { -/// let list = PyList::new(py, &[1, 2, 3]); +/// let list = PyList::new(py, &[1, 2, 3])?; /// py_run!(py, list, "assert list == [1, 2, 3]"); -/// }); +/// # Ok(()) +/// }) +/// # } /// ``` /// /// You can use this macro to test pyfunctions or pyclasses quickly. diff --git a/src/marker.rs b/src/marker.rs index 8af307621a8..92a154b3694 100644 --- a/src/marker.rs +++ b/src/marker.rs @@ -939,7 +939,7 @@ mod tests { // If allow_threads is implemented correctly, this thread still owns the GIL here // so the following Python calls should not cause crashes. - let list = PyList::new(py, [1, 2, 3, 4]); + let list = PyList::new(py, [1, 2, 3, 4]).unwrap(); assert_eq!(list.extract::>().unwrap(), vec![1, 2, 3, 4]); }); } @@ -947,7 +947,7 @@ mod tests { #[cfg(not(pyo3_disable_reference_pool))] #[test] fn test_allow_threads_pass_stuff_in() { - let list = Python::with_gil(|py| PyList::new(py, vec!["foo", "bar"]).unbind()); + let list = Python::with_gil(|py| PyList::new(py, vec!["foo", "bar"]).unwrap().unbind()); let mut v = vec![1, 2, 3]; let a = std::sync::Arc::new(String::from("foo")); diff --git a/src/tests/hygiene/pymethods.rs b/src/tests/hygiene/pymethods.rs index 6a1a2a50d13..5c027a5c3ae 100644 --- a/src/tests/hygiene/pymethods.rs +++ b/src/tests/hygiene/pymethods.rs @@ -72,7 +72,10 @@ impl Dummy { fn __delattr__(&mut self, name: ::std::string::String) {} - fn __dir__<'py>(&self, py: crate::Python<'py>) -> crate::Bound<'py, crate::types::PyList> { + fn __dir__<'py>( + &self, + py: crate::Python<'py>, + ) -> crate::PyResult> { crate::types::PyList::new(py, ::std::vec![0_u8]) } diff --git a/src/types/any.rs b/src/types/any.rs index 5e40a0fdc4b..37b79720730 100644 --- a/src/types/any.rs +++ b/src/types/any.rs @@ -2021,7 +2021,7 @@ class SimpleClass: let empty_list = PyList::empty(py).into_any(); assert!(empty_list.is_empty().unwrap()); - let list = PyList::new(py, vec![1, 2, 3]).into_any(); + let list = PyList::new(py, vec![1, 2, 3]).unwrap().into_any(); assert!(!list.is_empty().unwrap()); let not_container = 5.to_object(py).into_bound(py); diff --git a/src/types/datetime.rs b/src/types/datetime.rs index 2f6906064f3..ac956c250d3 100644 --- a/src/types/datetime.rs +++ b/src/types/datetime.rs @@ -223,7 +223,7 @@ impl PyDate { /// /// This is equivalent to `datetime.date.fromtimestamp` pub fn from_timestamp(py: Python<'_>, timestamp: i64) -> PyResult> { - let time_tuple = PyTuple::new(py, [timestamp]); + let time_tuple = PyTuple::new(py, [timestamp])?; // safety ensure that the API is loaded let _api = ensure_datetime_api(py)?; diff --git a/src/types/dict.rs b/src/types/dict.rs index 1be0ed22362..2136158e89e 100644 --- a/src/types/dict.rs +++ b/src/types/dict.rs @@ -629,7 +629,7 @@ mod tests { #[cfg(not(any(PyPy, GraalPy)))] fn test_from_sequence() { Python::with_gil(|py| { - let items = PyList::new(py, vec![("a", 1), ("b", 2)]); + let items = PyList::new(py, vec![("a", 1), ("b", 2)]).unwrap(); let dict = PyDict::from_sequence(&items).unwrap(); assert_eq!( 1, @@ -660,7 +660,7 @@ mod tests { #[cfg(not(any(PyPy, GraalPy)))] fn test_from_sequence_err() { Python::with_gil(|py| { - let items = PyList::new(py, vec!["a", "b"]); + let items = PyList::new(py, vec!["a", "b"]).unwrap(); assert!(PyDict::from_sequence(&items).is_err()); }); } diff --git a/src/types/list.rs b/src/types/list.rs index 9c6f8ff21c3..4db14849d46 100644 --- a/src/types/list.rs +++ b/src/types/list.rs @@ -25,18 +25,18 @@ pyobject_native_type_core!(PyList, pyobject_native_static_type_object!(ffi::PyLi #[inline] #[track_caller] -pub(crate) fn new_from_iter<'py>( - py: Python<'py>, - elements: &mut dyn ExactSizeIterator, -) -> Bound<'py, PyList> { - try_new_from_iter(py, &mut elements.map(Ok)).unwrap() +pub(crate) fn new_from_iter( + py: Python<'_>, + elements: impl ExactSizeIterator, +) -> Bound<'_, PyList> { + try_new_from_iter(py, elements.map(|e| e.into_bound(py)).map(Ok)).unwrap() } #[inline] #[track_caller] pub(crate) fn try_new_from_iter<'py>( py: Python<'py>, - elements: &mut dyn ExactSizeIterator>, + mut elements: impl ExactSizeIterator>>, ) -> PyResult> { unsafe { // PyList_New checks for overflow but has a bad error message, so we check ourselves @@ -54,7 +54,7 @@ pub(crate) fn try_new_from_iter<'py>( let mut counter: Py_ssize_t = 0; - for obj in elements.take(len as usize) { + for obj in (&mut elements).take(len as usize) { #[cfg(not(Py_LIMITED_API))] ffi::PyList_SET_ITEM(ptr, counter, obj?.into_ptr()); #[cfg(Py_LIMITED_API)] @@ -81,12 +81,13 @@ impl PyList { /// use pyo3::prelude::*; /// use pyo3::types::PyList; /// - /// # fn main() { + /// # fn main() -> PyResult<()> { /// Python::with_gil(|py| { /// let elements: Vec = vec![0, 1, 2, 3, 4, 5]; - /// let list = PyList::new(py, elements); + /// let list = PyList::new(py, elements)?; /// assert_eq!(format!("{:?}", list), "[0, 1, 2, 3, 4, 5]"); - /// }); + /// # Ok(()) + /// }) /// # } /// ``` /// @@ -96,16 +97,21 @@ impl PyList { /// All standard library structures implement this trait correctly, if they do, so calling this /// function with (for example) [`Vec`]`` or `&[T]` will always succeed. #[track_caller] - pub fn new( - py: Python<'_>, + pub fn new<'py, T, U>( + py: Python<'py>, elements: impl IntoIterator, - ) -> Bound<'_, PyList> + ) -> PyResult> where - T: ToPyObject, + T: IntoPyObject<'py>, U: ExactSizeIterator, { - let mut iter = elements.into_iter().map(|e| e.to_object(py)); - new_from_iter(py, &mut iter) + let iter = elements.into_iter().map(|e| { + e.into_pyobject(py) + .map(BoundObject::into_any) + .map(BoundObject::into_bound) + .map_err(Into::into) + }); + try_new_from_iter(py, iter) } /// Deprecated name for [`PyList::new`]. @@ -120,7 +126,7 @@ impl PyList { T: ToPyObject, U: ExactSizeIterator, { - Self::new(py, elements) + Self::new(py, elements.into_iter().map(|e| e.to_object(py))).unwrap() } /// Constructs a new empty list. @@ -164,7 +170,7 @@ pub trait PyListMethods<'py>: crate::sealed::Sealed { /// ``` /// use pyo3::{prelude::*, types::PyList}; /// Python::with_gil(|py| { - /// let list = PyList::new(py, [2, 3, 5, 7]); + /// let list = PyList::new(py, [2, 3, 5, 7]).unwrap(); /// let obj = list.get_item(0); /// assert_eq!(obj.unwrap().extract::().unwrap(), 2); /// }); @@ -282,7 +288,7 @@ impl<'py> PyListMethods<'py> for Bound<'py, PyList> { /// ``` /// use pyo3::{prelude::*, types::PyList}; /// Python::with_gil(|py| { - /// let list = PyList::new(py, [2, 3, 5, 7]); + /// let list = PyList::new(py, [2, 3, 5, 7]).unwrap(); /// let obj = list.get_item(0); /// assert_eq!(obj.unwrap().extract::().unwrap(), 2); /// }); @@ -573,7 +579,7 @@ mod tests { #[test] fn test_new() { Python::with_gil(|py| { - let list = PyList::new(py, [2, 3, 5, 7]); + let list = PyList::new(py, [2, 3, 5, 7]).unwrap(); assert_eq!(2, list.get_item(0).unwrap().extract::().unwrap()); assert_eq!(3, list.get_item(1).unwrap().extract::().unwrap()); assert_eq!(5, list.get_item(2).unwrap().extract::().unwrap()); @@ -584,7 +590,7 @@ mod tests { #[test] fn test_len() { Python::with_gil(|py| { - let list = PyList::new(py, [1, 2, 3, 4]); + let list = PyList::new(py, [1, 2, 3, 4]).unwrap(); assert_eq!(4, list.len()); }); } @@ -592,7 +598,7 @@ mod tests { #[test] fn test_get_item() { Python::with_gil(|py| { - let list = PyList::new(py, [2, 3, 5, 7]); + let list = PyList::new(py, [2, 3, 5, 7]).unwrap(); assert_eq!(2, list.get_item(0).unwrap().extract::().unwrap()); assert_eq!(3, list.get_item(1).unwrap().extract::().unwrap()); assert_eq!(5, list.get_item(2).unwrap().extract::().unwrap()); @@ -603,7 +609,7 @@ mod tests { #[test] fn test_get_slice() { Python::with_gil(|py| { - let list = PyList::new(py, [2, 3, 5, 7]); + let list = PyList::new(py, [2, 3, 5, 7]).unwrap(); let slice = list.get_slice(1, 3); assert_eq!(2, slice.len()); let slice = list.get_slice(1, 7); @@ -614,7 +620,7 @@ mod tests { #[test] fn test_set_item() { Python::with_gil(|py| { - let list = PyList::new(py, [2, 3, 5, 7]); + let list = PyList::new(py, [2, 3, 5, 7]).unwrap(); let val = 42i32.into_pyobject(py).unwrap(); let val2 = 42i32.into_pyobject(py).unwrap(); assert_eq!(2, list.get_item(0).unwrap().extract::().unwrap()); @@ -644,7 +650,7 @@ mod tests { #[test] fn test_insert() { Python::with_gil(|py| { - let list = PyList::new(py, [2, 3, 5, 7]); + let list = PyList::new(py, [2, 3, 5, 7]).unwrap(); let val = 42i32.into_pyobject(py).unwrap(); let val2 = 43i32.into_pyobject(py).unwrap(); assert_eq!(4, list.len()); @@ -676,7 +682,7 @@ mod tests { #[test] fn test_append() { Python::with_gil(|py| { - let list = PyList::new(py, [2]); + let list = PyList::new(py, [2]).unwrap(); list.append(3).unwrap(); assert_eq!(2, list.get_item(0).unwrap().extract::().unwrap()); assert_eq!(3, list.get_item(1).unwrap().extract::().unwrap()); @@ -701,7 +707,7 @@ mod tests { fn test_iter() { Python::with_gil(|py| { let v = vec![2, 3, 5, 7]; - let list = PyList::new(py, &v); + let list = PyList::new(py, &v).unwrap(); let mut idx = 0; for el in list { assert_eq!(v[idx], el.extract::().unwrap()); @@ -761,7 +767,7 @@ mod tests { #[test] fn test_into_iter() { Python::with_gil(|py| { - let list = PyList::new(py, [1, 2, 3, 4]); + let list = PyList::new(py, [1, 2, 3, 4]).unwrap(); for (i, item) in list.iter().enumerate() { assert_eq!((i + 1) as i32, item.extract::().unwrap()); } @@ -773,7 +779,7 @@ mod tests { use crate::types::any::PyAnyMethods; Python::with_gil(|py| { - let list = PyList::new(py, [1, 2, 3, 4]); + let list = PyList::new(py, [1, 2, 3, 4]).unwrap(); let mut items = vec![]; for item in &list { items.push(item.extract::().unwrap()); @@ -785,7 +791,7 @@ mod tests { #[test] fn test_as_sequence() { Python::with_gil(|py| { - let list = PyList::new(py, [1, 2, 3, 4]); + let list = PyList::new(py, [1, 2, 3, 4]).unwrap(); assert_eq!(list.as_sequence().len().unwrap(), 4); assert_eq!( @@ -802,7 +808,7 @@ mod tests { #[test] fn test_into_sequence() { Python::with_gil(|py| { - let list = PyList::new(py, [1, 2, 3, 4]); + let list = PyList::new(py, [1, 2, 3, 4]).unwrap(); let sequence = list.into_sequence(); @@ -815,7 +821,7 @@ mod tests { fn test_extract() { Python::with_gil(|py| { let v = vec![2, 3, 5, 7]; - let list = PyList::new(py, &v); + let list = PyList::new(py, &v).unwrap(); let v2 = list.as_ref().extract::>().unwrap(); assert_eq!(v, v2); }); @@ -825,7 +831,7 @@ mod tests { fn test_sort() { Python::with_gil(|py| { let v = vec![7, 3, 2, 5]; - let list = PyList::new(py, &v); + let list = PyList::new(py, &v).unwrap(); assert_eq!(7, list.get_item(0).unwrap().extract::().unwrap()); assert_eq!(3, list.get_item(1).unwrap().extract::().unwrap()); assert_eq!(2, list.get_item(2).unwrap().extract::().unwrap()); @@ -842,7 +848,7 @@ mod tests { fn test_reverse() { Python::with_gil(|py| { let v = vec![2, 3, 5, 7]; - let list = PyList::new(py, &v); + let list = PyList::new(py, &v).unwrap(); assert_eq!(2, list.get_item(0).unwrap().extract::().unwrap()); assert_eq!(3, list.get_item(1).unwrap().extract::().unwrap()); assert_eq!(5, list.get_item(2).unwrap().extract::().unwrap()); @@ -868,7 +874,7 @@ mod tests { #[test] fn test_list_get_item_invalid_index() { Python::with_gil(|py| { - let list = PyList::new(py, [2, 3, 5, 7]); + let list = PyList::new(py, [2, 3, 5, 7]).unwrap(); let obj = list.get_item(5); assert!(obj.is_err()); assert_eq!( @@ -881,7 +887,7 @@ mod tests { #[test] fn test_list_get_item_sanity() { Python::with_gil(|py| { - let list = PyList::new(py, [2, 3, 5, 7]); + let list = PyList::new(py, [2, 3, 5, 7]).unwrap(); let obj = list.get_item(0); assert_eq!(obj.unwrap().extract::().unwrap(), 2); }); @@ -891,7 +897,7 @@ mod tests { #[test] fn test_list_get_item_unchecked_sanity() { Python::with_gil(|py| { - let list = PyList::new(py, [2, 3, 5, 7]); + let list = PyList::new(py, [2, 3, 5, 7]).unwrap(); let obj = unsafe { list.get_item_unchecked(0) }; assert_eq!(obj.extract::().unwrap(), 2); }); @@ -900,7 +906,7 @@ mod tests { #[test] fn test_list_del_item() { Python::with_gil(|py| { - let list = PyList::new(py, [1, 1, 2, 3, 5, 8]); + let list = PyList::new(py, [1, 1, 2, 3, 5, 8]).unwrap(); assert!(list.del_item(10).is_err()); assert_eq!(1, list.get_item(0).unwrap().extract::().unwrap()); assert!(list.del_item(0).is_ok()); @@ -922,8 +928,8 @@ mod tests { #[test] fn test_list_set_slice() { Python::with_gil(|py| { - let list = PyList::new(py, [1, 1, 2, 3, 5, 8]); - let ins = PyList::new(py, [7, 4]); + let list = PyList::new(py, [1, 1, 2, 3, 5, 8]).unwrap(); + let ins = PyList::new(py, [7, 4]).unwrap(); list.set_slice(1, 4, &ins).unwrap(); assert_eq!([1, 7, 4, 5, 8], list.extract::<[i32; 5]>().unwrap()); list.set_slice(3, 100, &PyList::empty(py)).unwrap(); @@ -934,7 +940,7 @@ mod tests { #[test] fn test_list_del_slice() { Python::with_gil(|py| { - let list = PyList::new(py, [1, 1, 2, 3, 5, 8]); + let list = PyList::new(py, [1, 1, 2, 3, 5, 8]).unwrap(); list.del_slice(1, 4).unwrap(); assert_eq!([1, 5, 8], list.extract::<[i32; 3]>().unwrap()); list.del_slice(1, 100).unwrap(); @@ -945,7 +951,7 @@ mod tests { #[test] fn test_list_contains() { Python::with_gil(|py| { - let list = PyList::new(py, [1, 1, 2, 3, 5, 8]); + let list = PyList::new(py, [1, 1, 2, 3, 5, 8]).unwrap(); assert_eq!(6, list.len()); let bad_needle = 7i32.into_pyobject(py).unwrap(); @@ -962,7 +968,7 @@ mod tests { #[test] fn test_list_index() { Python::with_gil(|py| { - let list = PyList::new(py, [1, 1, 2, 3, 5, 8]); + let list = PyList::new(py, [1, 1, 2, 3, 5, 8]).unwrap(); assert_eq!(0, list.index(1i32).unwrap()); assert_eq!(2, list.index(2i32).unwrap()); assert_eq!(3, list.index(3i32).unwrap()); @@ -1000,7 +1006,7 @@ mod tests { fn too_long_iterator() { Python::with_gil(|py| { let iter = FaultyIter(0..usize::MAX, 73); - let _list = PyList::new(py, iter); + let _list = PyList::new(py, iter).unwrap(); }) } @@ -1011,7 +1017,7 @@ mod tests { fn too_short_iterator() { Python::with_gil(|py| { let iter = FaultyIter(0..35, 73); - let _list = PyList::new(py, iter); + let _list = PyList::new(py, iter).unwrap(); }) } @@ -1023,40 +1029,34 @@ mod tests { Python::with_gil(|py| { let iter = FaultyIter(0..0, usize::MAX); - let _list = PyList::new(py, iter); + let _list = PyList::new(py, iter).unwrap(); }) } - #[cfg(feature = "macros")] #[test] - fn bad_clone_mem_leaks() { - use crate::{IntoPy, Py, PyAny, ToPyObject}; + fn bad_intopyobject_doesnt_cause_leaks() { + use crate::types::PyInt; + use std::convert::Infallible; use std::sync::atomic::{AtomicUsize, Ordering::SeqCst}; static NEEDS_DESTRUCTING_COUNT: AtomicUsize = AtomicUsize::new(0); - #[crate::pyclass] - #[pyo3(crate = "crate")] struct Bad(usize); - impl Clone for Bad { - fn clone(&self) -> Self { - // This panic should not lead to a memory leak - assert_ne!(self.0, 42); - NEEDS_DESTRUCTING_COUNT.fetch_add(1, SeqCst); - - Bad(self.0) - } - } - impl Drop for Bad { fn drop(&mut self) { NEEDS_DESTRUCTING_COUNT.fetch_sub(1, SeqCst); } } - impl ToPyObject for Bad { - fn to_object(&self, py: Python<'_>) -> Py { - self.to_owned().into_py(py) + impl<'py> IntoPyObject<'py> for Bad { + type Target = PyInt; + type Output = crate::Bound<'py, Self::Target>; + type Error = Infallible; + + fn into_pyobject(self, py: Python<'py>) -> Result { + // This panic should not lead to a memory leak + assert_ne!(self.0, 42); + self.0.into_pyobject(py) } } @@ -1082,7 +1082,7 @@ mod tests { Python::with_gil(|py| { std::panic::catch_unwind(|| { let iter = FaultyIter(0..50, 50); - let _list = PyList::new(py, iter); + let _list = PyList::new(py, iter).unwrap(); }) .unwrap_err(); }); @@ -1097,9 +1097,9 @@ mod tests { #[test] fn test_list_to_tuple() { Python::with_gil(|py| { - let list = PyList::new(py, vec![1, 2, 3]); + let list = PyList::new(py, vec![1, 2, 3]).unwrap(); let tuple = list.to_tuple(); - let tuple_expected = PyTuple::new(py, vec![1, 2, 3]); + let tuple_expected = PyTuple::new(py, vec![1, 2, 3]).unwrap(); assert!(tuple.eq(tuple_expected).unwrap()); }) } diff --git a/src/types/sequence.rs b/src/types/sequence.rs index 2fca2b18a51..03e20644ee5 100644 --- a/src/types/sequence.rs +++ b/src/types/sequence.rs @@ -746,7 +746,11 @@ mod tests { let v = vec!["foo", "bar"]; let ob = (&v).into_pyobject(py).unwrap(); let seq = ob.downcast::().unwrap(); - assert!(seq.to_list().unwrap().eq(PyList::new(py, &v)).unwrap()); + assert!(seq + .to_list() + .unwrap() + .eq(PyList::new(py, &v).unwrap()) + .unwrap()); }); } @@ -759,7 +763,7 @@ mod tests { assert!(seq .to_list() .unwrap() - .eq(PyList::new(py, ["f", "o", "o"])) + .eq(PyList::new(py, ["f", "o", "o"]).unwrap()) .unwrap()); }); } @@ -773,7 +777,7 @@ mod tests { assert!(seq .to_tuple() .unwrap() - .eq(PyTuple::new(py, ["foo", "bar"])) + .eq(PyTuple::new(py, ["foo", "bar"]).unwrap()) .unwrap()); }); } @@ -784,7 +788,11 @@ mod tests { let v = vec!["foo", "bar"]; let ob = (&v).into_pyobject(py).unwrap(); let seq = ob.downcast::().unwrap(); - assert!(seq.to_tuple().unwrap().eq(PyTuple::new(py, &v)).unwrap()); + assert!(seq + .to_tuple() + .unwrap() + .eq(PyTuple::new(py, &v).unwrap()) + .unwrap()); }); } diff --git a/src/types/tuple.rs b/src/types/tuple.rs index 642a5c2d055..8f26e4d89e6 100644 --- a/src/types/tuple.rs +++ b/src/types/tuple.rs @@ -17,10 +17,10 @@ use crate::{ #[inline] #[track_caller] -fn new_from_iter<'py>( +fn try_new_from_iter<'py>( py: Python<'py>, - elements: &mut dyn ExactSizeIterator, -) -> Bound<'py, PyTuple> { + mut elements: impl ExactSizeIterator>>, +) -> PyResult> { unsafe { // PyTuple_New checks for overflow but has a bad error message, so we check ourselves let len: Py_ssize_t = elements @@ -36,18 +36,18 @@ fn new_from_iter<'py>( let mut counter: Py_ssize_t = 0; - for obj in elements.take(len as usize) { + for obj in (&mut elements).take(len as usize) { #[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))] - ffi::PyTuple_SET_ITEM(ptr, counter, obj.into_ptr()); + ffi::PyTuple_SET_ITEM(ptr, counter, obj?.into_ptr()); #[cfg(any(Py_LIMITED_API, PyPy, GraalPy))] - ffi::PyTuple_SetItem(ptr, counter, obj.into_ptr()); + ffi::PyTuple_SetItem(ptr, counter, obj?.into_ptr()); counter += 1; } assert!(elements.next().is_none(), "Attempted to create PyTuple but `elements` was larger than reported by its `ExactSizeIterator` implementation."); assert_eq!(len, counter, "Attempted to create PyTuple but `elements` was smaller than reported by its `ExactSizeIterator` implementation."); - tup + Ok(tup) } } @@ -76,12 +76,13 @@ impl PyTuple { /// use pyo3::prelude::*; /// use pyo3::types::PyTuple; /// - /// # fn main() { + /// # fn main() -> PyResult<()> { /// Python::with_gil(|py| { /// let elements: Vec = vec![0, 1, 2, 3, 4, 5]; - /// let tuple = PyTuple::new(py, elements); + /// let tuple = PyTuple::new(py, elements)?; /// assert_eq!(format!("{:?}", tuple), "(0, 1, 2, 3, 4, 5)"); - /// }); + /// # Ok(()) + /// }) /// # } /// ``` /// @@ -91,16 +92,21 @@ impl PyTuple { /// All standard library structures implement this trait correctly, if they do, so calling this /// function using [`Vec`]`` or `&[T]` will always succeed. #[track_caller] - pub fn new( - py: Python<'_>, + pub fn new<'py, T, U>( + py: Python<'py>, elements: impl IntoIterator, - ) -> Bound<'_, PyTuple> + ) -> PyResult> where - T: ToPyObject, + T: IntoPyObject<'py>, U: ExactSizeIterator, { - let mut elements = elements.into_iter().map(|e| e.to_object(py)); - new_from_iter(py, &mut elements) + let elements = elements.into_iter().map(|e| { + e.into_pyobject(py) + .map(BoundObject::into_any) + .map(BoundObject::into_bound) + .map_err(Into::into) + }); + try_new_from_iter(py, elements) } /// Deprecated name for [`PyTuple::new`]. @@ -115,7 +121,7 @@ impl PyTuple { T: ToPyObject, U: ExactSizeIterator, { - PyTuple::new(py, elements) + PyTuple::new(py, elements.into_iter().map(|e| e.to_object(py))).unwrap() } /// Constructs an empty tuple (on the Python side, a singleton object). @@ -542,6 +548,19 @@ macro_rules! tuple_conversion ({$length:expr,$(($refN:ident, $n:tt, $T:ident)),+ } } + impl <'a, 'py, $($T),+> IntoPyObject<'py> for &'a ($($T,)+) + where + $(&'a $T: IntoPyObject<'py>,)+ + { + type Target = PyTuple; + type Output = Bound<'py, Self::Target>; + type Error = PyErr; + + fn into_pyobject(self, py: Python<'py>) -> Result { + Ok(array_into_tuple(py, [$(self.$n.into_pyobject(py).map_err(Into::into)?.into_any().unbind()),+]).into_bound(py)) + } + } + impl <$($T: IntoPy),+> IntoPy> for ($($T,)+) { fn into_py(self, py: Python<'_>) -> Py { array_into_tuple(py, [$(self.$n.into_py(py)),+]) @@ -805,7 +824,7 @@ mod tests { #[test] fn test_new() { Python::with_gil(|py| { - let ob = PyTuple::new(py, [1, 2, 3]); + let ob = PyTuple::new(py, [1, 2, 3]).unwrap(); assert_eq!(3, ob.len()); let ob = ob.as_any(); assert_eq!((1, 2, 3), ob.extract().unwrap()); @@ -813,7 +832,7 @@ mod tests { let mut map = HashSet::new(); map.insert(1); map.insert(2); - PyTuple::new(py, map); + PyTuple::new(py, map).unwrap(); }); } @@ -841,7 +860,7 @@ mod tests { #[test] fn test_slice() { Python::with_gil(|py| { - let tup = PyTuple::new(py, [2, 3, 5, 7]); + let tup = PyTuple::new(py, [2, 3, 5, 7]).unwrap(); let slice = tup.get_slice(1, 3); assert_eq!(2, slice.len()); let slice = tup.get_slice(1, 7); @@ -900,7 +919,7 @@ mod tests { #[test] fn test_bound_iter() { Python::with_gil(|py| { - let tuple = PyTuple::new(py, [1, 2, 3]); + let tuple = PyTuple::new(py, [1, 2, 3]).unwrap(); assert_eq!(3, tuple.len()); let mut iter = tuple.iter(); @@ -923,7 +942,7 @@ mod tests { #[test] fn test_bound_iter_rev() { Python::with_gil(|py| { - let tuple = PyTuple::new(py, [1, 2, 3]); + let tuple = PyTuple::new(py, [1, 2, 3]).unwrap(); assert_eq!(3, tuple.len()); let mut iter = tuple.iter().rev(); @@ -1175,37 +1194,31 @@ mod tests { }) } - #[cfg(feature = "macros")] #[test] - fn bad_clone_mem_leaks() { - use crate::{IntoPy, Py, PyAny, ToPyObject}; + fn bad_intopyobject_doesnt_cause_leaks() { + use crate::types::PyInt; + use std::convert::Infallible; use std::sync::atomic::{AtomicUsize, Ordering::SeqCst}; static NEEDS_DESTRUCTING_COUNT: AtomicUsize = AtomicUsize::new(0); - #[crate::pyclass] - #[pyo3(crate = "crate")] struct Bad(usize); - impl Clone for Bad { - fn clone(&self) -> Self { - // This panic should not lead to a memory leak - assert_ne!(self.0, 42); - NEEDS_DESTRUCTING_COUNT.fetch_add(1, SeqCst); - - Bad(self.0) - } - } - impl Drop for Bad { fn drop(&mut self) { NEEDS_DESTRUCTING_COUNT.fetch_sub(1, SeqCst); } } - impl ToPyObject for Bad { - fn to_object(&self, py: Python<'_>) -> Py { - self.to_owned().into_py(py) + impl<'py> IntoPyObject<'py> for Bad { + type Target = PyInt; + type Output = crate::Bound<'py, Self::Target>; + type Error = Infallible; + + fn into_pyobject(self, py: Python<'py>) -> Result { + // This panic should not lead to a memory leak + assert_ne!(self.0, 42); + self.0.into_pyobject(py) } } @@ -1243,37 +1256,31 @@ mod tests { ); } - #[cfg(feature = "macros")] #[test] - fn bad_clone_mem_leaks_2() { - use crate::{IntoPy, Py, PyAny, ToPyObject}; + fn bad_intopyobject_doesnt_cause_leaks_2() { + use crate::types::PyInt; + use std::convert::Infallible; use std::sync::atomic::{AtomicUsize, Ordering::SeqCst}; static NEEDS_DESTRUCTING_COUNT: AtomicUsize = AtomicUsize::new(0); - #[crate::pyclass] - #[pyo3(crate = "crate")] struct Bad(usize); - impl Clone for Bad { - fn clone(&self) -> Self { - // This panic should not lead to a memory leak - assert_ne!(self.0, 3); - NEEDS_DESTRUCTING_COUNT.fetch_add(1, SeqCst); - - Bad(self.0) - } - } - impl Drop for Bad { fn drop(&mut self) { NEEDS_DESTRUCTING_COUNT.fetch_sub(1, SeqCst); } } - impl ToPyObject for Bad { - fn to_object(&self, py: Python<'_>) -> Py { - self.to_owned().into_py(py) + impl<'py> IntoPyObject<'py> for &Bad { + type Target = PyInt; + type Output = crate::Bound<'py, Self::Target>; + type Error = Infallible; + + fn into_pyobject(self, py: Python<'py>) -> Result { + // This panic should not lead to a memory leak + assert_ne!(self.0, 3); + self.0.into_pyobject(py) } } @@ -1281,7 +1288,7 @@ mod tests { NEEDS_DESTRUCTING_COUNT.store(4, SeqCst); Python::with_gil(|py| { std::panic::catch_unwind(|| { - let _tuple: Py = s.to_object(py); + let _tuple = (&s).into_pyobject(py).unwrap(); }) .unwrap_err(); }); @@ -1297,9 +1304,9 @@ mod tests { #[test] fn test_tuple_to_list() { Python::with_gil(|py| { - let tuple = PyTuple::new(py, vec![1, 2, 3]); + let tuple = PyTuple::new(py, vec![1, 2, 3]).unwrap(); let list = tuple.to_list(); - let list_expected = PyList::new(py, vec![1, 2, 3]); + let list_expected = PyList::new(py, vec![1, 2, 3]).unwrap(); assert!(list.eq(list_expected).unwrap()); }) } @@ -1307,7 +1314,7 @@ mod tests { #[test] fn test_tuple_as_sequence() { Python::with_gil(|py| { - let tuple = PyTuple::new(py, vec![1, 2, 3]); + let tuple = PyTuple::new(py, vec![1, 2, 3]).unwrap(); let sequence = tuple.as_sequence(); assert!(tuple.get_item(0).unwrap().eq(1).unwrap()); assert!(sequence.get_item(0).unwrap().eq(1).unwrap()); @@ -1320,7 +1327,7 @@ mod tests { #[test] fn test_tuple_into_sequence() { Python::with_gil(|py| { - let tuple = PyTuple::new(py, vec![1, 2, 3]); + let tuple = PyTuple::new(py, vec![1, 2, 3]).unwrap(); let sequence = tuple.into_sequence(); assert!(sequence.get_item(0).unwrap().eq(1).unwrap()); assert_eq!(sequence.len().unwrap(), 3); @@ -1330,7 +1337,7 @@ mod tests { #[test] fn test_bound_tuple_get_item() { Python::with_gil(|py| { - let tuple = PyTuple::new(py, vec![1, 2, 3, 4]); + let tuple = PyTuple::new(py, vec![1, 2, 3, 4]).unwrap(); assert_eq!(tuple.len(), 4); assert_eq!(tuple.get_item(0).unwrap().extract::().unwrap(), 1); diff --git a/src/types/typeobject.rs b/src/types/typeobject.rs index 8d4e759580c..7a66b7ad0df 100644 --- a/src/types/typeobject.rs +++ b/src/types/typeobject.rs @@ -285,7 +285,8 @@ mod tests { py.get_type::(), py.get_type::() ] - )) + ) + .unwrap()) .unwrap()); }); } @@ -296,7 +297,7 @@ mod tests { assert!(py .get_type::() .bases() - .eq(PyTuple::new(py, [py.get_type::()])) + .eq(PyTuple::new(py, [py.get_type::()]).unwrap()) .unwrap()); }); } diff --git a/tests/test_frompyobject.rs b/tests/test_frompyobject.rs index 96f34ffd5b6..a1b91c25128 100644 --- a/tests/test_frompyobject.rs +++ b/tests/test_frompyobject.rs @@ -168,10 +168,24 @@ pub struct Tuple(String, usize); #[test] fn test_tuple_struct() { Python::with_gil(|py| { - let tup = PyTuple::new(py, &[1.into_py(py), "test".into_py(py)]); + let tup = PyTuple::new( + py, + &[ + 1i32.into_pyobject(py).unwrap().into_any(), + "test".into_pyobject(py).unwrap().into_any(), + ], + ) + .unwrap(); let tup = tup.extract::(); assert!(tup.is_err()); - let tup = PyTuple::new(py, &["test".into_py(py), 1.into_py(py)]); + let tup = PyTuple::new( + py, + &[ + "test".into_pyobject(py).unwrap().into_any(), + 1i32.into_pyobject(py).unwrap().into_any(), + ], + ) + .unwrap(); let tup = tup .extract::() .expect("Failed to extract Tuple from PyTuple"); @@ -333,7 +347,7 @@ pub struct PyBool { #[test] fn test_enum() { Python::with_gil(|py| { - let tup = PyTuple::new(py, &[1.into_py(py), "test".into_py(py)]); + let tup = PyTuple::new(py, &[1i32.into_py(py), "test".into_py(py)]).unwrap(); let f = tup .extract::>() .expect("Failed to extract Foo from tuple"); @@ -599,6 +613,7 @@ pub struct TransparentFromPyWith { fn test_transparent_from_py_with() { Python::with_gil(|py| { let result = PyList::new(py, [1, 2, 3]) + .unwrap() .extract::() .unwrap(); let expected = TransparentFromPyWith { len: 3 }; diff --git a/tests/test_getter_setter.rs b/tests/test_getter_setter.rs index dc67c040fc5..8966471abe2 100644 --- a/tests/test_getter_setter.rs +++ b/tests/test_getter_setter.rs @@ -53,7 +53,7 @@ impl ClassWithProperties { } #[getter] - fn get_data_list<'py>(&self, py: Python<'py>) -> Bound<'py, PyList> { + fn get_data_list<'py>(&self, py: Python<'py>) -> PyResult> { PyList::new(py, [self.num]) } } diff --git a/tests/test_methods.rs b/tests/test_methods.rs index ecf7f64e94c..2a9ffbec788 100644 --- a/tests/test_methods.rs +++ b/tests/test_methods.rs @@ -703,7 +703,7 @@ impl MethodWithLifeTime { for _ in 0..set.len() { items.push(set.pop().unwrap()); } - let list = PyList::new(py, items); + let list = PyList::new(py, items)?; list.sort()?; Ok(list) } diff --git a/tests/test_proto_methods.rs b/tests/test_proto_methods.rs index 3cca16151aa..9fa6a8b888e 100644 --- a/tests/test_proto_methods.rs +++ b/tests/test_proto_methods.rs @@ -851,6 +851,7 @@ struct DefaultedContains; impl DefaultedContains { fn __iter__(&self, py: Python<'_>) -> PyObject { PyList::new(py, ["a", "b", "c"]) + .unwrap() .as_ref() .iter() .unwrap() @@ -865,6 +866,7 @@ struct NoContains; impl NoContains { fn __iter__(&self, py: Python<'_>) -> PyObject { PyList::new(py, ["a", "b", "c"]) + .unwrap() .as_ref() .iter() .unwrap() diff --git a/tests/test_various.rs b/tests/test_various.rs index dc6bbc76dba..27192aba3bb 100644 --- a/tests/test_various.rs +++ b/tests/test_various.rs @@ -91,7 +91,7 @@ fn intopytuple_pyclass() { #[test] fn pytuple_primitive_iter() { Python::with_gil(|py| { - let tup = PyTuple::new(py, [1u32, 2, 3].iter()); + let tup = PyTuple::new(py, [1u32, 2, 3].iter()).unwrap(); py_assert!(py, tup, "tup == (1, 2, 3)"); }); } @@ -106,7 +106,8 @@ fn pytuple_pyclass_iter() { Py::new(py, SimplePyClass {}).unwrap(), ] .iter(), - ); + ) + .unwrap(); py_assert!(py, tup, "type(tup[0]).__name__ == 'SimplePyClass'"); py_assert!(py, tup, "type(tup[0]).__name__ == type(tup[0]).__name__"); py_assert!(py, tup, "tup[0] != tup[1]"); diff --git a/tests/ui/invalid_property_args.stderr b/tests/ui/invalid_property_args.stderr index 70f3dd4e8f8..786533efd53 100644 --- a/tests/ui/invalid_property_args.stderr +++ b/tests/ui/invalid_property_args.stderr @@ -56,13 +56,13 @@ error[E0277]: `PhantomData` cannot be converted to a Python object = note: implement `IntoPyObject` for `&PhantomData` or `IntoPyObject + Clone` for `PhantomData` to define the conversion = help: the following other types implement trait `IntoPyObject<'py>`: &&str - &'a BTreeMap - &'a BTreeSet - &'a Cell - &'a HashMap - &'a HashSet - &'a Option - &'a Py + &'a (T0, T1) + &'a (T0, T1, T2) + &'a (T0, T1, T2, T3) + &'a (T0, T1, T2, T3, T4) + &'a (T0, T1, T2, T3, T4, T5) + &'a (T0, T1, T2, T3, T4, T5, T6) + &'a (T0, T1, T2, T3, T4, T5, T6, T7) and $N others = note: required for `PhantomData` to implement `for<'py> PyO3GetField<'py>` note: required by a bound in `PyClassGetterGenerator::::generate` diff --git a/tests/ui/missing_intopy.stderr b/tests/ui/missing_intopy.stderr index f7dcca419bf..653fb785dfd 100644 --- a/tests/ui/missing_intopy.stderr +++ b/tests/ui/missing_intopy.stderr @@ -9,13 +9,13 @@ error[E0277]: `Blah` cannot be converted to a Python object = note: if you do not own `Blah` you can perform a manual conversion to one of the types in `pyo3::types::*` = help: the following other types implement trait `IntoPyObject<'py>`: &&str - &'a BTreeMap - &'a BTreeSet - &'a Cell - &'a HashMap - &'a HashSet - &'a Option - &'a Py + &'a (T0, T1) + &'a (T0, T1, T2) + &'a (T0, T1, T2, T3) + &'a (T0, T1, T2, T3, T4) + &'a (T0, T1, T2, T3, T4, T5) + &'a (T0, T1, T2, T3, T4, T5, T6) + &'a (T0, T1, T2, T3, T4, T5, T6, T7) and $N others note: required by a bound in `UnknownReturnType::::wrap` --> src/impl_/wrap.rs From 8a6855e1ab20ac5a30525485a4615d18181a267d Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Mon, 30 Sep 2024 21:00:38 +0100 Subject: [PATCH 290/495] drop support for PyPy 3.7 and 3.8 (#4582) * drop support for PyPy 3.7 and 3.8 * newsfragment --- .github/workflows/build.yml | 14 +++---- .github/workflows/ci.yml | 2 - README.md | 9 +++-- guide/src/building-and-distribution.md | 4 +- newsfragments/4582.packaging.md | 1 + noxfile.py | 6 +-- pyo3-build-config/src/impl_.rs | 56 +++++++++++++------------- pyo3-ffi/ACKNOWLEDGEMENTS | 2 +- pyo3-ffi/README.md | 9 +++-- pyo3-ffi/build.rs | 4 +- pyo3-ffi/src/cpython/object.rs | 9 ----- pyo3-ffi/src/datetime.rs | 5 --- pyo3-ffi/src/lib.rs | 7 ++-- pyo3-ffi/src/pystate.rs | 4 -- src/impl_/pymodule.rs | 16 -------- src/lib.rs | 9 +++-- src/types/function.rs | 4 +- 17 files changed, 67 insertions(+), 94 deletions(-) create mode 100644 newsfragments/4582.packaging.md diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 077ca08cf4e..ed24957ad2a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -22,7 +22,7 @@ on: jobs: build: - continue-on-error: ${{ endsWith(inputs.python-version, '-dev') || contains(fromJSON('["3.7", "pypy3.7"]'), inputs.python-version) || inputs.rust == 'beta' || inputs.rust == 'nightly' }} + continue-on-error: ${{ endsWith(inputs.python-version, '-dev') || contains(fromJSON('["3.7", "3.8"]'), inputs.python-version) || contains(fromJSON('["beta", "nightly"]'), inputs.rust) }} runs-on: ${{ inputs.os }} if: ${{ !(startsWith(inputs.python-version, 'graalpy') && startsWith(inputs.os, 'windows')) }} steps: @@ -95,8 +95,8 @@ jobs: run: cargo build --lib --tests --no-default-features --features "multiple-pymethods full $MAYBE_NIGHTLY" - if: ${{ startsWith(inputs.python-version, 'pypy') }} - name: Build PyPy (abi3-py37) - run: cargo build --lib --tests --no-default-features --features "multiple-pymethods abi3-py37 full $MAYBE_NIGHTLY" + name: Build PyPy (abi3-py39) + run: cargo build --lib --tests --no-default-features --features "multiple-pymethods abi3-py39 full $MAYBE_NIGHTLY" # Run tests (except on PyPy, because no embedding API). - if: ${{ !startsWith(inputs.python-version, 'pypy') && !startsWith(inputs.python-version, 'graalpy') }} @@ -131,8 +131,7 @@ jobs: CARGO_TARGET_DIR: ${{ github.workspace }}/target - uses: dorny/paths-filter@v3 - # pypy 3.7 and 3.8 are not PEP 3123 compliant so fail checks here - if: ${{ inputs.rust == 'stable' && inputs.python-version != 'pypy3.7' && inputs.python-version != 'pypy3.8' && !startsWith(inputs.python-version, 'graalpy') }} + if: ${{ inputs.rust == 'stable' && !startsWith(inputs.python-version, 'graalpy') }} id: ffi-changes with: base: ${{ github.event.pull_request.base.ref || github.event.merge_group.base_ref }} @@ -145,9 +144,8 @@ jobs: - '.github/workflows/build.yml' - name: Run pyo3-ffi-check - # pypy 3.7 and 3.8 are not PEP 3123 compliant so fail checks here, nor - # is pypy 3.9 on windows - if: ${{ endsWith(inputs.python-version, '-dev') || (steps.ffi-changes.outputs.changed == 'true' && inputs.rust == 'stable' && inputs.python-version != 'pypy3.7' && inputs.python-version != 'pypy3.8' && !startsWith(inputs.python-version, 'graalpy') && !(inputs.python-version == 'pypy3.9' && contains(inputs.os, 'windows'))) }} + # pypy 3.9 on windows is not PEP 3123 compliant, nor is graalpy + if: ${{ endsWith(inputs.python-version, '-dev') || (steps.ffi-changes.outputs.changed == 'true' && inputs.rust == 'stable' && !startsWith(inputs.python-version, 'graalpy') && !(inputs.python-version == 'pypy3.9' && contains(inputs.os, 'windows'))) }} run: nox -s ffi-check env: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9ceb4dd64c7..3e085f9dbab 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -242,8 +242,6 @@ jobs: "3.11", "3.12", "3.13-dev", - "pypy3.7", - "pypy3.8", "pypy3.9", "pypy3.10", "graalpy24.0", diff --git a/README.md b/README.md index 28f9c0af0b6..94b7fa49f70 100644 --- a/README.md +++ b/README.md @@ -16,9 +16,12 @@ ## Usage -PyO3 supports the following software versions: - - Python 3.7 and up (CPython, PyPy, and GraalPy) - - Rust 1.63 and up +Requires Rust 1.63 or greater. + +PyO3 supports the following Python distributions: + - CPython 3.7 or greater + - PyPy 7.3 (Python 3.9+) + - GraalPy 24.0 or greater (Python 3.10+) You can use PyO3 to write a native Python module in Rust, or to embed Python in a Rust binary. The following sections explain each of these in turn. diff --git a/guide/src/building-and-distribution.md b/guide/src/building-and-distribution.md index 699f6561828..1a806304d22 100644 --- a/guide/src/building-and-distribution.md +++ b/guide/src/building-and-distribution.md @@ -105,9 +105,9 @@ Rather than using just the `.so` or `.pyd` extension suggested above (depending # CPython 3.10 on macOS .cpython-310-darwin.so -# PyPy 7.3 (Python 3.8) on Linux +# PyPy 7.3 (Python 3.9) on Linux $ python -c 'import sysconfig; print(sysconfig.get_config_var("EXT_SUFFIX"))' -.pypy38-pp73-x86_64-linux-gnu.so +.pypy39-pp73-x86_64-linux-gnu.so ``` So, for example, a valid module library name on CPython 3.10 for macOS is `your_module.cpython-310-darwin.so`, and its equivalent when compiled for PyPy 7.3 on Linux would be `your_module.pypy38-pp73-x86_64-linux-gnu.so`. diff --git a/newsfragments/4582.packaging.md b/newsfragments/4582.packaging.md new file mode 100644 index 00000000000..524ee02e017 --- /dev/null +++ b/newsfragments/4582.packaging.md @@ -0,0 +1 @@ +Drop support for PyPy 3.7 and 3.8. diff --git a/noxfile.py b/noxfile.py index 7f012ce2fc5..b526c71f2f3 100644 --- a/noxfile.py +++ b/noxfile.py @@ -31,7 +31,7 @@ PYO3_GUIDE_TARGET = PYO3_TARGET / "guide" PYO3_DOCS_TARGET = PYO3_TARGET / "doc" PY_VERSIONS = ("3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13") -PYPY_VERSIONS = ("3.7", "3.8", "3.9", "3.10") +PYPY_VERSIONS = ("3.9", "3.10") @nox.session(venv_backend="none") @@ -646,8 +646,8 @@ def test_version_limits(session: nox.Session): env["PYO3_USE_ABI3_FORWARD_COMPATIBILITY"] = "1" _run_cargo(session, "check", env=env) - assert "3.6" not in PYPY_VERSIONS - config_file.set("PyPy", "3.6") + assert "3.8" not in PYPY_VERSIONS + config_file.set("PyPy", "3.8") _run_cargo(session, "check", env=env, expect_error=True) assert "3.11" not in PYPY_VERSIONS diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index a7ae4254bbe..571f9cb5a0c 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -1648,16 +1648,11 @@ fn default_lib_name_unix( } } }, - PythonImplementation::PyPy => { - if version >= (PythonVersion { major: 3, minor: 9 }) { - match ld_version { - Some(ld_version) => format!("pypy{}-c", ld_version), - None => format!("pypy{}.{}-c", version.major, version.minor), - } - } else { - format!("pypy{}-c", version.major) - } - } + PythonImplementation::PyPy => match ld_version { + Some(ld_version) => format!("pypy{}-c", ld_version), + None => format!("pypy{}.{}-c", version.major, version.minor), + }, + PythonImplementation::GraalPy => "python-native".to_string(), } } @@ -2348,17 +2343,17 @@ mod tests { use PythonImplementation::*; assert_eq!( super::default_lib_name_windows( - PythonVersion { major: 3, minor: 7 }, + PythonVersion { major: 3, minor: 9 }, CPython, false, false, false, ), - "python37", + "python39", ); assert_eq!( super::default_lib_name_windows( - PythonVersion { major: 3, minor: 7 }, + PythonVersion { major: 3, minor: 9 }, CPython, true, false, @@ -2368,17 +2363,17 @@ mod tests { ); assert_eq!( super::default_lib_name_windows( - PythonVersion { major: 3, minor: 7 }, + PythonVersion { major: 3, minor: 9 }, CPython, false, true, false, ), - "python3.7", + "python3.9", ); assert_eq!( super::default_lib_name_windows( - PythonVersion { major: 3, minor: 7 }, + PythonVersion { major: 3, minor: 9 }, CPython, true, true, @@ -2388,35 +2383,35 @@ mod tests { ); assert_eq!( super::default_lib_name_windows( - PythonVersion { major: 3, minor: 7 }, + PythonVersion { major: 3, minor: 9 }, PyPy, true, false, false, ), - "python37", + "python39", ); assert_eq!( super::default_lib_name_windows( - PythonVersion { major: 3, minor: 7 }, + PythonVersion { major: 3, minor: 9 }, CPython, false, false, true, ), - "python37_d", + "python39_d", ); // abi3 debug builds on windows use version-specific lib // to workaround https://github.com/python/cpython/issues/101614 assert_eq!( super::default_lib_name_windows( - PythonVersion { major: 3, minor: 7 }, + PythonVersion { major: 3, minor: 9 }, CPython, true, false, true, ), - "python37_d", + "python39_d", ); } @@ -2447,13 +2442,12 @@ mod tests { "python3.7md", ); - // PyPy 3.7 ignores ldversion + // PyPy 3.9 includes ldversion assert_eq!( - super::default_lib_name_unix(PythonVersion { major: 3, minor: 7 }, PyPy, Some("3.7md")), - "pypy3-c", + super::default_lib_name_unix(PythonVersion { major: 3, minor: 9 }, PyPy, None), + "pypy3.9-c", ); - // PyPy 3.9 includes ldversion assert_eq!( super::default_lib_name_unix(PythonVersion { major: 3, minor: 9 }, PyPy, Some("3.9d")), "pypy3.9d-c", @@ -2692,7 +2686,7 @@ mod tests { fn test_build_script_outputs_base() { let interpreter_config = InterpreterConfig { implementation: PythonImplementation::CPython, - version: PythonVersion { major: 3, minor: 8 }, + version: PythonVersion { major: 3, minor: 9 }, shared: true, abi3: false, lib_name: Some("python3".into()), @@ -2708,6 +2702,7 @@ mod tests { [ "cargo:rustc-cfg=Py_3_7".to_owned(), "cargo:rustc-cfg=Py_3_8".to_owned(), + "cargo:rustc-cfg=Py_3_9".to_owned(), ] ); @@ -2720,6 +2715,7 @@ mod tests { [ "cargo:rustc-cfg=Py_3_7".to_owned(), "cargo:rustc-cfg=Py_3_8".to_owned(), + "cargo:rustc-cfg=Py_3_9".to_owned(), "cargo:rustc-cfg=PyPy".to_owned(), ] ); @@ -2729,7 +2725,7 @@ mod tests { fn test_build_script_outputs_abi3() { let interpreter_config = InterpreterConfig { implementation: PythonImplementation::CPython, - version: PythonVersion { major: 3, minor: 7 }, + version: PythonVersion { major: 3, minor: 9 }, shared: true, abi3: true, lib_name: Some("python3".into()), @@ -2745,6 +2741,8 @@ mod tests { interpreter_config.build_script_outputs(), [ "cargo:rustc-cfg=Py_3_7".to_owned(), + "cargo:rustc-cfg=Py_3_8".to_owned(), + "cargo:rustc-cfg=Py_3_9".to_owned(), "cargo:rustc-cfg=Py_LIMITED_API".to_owned(), ] ); @@ -2757,6 +2755,8 @@ mod tests { interpreter_config.build_script_outputs(), [ "cargo:rustc-cfg=Py_3_7".to_owned(), + "cargo:rustc-cfg=Py_3_8".to_owned(), + "cargo:rustc-cfg=Py_3_9".to_owned(), "cargo:rustc-cfg=PyPy".to_owned(), "cargo:rustc-cfg=Py_LIMITED_API".to_owned(), ] diff --git a/pyo3-ffi/ACKNOWLEDGEMENTS b/pyo3-ffi/ACKNOWLEDGEMENTS index 4502d7774e0..8b20727dece 100644 --- a/pyo3-ffi/ACKNOWLEDGEMENTS +++ b/pyo3-ffi/ACKNOWLEDGEMENTS @@ -3,4 +3,4 @@ for binary compatibility, with additional metadata to support PyPy. For original implementations please see: - https://github.com/python/cpython - - https://foss.heptapod.net/pypy/pypy + - https://github.com/pypy/pypy diff --git a/pyo3-ffi/README.md b/pyo3-ffi/README.md index 75a34b6e72a..c5acc96ed3b 100644 --- a/pyo3-ffi/README.md +++ b/pyo3-ffi/README.md @@ -12,9 +12,12 @@ Manual][capi] for up-to-date documentation. # Minimum supported Rust and Python versions -PyO3 supports the following software versions: - - Python 3.7 and up (CPython and PyPy) - - Rust 1.63 and up +Requires Rust 1.63 or greater. + +`pyo3-ffi` supports the following Python distributions: + - CPython 3.7 or greater + - PyPy 7.3 (Python 3.9+) + - GraalPy 24.0 or greater (Python 3.10+) # Example: Building Python Native modules diff --git a/pyo3-ffi/build.rs b/pyo3-ffi/build.rs index b0f1c28d448..622c2707110 100644 --- a/pyo3-ffi/build.rs +++ b/pyo3-ffi/build.rs @@ -23,7 +23,7 @@ const SUPPORTED_VERSIONS_CPYTHON: SupportedVersions = SupportedVersions { }; const SUPPORTED_VERSIONS_PYPY: SupportedVersions = SupportedVersions { - min: PythonVersion { major: 3, minor: 7 }, + min: PythonVersion { major: 3, minor: 9 }, max: PythonVersion { major: 3, minor: 10, @@ -110,7 +110,7 @@ fn ensure_python_version(interpreter_config: &InterpreterConfig) -> Result<()> { PythonImplementation::CPython => {} PythonImplementation::PyPy => 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." + See https://github.com/pypy/pypy/issues/3397 for more information." ), PythonImplementation::GraalPy => warn!( "GraalPy does not support abi3 so the build artifacts will be version-specific." diff --git a/pyo3-ffi/src/cpython/object.rs b/pyo3-ffi/src/cpython/object.rs index 23d7f94081e..35ddf25a2de 100644 --- a/pyo3-ffi/src/cpython/object.rs +++ b/pyo3-ffi/src/cpython/object.rs @@ -210,15 +210,6 @@ pub type printfunc = #[repr(C)] #[derive(Debug)] pub struct PyTypeObject { - #[cfg(all(PyPy, not(Py_3_9)))] - pub ob_refcnt: Py_ssize_t, - #[cfg(all(PyPy, not(Py_3_9)))] - pub ob_pypy_link: Py_ssize_t, - #[cfg(all(PyPy, not(Py_3_9)))] - pub ob_type: *mut PyTypeObject, - #[cfg(all(PyPy, not(Py_3_9)))] - pub ob_size: Py_ssize_t, - #[cfg(not(all(PyPy, not(Py_3_9))))] pub ob_base: object::PyVarObject, #[cfg(GraalPy)] pub ob_size: Py_ssize_t, diff --git a/pyo3-ffi/src/datetime.rs b/pyo3-ffi/src/datetime.rs index bbee057d561..7283b6d4e52 100644 --- a/pyo3-ffi/src/datetime.rs +++ b/pyo3-ffi/src/datetime.rs @@ -3,11 +3,6 @@ //! This is the unsafe thin wrapper around the [CPython C API](https://docs.python.org/3/c-api/datetime.html), //! and covers the various date and time related objects in the Python `datetime` //! standard library module. -//! -//! A note regarding PyPy (cpyext) support: -//! -//! Support for `PyDateTime_CAPI` is limited as of PyPy 7.0.0. -//! `DateTime_FromTimestamp` and `Date_FromTimestamp` are currently not supported. #[cfg(GraalPy)] use crate::{PyLong_AsLong, PyLong_Check, PyObject_GetAttrString, Py_DecRef}; diff --git a/pyo3-ffi/src/lib.rs b/pyo3-ffi/src/lib.rs index 1e3f804b1f4..c6157401124 100644 --- a/pyo3-ffi/src/lib.rs +++ b/pyo3-ffi/src/lib.rs @@ -50,9 +50,10 @@ //! //! # Minimum supported Rust and Python versions //! -//! PyO3 supports the following software versions: -//! - Python 3.7 and up (CPython and PyPy) -//! - Rust 1.63 and up +//! `pyo3-ffi` supports the following Python distributions: +//! - CPython 3.7 or greater +//! - PyPy 7.3 (Python 3.9+) +//! - GraalPy 24.0 or greater (Python 3.10+) //! //! # Example: Building Python Native modules //! diff --git a/pyo3-ffi/src/pystate.rs b/pyo3-ffi/src/pystate.rs index d2fd39e497d..23aeea3a1de 100644 --- a/pyo3-ffi/src/pystate.rs +++ b/pyo3-ffi/src/pystate.rs @@ -1,4 +1,3 @@ -#[cfg(any(not(PyPy), Py_3_9))] use crate::moduleobject::PyModuleDef; use crate::object::PyObject; use std::os::raw::c_int; @@ -28,15 +27,12 @@ extern "C" { #[cfg(not(PyPy))] pub fn PyInterpreterState_GetID(arg1: *mut PyInterpreterState) -> i64; - #[cfg(any(not(PyPy), Py_3_9))] // only on PyPy since 3.9 #[cfg_attr(PyPy, link_name = "PyPyState_AddModule")] pub fn PyState_AddModule(arg1: *mut PyObject, arg2: *mut PyModuleDef) -> c_int; - #[cfg(any(not(PyPy), Py_3_9))] // only on PyPy since 3.9 #[cfg_attr(PyPy, link_name = "PyPyState_RemoveModule")] pub fn PyState_RemoveModule(arg1: *mut PyModuleDef) -> c_int; - #[cfg(any(not(PyPy), Py_3_9))] // only on PyPy since 3.9 // only has PyPy prefix since 3.10 #[cfg_attr(all(PyPy, Py_3_10), link_name = "PyPyState_FindModule")] pub fn PyState_FindModule(arg1: *mut PyModuleDef) -> *mut PyObject; diff --git a/src/impl_/pymodule.rs b/src/impl_/pymodule.rs index ab38bc49e8e..97eb2103dfe 100644 --- a/src/impl_/pymodule.rs +++ b/src/impl_/pymodule.rs @@ -89,22 +89,6 @@ impl ModuleDef { } /// Builds a module using user given initializer. Used for [`#[pymodule]`][crate::pymodule]. pub fn make_module(&'static self, py: Python<'_>) -> PyResult> { - #[cfg(all(PyPy, not(Py_3_8)))] - { - use crate::types::any::PyAnyMethods; - const PYPY_GOOD_VERSION: [u8; 3] = [7, 3, 8]; - let version = py - .import("sys")? - .getattr("implementation")? - .getattr("version")?; - if version.lt(crate::types::PyTuple::new(py, PYPY_GOOD_VERSION)?)? { - let warn = py.import("warnings")?.getattr("warn")?; - warn.call1(( - "PyPy 3.7 versions older than 7.3.8 are known to have binary \ - compatibility issues which may cause segfaults. Please upgrade.", - ))?; - } - } // Check the interpreter ID has not changed, since we currently have no way to guarantee // that static data is not reused across interpreters. // diff --git a/src/lib.rs b/src/lib.rs index 7f1329c9ea5..8181afb4347 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -131,9 +131,12 @@ //! //! # Minimum supported Rust and Python versions //! -//! PyO3 supports the following software versions: -//! - Python 3.7 and up (CPython and PyPy) -//! - Rust 1.63 and up +//! Requires Rust 1.63 or greater. +//! +//! PyO3 supports the following Python distributions: +//! - CPython 3.7 or greater +//! - PyPy 7.3 (Python 3.9+) +//! - GraalPy 24.0 or greater (Python 3.10+) //! //! # Example: Building a native Python module //! diff --git a/src/types/function.rs b/src/types/function.rs index 8d226a9e792..936176add22 100644 --- a/src/types/function.rs +++ b/src/types/function.rs @@ -222,8 +222,8 @@ unsafe impl Send for ClosureDestructor {} /// Values of this type are accessed via PyO3's smart pointers, e.g. as /// [`Py`][crate::Py] or [`Bound<'py, PyFunction>`][Bound]. #[repr(transparent)] -#[cfg(all(not(Py_LIMITED_API), not(all(PyPy, not(Py_3_8)))))] +#[cfg(not(Py_LIMITED_API))] pub struct PyFunction(PyAny); -#[cfg(all(not(Py_LIMITED_API), not(all(PyPy, not(Py_3_8)))))] +#[cfg(not(Py_LIMITED_API))] pyobject_native_type_core!(PyFunction, pyobject_native_static_type_object!(ffi::PyFunction_Type), #checkfunction=ffi::PyFunction_Check); From 116170a7507fb8fc31ff9072bfeb6ab37a6b4926 Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Mon, 30 Sep 2024 22:18:24 +0200 Subject: [PATCH 291/495] migrate `PySet` to `IntoPyObject` (#4536) * migrate `PySetMethods` trait bounds * migrate constructor trait bounds --- src/conversions/hashbrown.rs | 24 ++------ src/conversions/std/set.rs | 50 +++------------- src/instance.rs | 10 ++++ src/types/frozenset.rs | 98 +++++++++++++++++------------- src/types/set.rs | 113 ++++++++++++++++++++++------------- 5 files changed, 150 insertions(+), 145 deletions(-) diff --git a/src/conversions/hashbrown.rs b/src/conversions/hashbrown.rs index e6ebb35232e..a3fd61a6412 100644 --- a/src/conversions/hashbrown.rs +++ b/src/conversions/hashbrown.rs @@ -147,15 +147,7 @@ where type Error = PyErr; fn into_pyobject(self, py: Python<'py>) -> Result { - try_new_from_iter( - py, - self.into_iter().map(|item| { - item.into_pyobject(py) - .map(BoundObject::into_any) - .map(BoundObject::unbind) - .map_err(Into::into) - }), - ) + try_new_from_iter(py, self) } } @@ -169,15 +161,7 @@ where type Error = PyErr; fn into_pyobject(self, py: Python<'py>) -> Result { - try_new_from_iter( - py, - self.into_iter().map(|item| { - item.into_pyobject(py) - .map(BoundObject::into_any) - .map(BoundObject::unbind) - .map_err(Into::into) - }), - ) + try_new_from_iter(py, self) } } @@ -272,11 +256,11 @@ mod tests { #[test] fn test_extract_hashbrown_hashset() { Python::with_gil(|py| { - let set = PySet::new(py, &[1, 2, 3, 4, 5]).unwrap(); + let set = PySet::new(py, [1, 2, 3, 4, 5]).unwrap(); let hash_set: hashbrown::HashSet = set.extract().unwrap(); assert_eq!(hash_set, [1, 2, 3, 4, 5].iter().copied().collect()); - let set = PyFrozenSet::new(py, &[1, 2, 3, 4, 5]).unwrap(); + let set = PyFrozenSet::new(py, [1, 2, 3, 4, 5]).unwrap(); let hash_set: hashbrown::HashSet = set.extract().unwrap(); assert_eq!(hash_set, [1, 2, 3, 4, 5].iter().copied().collect()); }); diff --git a/src/conversions/std/set.rs b/src/conversions/std/set.rs index c9738069080..cc706c43f01 100644 --- a/src/conversions/std/set.rs +++ b/src/conversions/std/set.rs @@ -11,7 +11,7 @@ use crate::{ set::{new_from_iter, try_new_from_iter, PySetMethods}, PyFrozenSet, PySet, }, - BoundObject, FromPyObject, IntoPy, PyAny, PyErr, PyObject, PyResult, Python, ToPyObject, + FromPyObject, IntoPy, PyAny, PyErr, PyObject, PyResult, Python, ToPyObject, }; impl ToPyObject for collections::HashSet @@ -64,15 +64,7 @@ where type Error = PyErr; fn into_pyobject(self, py: Python<'py>) -> Result { - try_new_from_iter( - py, - self.into_iter().map(|item| { - item.into_pyobject(py) - .map(BoundObject::into_any) - .map(BoundObject::unbind) - .map_err(Into::into) - }), - ) + try_new_from_iter(py, self) } } @@ -86,15 +78,7 @@ where type Error = PyErr; fn into_pyobject(self, py: Python<'py>) -> Result { - try_new_from_iter( - py, - self.iter().map(|item| { - item.into_pyobject(py) - .map(BoundObject::into_any) - .map(BoundObject::unbind) - .map_err(Into::into) - }), - ) + try_new_from_iter(py, self.iter()) } } @@ -147,15 +131,7 @@ where type Error = PyErr; fn into_pyobject(self, py: Python<'py>) -> Result { - try_new_from_iter( - py, - self.into_iter().map(|item| { - item.into_pyobject(py) - .map(BoundObject::into_any) - .map(BoundObject::unbind) - .map_err(Into::into) - }), - ) + try_new_from_iter(py, self) } } @@ -168,15 +144,7 @@ where type Error = PyErr; fn into_pyobject(self, py: Python<'py>) -> Result { - try_new_from_iter( - py, - self.iter().map(|item| { - item.into_pyobject(py) - .map(BoundObject::into_any) - .map(BoundObject::unbind) - .map_err(Into::into) - }), - ) + try_new_from_iter(py, self.iter()) } } @@ -212,11 +180,11 @@ mod tests { #[test] fn test_extract_hashset() { Python::with_gil(|py| { - let set = PySet::new(py, &[1, 2, 3, 4, 5]).unwrap(); + let set = PySet::new(py, [1, 2, 3, 4, 5]).unwrap(); let hash_set: HashSet = set.extract().unwrap(); assert_eq!(hash_set, [1, 2, 3, 4, 5].iter().copied().collect()); - let set = PyFrozenSet::new(py, &[1, 2, 3, 4, 5]).unwrap(); + let set = PyFrozenSet::new(py, [1, 2, 3, 4, 5]).unwrap(); let hash_set: HashSet = set.extract().unwrap(); assert_eq!(hash_set, [1, 2, 3, 4, 5].iter().copied().collect()); }); @@ -225,11 +193,11 @@ mod tests { #[test] fn test_extract_btreeset() { Python::with_gil(|py| { - let set = PySet::new(py, &[1, 2, 3, 4, 5]).unwrap(); + let set = PySet::new(py, [1, 2, 3, 4, 5]).unwrap(); let hash_set: BTreeSet = set.extract().unwrap(); assert_eq!(hash_set, [1, 2, 3, 4, 5].iter().copied().collect()); - let set = PyFrozenSet::new(py, &[1, 2, 3, 4, 5]).unwrap(); + let set = PyFrozenSet::new(py, [1, 2, 3, 4, 5]).unwrap(); let hash_set: BTreeSet = set.extract().unwrap(); assert_eq!(hash_set, [1, 2, 3, 4, 5].iter().copied().collect()); }); diff --git a/src/instance.rs b/src/instance.rs index 1ff18f82466..2b19fd10c43 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -30,6 +30,8 @@ pub trait BoundObject<'py, T>: bound_object_sealed::Sealed { fn into_any(self) -> Self::Any; /// Turn this smart pointer into a strong reference pointer fn into_ptr(self) -> *mut ffi::PyObject; + /// Turn this smart pointer into a borrowed reference pointer + fn as_ptr(&self) -> *mut ffi::PyObject; /// Turn this smart pointer into an owned [`Py`] fn unbind(self) -> Py; } @@ -616,6 +618,10 @@ impl<'py, T> BoundObject<'py, T> for Bound<'py, T> { self.into_ptr() } + fn as_ptr(&self) -> *mut ffi::PyObject { + self.as_ptr() + } + fn unbind(self) -> Py { self.unbind() } @@ -835,6 +841,10 @@ impl<'a, 'py, T> BoundObject<'py, T> for Borrowed<'a, 'py, T> { (*self).to_owned().into_ptr() } + fn as_ptr(&self) -> *mut ffi::PyObject { + (*self).as_ptr() + } + fn unbind(self) -> Py { (*self).to_owned().unbind() } diff --git a/src/types/frozenset.rs b/src/types/frozenset.rs index 2e41864872d..88d726b136d 100644 --- a/src/types/frozenset.rs +++ b/src/types/frozenset.rs @@ -1,3 +1,4 @@ +use crate::conversion::IntoPyObject; use crate::types::PyIterator; use crate::{ err::{self, PyErr, PyResult}, @@ -5,8 +6,9 @@ use crate::{ ffi_ptr_ext::FfiPtrExt, py_result_ext::PyResultExt, types::any::PyAnyMethods, - Bound, PyAny, PyObject, Python, ToPyObject, + Bound, PyAny, Python, ToPyObject, }; +use crate::{Borrowed, BoundObject}; use std::ptr; /// Allows building a Python `frozenset` one item at a time @@ -27,15 +29,21 @@ impl<'py> PyFrozenSetBuilder<'py> { /// Adds an element to the set. pub fn add(&mut self, key: K) -> PyResult<()> where - K: ToPyObject, + K: IntoPyObject<'py>, { - fn inner(frozenset: &Bound<'_, PyFrozenSet>, key: PyObject) -> PyResult<()> { + fn inner(frozenset: &Bound<'_, PyFrozenSet>, key: Borrowed<'_, '_, PyAny>) -> PyResult<()> { err::error_on_minusone(frozenset.py(), unsafe { ffi::PySet_Add(frozenset.as_ptr(), key.as_ptr()) }) } - inner(&self.py_frozen_set, key.to_object(self.py_frozen_set.py())) + inner( + &self.py_frozen_set, + key.into_pyobject(self.py_frozen_set.py()) + .map_err(Into::into)? + .into_any() + .as_borrowed(), + ) } /// Finish building the set and take ownership of its current value @@ -83,11 +91,14 @@ impl PyFrozenSet { /// /// May panic when running out of memory. #[inline] - pub fn new<'a, 'p, T: ToPyObject + 'a>( - py: Python<'p>, - elements: impl IntoIterator, - ) -> PyResult> { - new_from_iter(py, elements) + pub fn new<'py, T>( + py: Python<'py>, + elements: impl IntoIterator, + ) -> PyResult> + where + T: IntoPyObject<'py>, + { + try_new_from_iter(py, elements) } /// Deprecated name for [`PyFrozenSet::new`]. @@ -97,7 +108,7 @@ impl PyFrozenSet { py: Python<'p>, elements: impl IntoIterator, ) -> PyResult> { - Self::new(py, elements) + Self::new(py, elements.into_iter().map(|e| e.to_object(py))) } /// Creates a new empty frozen set @@ -139,7 +150,7 @@ pub trait PyFrozenSetMethods<'py>: crate::sealed::Sealed { /// This is equivalent to the Python expression `key in self`. fn contains(&self, key: K) -> PyResult where - K: ToPyObject; + K: IntoPyObject<'py>; /// Returns an iterator of values in this set. fn iter(&self) -> BoundFrozenSetIterator<'py>; @@ -153,9 +164,12 @@ impl<'py> PyFrozenSetMethods<'py> for Bound<'py, PyFrozenSet> { fn contains(&self, key: K) -> PyResult where - K: ToPyObject, + K: IntoPyObject<'py>, { - fn inner(frozenset: &Bound<'_, PyFrozenSet>, key: Bound<'_, PyAny>) -> PyResult { + fn inner( + frozenset: &Bound<'_, PyFrozenSet>, + key: Borrowed<'_, '_, PyAny>, + ) -> PyResult { match unsafe { ffi::PySet_Contains(frozenset.as_ptr(), key.as_ptr()) } { 1 => Ok(true), 0 => Ok(false), @@ -164,7 +178,13 @@ impl<'py> PyFrozenSetMethods<'py> for Bound<'py, PyFrozenSet> { } let py = self.py(); - inner(self, key.to_object(py).into_bound(py)) + inner( + self, + key.into_pyobject(py) + .map_err(Into::into)? + .into_any() + .as_borrowed(), + ) } fn iter(&self) -> BoundFrozenSetIterator<'py> { @@ -229,31 +249,27 @@ impl<'py> ExactSizeIterator for BoundFrozenSetIterator<'py> { } #[inline] -pub(crate) fn new_from_iter( - py: Python<'_>, +pub(crate) fn try_new_from_iter<'py, T>( + py: Python<'py>, elements: impl IntoIterator, -) -> PyResult> { - fn inner<'py>( - py: Python<'py>, - elements: &mut dyn Iterator, - ) -> PyResult> { - let set = unsafe { - // We create the `Py` pointer because its Drop cleans up the set if user code panics. - ffi::PyFrozenSet_New(std::ptr::null_mut()) - .assume_owned_or_err(py)? - .downcast_into_unchecked() - }; - let ptr = set.as_ptr(); - - for obj in elements { - err::error_on_minusone(py, unsafe { ffi::PySet_Add(ptr, obj.as_ptr()) })?; - } - - Ok(set) +) -> PyResult> +where + T: IntoPyObject<'py>, +{ + let set = unsafe { + // We create the `Py` pointer because its Drop cleans up the set if user code panics. + ffi::PyFrozenSet_New(std::ptr::null_mut()) + .assume_owned_or_err(py)? + .downcast_into_unchecked() + }; + let ptr = set.as_ptr(); + + for e in elements { + let obj = e.into_pyobject(py).map_err(Into::into)?; + err::error_on_minusone(py, unsafe { ffi::PySet_Add(ptr, obj.as_ptr()) })?; } - let mut iter = elements.into_iter().map(|e| e.to_object(py)); - inner(py, &mut iter) + Ok(set) } #[cfg(test)] @@ -263,7 +279,7 @@ mod tests { #[test] fn test_frozenset_new_and_len() { Python::with_gil(|py| { - let set = PyFrozenSet::new(py, &[1]).unwrap(); + let set = PyFrozenSet::new(py, [1]).unwrap(); assert_eq!(1, set.len()); let v = vec![1]; @@ -283,7 +299,7 @@ mod tests { #[test] fn test_frozenset_contains() { Python::with_gil(|py| { - let set = PyFrozenSet::new(py, &[1]).unwrap(); + let set = PyFrozenSet::new(py, [1]).unwrap(); assert!(set.contains(1).unwrap()); }); } @@ -291,7 +307,7 @@ mod tests { #[test] fn test_frozenset_iter() { Python::with_gil(|py| { - let set = PyFrozenSet::new(py, &[1]).unwrap(); + let set = PyFrozenSet::new(py, [1]).unwrap(); for el in set { assert_eq!(1i32, el.extract::().unwrap()); @@ -302,7 +318,7 @@ mod tests { #[test] fn test_frozenset_iter_bound() { Python::with_gil(|py| { - let set = PyFrozenSet::new(py, &[1]).unwrap(); + let set = PyFrozenSet::new(py, [1]).unwrap(); for el in &set { assert_eq!(1i32, el.extract::().unwrap()); @@ -313,7 +329,7 @@ mod tests { #[test] fn test_frozenset_iter_size_hint() { Python::with_gil(|py| { - let set = PyFrozenSet::new(py, &[1]).unwrap(); + let set = PyFrozenSet::new(py, [1]).unwrap(); let mut iter = set.iter(); // Exact size diff --git a/src/types/set.rs b/src/types/set.rs index 42b827a9ee0..eddc2eb8885 100644 --- a/src/types/set.rs +++ b/src/types/set.rs @@ -1,3 +1,4 @@ +use crate::conversion::IntoPyObject; use crate::types::PyIterator; use crate::{ err::{self, PyErr, PyResult}, @@ -6,7 +7,7 @@ use crate::{ py_result_ext::PyResultExt, types::any::PyAnyMethods, }; -use crate::{ffi, PyAny, PyObject, Python, ToPyObject}; +use crate::{ffi, Borrowed, BoundObject, PyAny, Python, ToPyObject}; use std::ptr; /// Represents a Python `set`. @@ -42,11 +43,14 @@ impl PySet { /// /// Returns an error if some element is not hashable. #[inline] - pub fn new<'a, 'p, T: ToPyObject + 'a>( - py: Python<'p>, - elements: impl IntoIterator, - ) -> PyResult> { - new_from_iter(py, elements) + pub fn new<'py, T>( + py: Python<'py>, + elements: impl IntoIterator, + ) -> PyResult> + where + T: IntoPyObject<'py>, + { + try_new_from_iter(py, elements) } /// Deprecated name for [`PySet::new`]. @@ -56,7 +60,7 @@ impl PySet { py: Python<'p>, elements: impl IntoIterator, ) -> PyResult> { - Self::new(py, elements) + Self::new(py, elements.into_iter().map(|e| e.to_object(py))) } /// Creates a new empty set. @@ -101,19 +105,19 @@ pub trait PySetMethods<'py>: crate::sealed::Sealed { /// This is equivalent to the Python expression `key in self`. fn contains(&self, key: K) -> PyResult where - K: ToPyObject; + K: IntoPyObject<'py>; /// Removes the element from the set if it is present. /// /// Returns `true` if the element was present in the set. fn discard(&self, key: K) -> PyResult where - K: ToPyObject; + K: IntoPyObject<'py>; /// Adds an element to the set. fn add(&self, key: K) -> PyResult<()> where - K: ToPyObject; + K: IntoPyObject<'py>; /// Removes and returns an arbitrary element from the set. fn pop(&self) -> Option>; @@ -141,9 +145,9 @@ impl<'py> PySetMethods<'py> for Bound<'py, PySet> { fn contains(&self, key: K) -> PyResult where - K: ToPyObject, + K: IntoPyObject<'py>, { - fn inner(set: &Bound<'_, PySet>, key: Bound<'_, PyAny>) -> PyResult { + fn inner(set: &Bound<'_, PySet>, key: Borrowed<'_, '_, PyAny>) -> PyResult { match unsafe { ffi::PySet_Contains(set.as_ptr(), key.as_ptr()) } { 1 => Ok(true), 0 => Ok(false), @@ -152,14 +156,20 @@ impl<'py> PySetMethods<'py> for Bound<'py, PySet> { } let py = self.py(); - inner(self, key.to_object(py).into_bound(py)) + inner( + self, + key.into_pyobject(py) + .map_err(Into::into)? + .into_any() + .as_borrowed(), + ) } fn discard(&self, key: K) -> PyResult where - K: ToPyObject, + K: IntoPyObject<'py>, { - fn inner(set: &Bound<'_, PySet>, key: Bound<'_, PyAny>) -> PyResult { + fn inner(set: &Bound<'_, PySet>, key: Borrowed<'_, '_, PyAny>) -> PyResult { match unsafe { ffi::PySet_Discard(set.as_ptr(), key.as_ptr()) } { 1 => Ok(true), 0 => Ok(false), @@ -168,21 +178,33 @@ impl<'py> PySetMethods<'py> for Bound<'py, PySet> { } let py = self.py(); - inner(self, key.to_object(py).into_bound(py)) + inner( + self, + key.into_pyobject(py) + .map_err(Into::into)? + .into_any() + .as_borrowed(), + ) } fn add(&self, key: K) -> PyResult<()> where - K: ToPyObject, + K: IntoPyObject<'py>, { - fn inner(set: &Bound<'_, PySet>, key: Bound<'_, PyAny>) -> PyResult<()> { + fn inner(set: &Bound<'_, PySet>, key: Borrowed<'_, '_, PyAny>) -> PyResult<()> { err::error_on_minusone(set.py(), unsafe { ffi::PySet_Add(set.as_ptr(), key.as_ptr()) }) } let py = self.py(); - inner(self, key.to_object(py).into_bound(py)) + inner( + self, + key.into_pyobject(py) + .map_err(Into::into)? + .into_any() + .as_borrowed(), + ) } fn pop(&self) -> Option> { @@ -268,15 +290,18 @@ pub(crate) fn new_from_iter( py: Python<'_>, elements: impl IntoIterator, ) -> PyResult> { - let mut iter = elements.into_iter().map(|e| Ok(e.to_object(py))); + let mut iter = elements.into_iter().map(|e| e.to_object(py)); try_new_from_iter(py, &mut iter) } #[inline] -pub(crate) fn try_new_from_iter( - py: Python<'_>, - elements: impl IntoIterator>, -) -> PyResult> { +pub(crate) fn try_new_from_iter<'py, T>( + py: Python<'py>, + elements: impl IntoIterator, +) -> PyResult> +where + T: IntoPyObject<'py>, +{ let set = unsafe { // We create the `Bound` pointer because its Drop cleans up the set if // user code errors or panics. @@ -286,8 +311,9 @@ pub(crate) fn try_new_from_iter( }; let ptr = set.as_ptr(); - for obj in elements { - err::error_on_minusone(py, unsafe { ffi::PySet_Add(ptr, obj?.as_ptr()) })?; + for e in elements { + let obj = e.into_pyobject(py).map_err(Into::into)?; + err::error_on_minusone(py, unsafe { ffi::PySet_Add(ptr, obj.as_ptr()) })?; } Ok(set) @@ -297,16 +323,17 @@ pub(crate) fn try_new_from_iter( mod tests { use super::PySet; use crate::{ + conversion::IntoPyObject, ffi, types::{PyAnyMethods, PySetMethods}, - Python, ToPyObject, + Python, }; use std::collections::HashSet; #[test] fn test_set_new() { Python::with_gil(|py| { - let set = PySet::new(py, &[1]).unwrap(); + let set = PySet::new(py, [1]).unwrap(); assert_eq!(1, set.len()); let v = vec![1]; @@ -326,13 +353,13 @@ mod tests { #[test] fn test_set_len() { Python::with_gil(|py| { - let mut v = HashSet::new(); - let ob = v.to_object(py); - let set = ob.downcast_bound::(py).unwrap(); + let mut v = HashSet::::new(); + let ob = (&v).into_pyobject(py).unwrap(); + let set = ob.downcast::().unwrap(); assert_eq!(0, set.len()); v.insert(7); - let ob = v.to_object(py); - let set2 = ob.downcast_bound::(py).unwrap(); + let ob = v.into_pyobject(py).unwrap(); + let set2 = ob.downcast::().unwrap(); assert_eq!(1, set2.len()); }); } @@ -340,7 +367,7 @@ mod tests { #[test] fn test_set_clear() { Python::with_gil(|py| { - let set = PySet::new(py, &[1]).unwrap(); + let set = PySet::new(py, [1]).unwrap(); assert_eq!(1, set.len()); set.clear(); assert_eq!(0, set.len()); @@ -350,7 +377,7 @@ mod tests { #[test] fn test_set_contains() { Python::with_gil(|py| { - let set = PySet::new(py, &[1]).unwrap(); + let set = PySet::new(py, [1]).unwrap(); assert!(set.contains(1).unwrap()); }); } @@ -358,7 +385,7 @@ mod tests { #[test] fn test_set_discard() { Python::with_gil(|py| { - let set = PySet::new(py, &[1]).unwrap(); + let set = PySet::new(py, [1]).unwrap(); assert!(!set.discard(2).unwrap()); assert_eq!(1, set.len()); @@ -373,7 +400,7 @@ mod tests { #[test] fn test_set_add() { Python::with_gil(|py| { - let set = PySet::new(py, &[1, 2]).unwrap(); + let set = PySet::new(py, [1, 2]).unwrap(); set.add(1).unwrap(); // Add a dupliated element assert!(set.contains(1).unwrap()); }); @@ -382,7 +409,7 @@ mod tests { #[test] fn test_set_pop() { Python::with_gil(|py| { - let set = PySet::new(py, &[1]).unwrap(); + let set = PySet::new(py, [1]).unwrap(); let val = set.pop(); assert!(val.is_some()); let val2 = set.pop(); @@ -400,7 +427,7 @@ mod tests { #[test] fn test_set_iter() { Python::with_gil(|py| { - let set = PySet::new(py, &[1]).unwrap(); + let set = PySet::new(py, [1]).unwrap(); for el in set { assert_eq!(1i32, el.extract::<'_, i32>().unwrap()); @@ -413,7 +440,7 @@ mod tests { use crate::types::any::PyAnyMethods; Python::with_gil(|py| { - let set = PySet::new(py, &[1]).unwrap(); + let set = PySet::new(py, [1]).unwrap(); for el in &set { assert_eq!(1i32, el.extract::().unwrap()); @@ -425,7 +452,7 @@ mod tests { #[should_panic] fn test_set_iter_mutation() { Python::with_gil(|py| { - let set = PySet::new(py, &[1, 2, 3, 4, 5]).unwrap(); + let set = PySet::new(py, [1, 2, 3, 4, 5]).unwrap(); for _ in &set { let _ = set.add(42); @@ -437,7 +464,7 @@ mod tests { #[should_panic] fn test_set_iter_mutation_same_len() { Python::with_gil(|py| { - let set = PySet::new(py, &[1, 2, 3, 4, 5]).unwrap(); + let set = PySet::new(py, [1, 2, 3, 4, 5]).unwrap(); for item in &set { let item: i32 = item.extract().unwrap(); @@ -450,7 +477,7 @@ mod tests { #[test] fn test_set_iter_size_hint() { Python::with_gil(|py| { - let set = PySet::new(py, &[1]).unwrap(); + let set = PySet::new(py, [1]).unwrap(); let mut iter = set.iter(); // Exact size From ce18f79d71f4d3eac54f55f7633cf08d2f57b64e Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Mon, 30 Sep 2024 14:19:42 -0600 Subject: [PATCH 292/495] Don't use PyList.get_item_unchecked() on free-threaded build (#4539) * disable list.get_item_unchecked() on the free-threaded build * add changelog entry --- newsfragments/4539.removed.md | 3 +++ src/types/list.rs | 10 +++++----- 2 files changed, 8 insertions(+), 5 deletions(-) create mode 100644 newsfragments/4539.removed.md diff --git a/newsfragments/4539.removed.md b/newsfragments/4539.removed.md new file mode 100644 index 00000000000..dd1da0169b6 --- /dev/null +++ b/newsfragments/4539.removed.md @@ -0,0 +1,3 @@ +* `PyListMethods::get_item_unchecked` is disabled on the free-threaded build. + It relies on accessing list internals without any locking and is not + thread-safe without the GIL to synchronize access. diff --git a/src/types/list.rs b/src/types/list.rs index 4db14849d46..a8952273341 100644 --- a/src/types/list.rs +++ b/src/types/list.rs @@ -182,7 +182,7 @@ pub trait PyListMethods<'py>: crate::sealed::Sealed { /// # Safety /// /// Caller must verify that the index is within the bounds of the list. - #[cfg(not(Py_LIMITED_API))] + #[cfg(not(any(Py_LIMITED_API, Py_GIL_DISABLED)))] unsafe fn get_item_unchecked(&self, index: usize) -> Bound<'py, PyAny>; /// Takes the slice `self[low:high]` and returns it as a new list. @@ -305,7 +305,7 @@ impl<'py> PyListMethods<'py> for Bound<'py, PyList> { /// # Safety /// /// Caller must verify that the index is within the bounds of the list. - #[cfg(not(Py_LIMITED_API))] + #[cfg(not(any(Py_LIMITED_API, Py_GIL_DISABLED)))] unsafe fn get_item_unchecked(&self, index: usize) -> Bound<'py, PyAny> { // PyList_GET_ITEM return borrowed ptr; must make owned for safety (see #890). ffi::PyList_GET_ITEM(self.as_ptr(), index as Py_ssize_t) @@ -496,9 +496,9 @@ impl<'py> BoundListIterator<'py> { } unsafe fn get_item(&self, index: usize) -> Bound<'py, PyAny> { - #[cfg(any(Py_LIMITED_API, PyPy))] + #[cfg(any(Py_LIMITED_API, PyPy, Py_GIL_DISABLED))] let item = self.list.get_item(index).expect("list.get failed"); - #[cfg(not(any(Py_LIMITED_API, PyPy)))] + #[cfg(not(any(Py_LIMITED_API, PyPy, Py_GIL_DISABLED)))] let item = self.list.get_item_unchecked(index); item } @@ -893,7 +893,7 @@ mod tests { }); } - #[cfg(not(any(Py_LIMITED_API, PyPy)))] + #[cfg(not(any(Py_LIMITED_API, PyPy, Py_GIL_DISABLED)))] #[test] fn test_list_get_item_unchecked_sanity() { Python::with_gil(|py| { From faed5e2b41f78f3fb9bb68f8d878b069c0e9810a Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Tue, 1 Oct 2024 23:09:08 +0200 Subject: [PATCH 293/495] migrate `PyDictMethods` trait bounds (#4493) * migrate `PyDictMethods` trait bounds * migrate `IntoPyDict` * make `IntoPyDict::into_py_dict` fallible * update migration guide * remove unnecessary `clone` Co-authored-by: David Hewitt --------- Co-authored-by: David Hewitt --- README.md | 2 +- guide/src/exception.md | 7 +- guide/src/migration.md | 42 +++-- guide/src/module.md | 2 +- .../python-from-rust/calling-existing-code.md | 2 +- guide/src/python-from-rust/function-calls.md | 6 +- newsfragments/4493.changed.md | 1 + src/conversions/anyhow.rs | 4 +- src/conversions/chrono.rs | 2 +- src/conversions/eyre.rs | 4 +- src/conversions/hashbrown.rs | 32 ++-- src/conversions/indexmap.rs | 31 ++-- src/conversions/std/map.rs | 54 +++--- src/exceptions.rs | 15 +- src/impl_/extract_argument.rs | 4 +- src/instance.rs | 2 +- src/lib.rs | 2 +- src/macros.rs | 9 +- src/marker.rs | 2 +- src/tests/common.rs | 4 +- src/types/any.rs | 2 +- src/types/dict.rs | 156 ++++++++++-------- tests/test_buffer_protocol.rs | 4 +- tests/test_class_basics.rs | 3 +- tests/test_class_new.rs | 2 +- tests/test_coroutine.rs | 11 +- tests/test_datetime.rs | 6 +- tests/test_enum.rs | 6 +- tests/test_getter_setter.rs | 4 +- tests/test_inheritance.rs | 8 +- tests/test_macro_docs.rs | 4 +- tests/test_mapping.rs | 4 +- tests/test_methods.rs | 20 ++- tests/test_module.rs | 10 +- tests/test_sequence.rs | 12 +- tests/test_static_slots.rs | 4 +- 36 files changed, 278 insertions(+), 205 deletions(-) create mode 100644 newsfragments/4493.changed.md diff --git a/README.md b/README.md index 94b7fa49f70..e9562bf7e12 100644 --- a/README.md +++ b/README.md @@ -156,7 +156,7 @@ fn main() -> PyResult<()> { let sys = py.import("sys")?; let version: String = sys.getattr("version")?.extract()?; - let locals = [("os", py.import("os")?)].into_py_dict(py); + let locals = [("os", py.import("os")?)].into_py_dict(py)?; let code = c_str!("os.getenv('USER') or os.getenv('USERNAME') or 'Unknown'"); let user: String = py.eval(code, None, Some(&locals))?.extract()?; diff --git a/guide/src/exception.md b/guide/src/exception.md index 4a1f1425d72..fd427a34fb2 100644 --- a/guide/src/exception.md +++ b/guide/src/exception.md @@ -23,15 +23,18 @@ use pyo3::exceptions::PyException; create_exception!(mymodule, CustomError, PyException); +# fn main() -> PyResult<()> { Python::with_gil(|py| { - let ctx = [("CustomError", py.get_type::())].into_py_dict(py); + let ctx = [("CustomError", py.get_type::())].into_py_dict(py)?; pyo3::py_run!( py, *ctx, "assert str(CustomError) == \"\"" ); pyo3::py_run!(py, *ctx, "assert CustomError('oops').args == ('oops',)"); -}); +# Ok(()) +}) +# } ``` When using PyO3 to create an extension module, you can add the new exception to diff --git a/guide/src/migration.md b/guide/src/migration.md index fb212743702..75b102d1c40 100644 --- a/guide/src/migration.md +++ b/guide/src/migration.md @@ -45,23 +45,26 @@ let tup = PyTuple::new(py, [1, 2, 3]); ### Free-threaded Python Support -
-Click to expand PyO3 0.23 introduces preliminary support for the new free-threaded build of -CPython 3.13. PyO3 features that implicitly assumed the existence of the GIL -are not exposed in the free-threaded build, since they are no longer safe. -Other features, such as `GILOnceCell`, have been internally rewritten to be -threadsafe without the GIL. +CPython 3.13. PyO3 features that implicitly assumed the existence of the GIL are +not exposed in the free-threaded build, since they are no longer safe. Other +features, such as `GILOnceCell`, have been internally rewritten to be threadsafe +without the GIL. If you make use of these features then you will need to account for the unavailability of this API in the free-threaded build. One way to handle it is via conditional compilation -- extensions built for the free-threaded build will have the `Py_GIL_DISABLED` attribute defined. -### `GILProtected` - -`GILProtected` allows mutable access to static data by leveraging the GIL to -lock concurrent access from other threads. In free-threaded python there is no -GIL, so you will need to replace this type with some other form of locking. In -many cases, `std::sync::Atomic` or `std::sync::Mutex` will be sufficient. If the -locks do not guard the execution of arbitrary Python code or use of the CPython -C API then conditional compilation is likely unnecessary since `GILProtected` -was not needed in the first place. - -Before: - -```rust -# fn main() { -# #[cfg(not(Py_GIL_DISABLED))] { -# use pyo3::prelude::*; -use pyo3::sync::GILProtected; -use pyo3::types::{PyDict, PyNone}; -use std::cell::RefCell; - -static OBJECTS: GILProtected>>> = - GILProtected::new(RefCell::new(Vec::new())); - -Python::with_gil(|py| { - // stand-in for something that executes arbitrary python code - let d = PyDict::new(py); - d.set_item(PyNone::get(py), PyNone::get(py)).unwrap(); - OBJECTS.get(py).borrow_mut().push(d.unbind()); -}); -# }} -``` - -After: - -```rust -# use pyo3::prelude::*; -# fn main() { -use pyo3::types::{PyDict, PyNone}; -use std::sync::Mutex; - -static OBJECTS: Mutex>> = Mutex::new(Vec::new()); - -Python::with_gil(|py| { - // stand-in for something that executes arbitrary python code - let d = PyDict::new(py); - d.set_item(PyNone::get(py), PyNone::get(py)).unwrap(); - // we're not executing python code while holding the lock, so GILProtected - // was never needed - OBJECTS.lock().unwrap().push(d.unbind()); -}); -# } -``` - -If you are executing arbitrary Python code while holding the lock, then you will -need to use conditional compilation to use `GILProtected` on GIL-enabled python -builds and mutexes otherwise. Python 3.13 introduces `PyMutex`, which releases -the GIL while the lock is held, so that is another option if you only need to -support newer Python versions. - -
+See [the guide section on free-threaded Python](free-threading.md) for more +details about supporting free-threaded Python in your PyO3 extensions. ## from 0.21.* to 0.22 diff --git a/newsfragments/4577.added.md b/newsfragments/4577.added.md new file mode 100644 index 00000000000..71858564fe5 --- /dev/null +++ b/newsfragments/4577.added.md @@ -0,0 +1 @@ +* Added a guide page for free-threaded Python. diff --git a/src/lib.rs b/src/lib.rs index 7de32ca264f..247b42ac372 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -501,6 +501,7 @@ pub mod doc_test { "guide/src/exception.md" => guide_exception_md, "guide/src/faq.md" => guide_faq_md, "guide/src/features.md" => guide_features_md, + "guide/src/free-threading.md" => guide_free_threading_md, "guide/src/function.md" => guide_function_md, "guide/src/function/error-handling.md" => guide_function_error_handling_md, "guide/src/function/signature.md" => guide_function_signature_md, From 4bb40587a24137e64e47d08b343191634ddc4565 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Mon, 21 Oct 2024 18:42:26 +0100 Subject: [PATCH 330/495] ci: fix clippy beta warning (#4637) --- src/impl_/pyclass.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/impl_/pyclass.rs b/src/impl_/pyclass.rs index 53c8dacff35..5a1fb963271 100644 --- a/src/impl_/pyclass.rs +++ b/src/impl_/pyclass.rs @@ -1458,6 +1458,9 @@ impl> IsIntoPy { probe!(IsIntoPyObjectRef); +// Possible clippy beta regression, +// see https://github.com/rust-lang/rust-clippy/issues/13578 +#[allow(clippy::extra_unused_lifetimes)] impl<'a, 'py, T: 'a> IsIntoPyObjectRef where &'a T: IntoPyObject<'py>, From 133eac84d40c2c6689edbb56294af6d528e7288f Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Mon, 21 Oct 2024 20:37:59 +0100 Subject: [PATCH 331/495] ci: run coverage for the free-threaded build (#4638) --- .github/workflows/ci.yml | 18 +++++++++--- .github/workflows/coverage-pr-base.yml | 40 ++++++++++++++++++++++++++ noxfile.py | 12 ++++++++ 3 files changed, 66 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/coverage-pr-base.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1efe6b06db1..dcc4dd8bf0b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -451,10 +451,6 @@ jobs: runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 - with: - # Use the PR head, not the merge commit, because the head commit (and the base) - # is what codecov uses to calculate diffs. - ref: ${{ github.event.pull_request.head.sha }} - uses: actions/setup-python@v5 with: python-version: '3.12' @@ -572,10 +568,24 @@ jobs: with: python-version: '3.13-dev' nogil: true + - name: Install cargo-llvm-cov + uses: taiki-e/install-action@cargo-llvm-cov - run: python3 -m sysconfig - run: python3 -m pip install --upgrade pip && pip install nox + - name: Prepare coverage environment + run: | + cargo llvm-cov clean --workspace --profraw-only + nox -s set-coverage-env - run: nox -s ffi-check - run: nox + - name: Generate coverage report + run: nox -s generate-coverage-report + - name: Upload coverage report + uses: codecov/codecov-action@v4 + with: + file: coverage.json + name: test-free-threaded + token: ${{ secrets.CODECOV_TOKEN }} test-version-limits: needs: [fmt] diff --git a/.github/workflows/coverage-pr-base.yml b/.github/workflows/coverage-pr-base.yml new file mode 100644 index 00000000000..d4f04fc6e69 --- /dev/null +++ b/.github/workflows/coverage-pr-base.yml @@ -0,0 +1,40 @@ +# This runs as a separate job because it needs to run on the `pull_request_target` event +# in order to access the CODECOV_TOKEN secret. +# +# This is safe because this doesn't run arbitrary code from PRs. + +name: Set Codecov PR base +on: + # See safety note / doc at the top of this file. + pull_request_target: + +jobs: + coverage-pr-base: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + - name: Set PR base on codecov + run: | + # fetch the merge commit between the PR base and head + BASE_REF=refs/heads/${{ github.event.pull_request.base.ref }} + MERGE_REF=refs/pull/${{ github.event.pull_request.number }}/merge + + git fetch --progress --depth=1 origin "+$BASE_REF:$BASE_REF" "+$MERGE_REF:$MERGE_REF" + while [ -z "$(git merge-base "$BASE_REF" "$MERGE_REF")" ]; do + git fetch -q --deepen="10" origin "$BASE_REF" "$MERGE_REF"; + done + + MERGE_BASE=$(git merge-base "$BASE_REF" "$MERGE_REF") + echo "Merge base: $MERGE_BASE" + + # inform codecov about the merge base + pip install codecov-cli + codecovcli pr-base-picking \ + --base-sha $MERGE_BASE \ + --pr ${{ github.event.number }} \ + --slug PyO3/pyo3 \ + --token ${{ secrets.CODECOV_TOKEN }} \ + --service github diff --git a/noxfile.py b/noxfile.py index f29bb45c109..ce59162f120 100644 --- a/noxfile.py +++ b/noxfile.py @@ -72,7 +72,19 @@ def coverage(session: nox.Session) -> None: session.env.update(_get_coverage_env()) _run_cargo(session, "llvm-cov", "clean", "--workspace") test(session) + generate_coverage_report(session) + +@nox.session(name="set-coverage-env", venv_backend="none") +def set_coverage_env(session: nox.Session) -> None: + """For use in GitHub Actions to set coverage environment variables.""" + with open(os.environ["GITHUB_ENV"], "a") as env_file: + for k, v in _get_coverage_env().items(): + print(f"{k}={v}", file=env_file) + + +@nox.session(name="generate-coverage-report", venv_backend="none") +def generate_coverage_report(session: nox.Session) -> None: cov_format = "codecov" output_file = "coverage.json" From 9a4a9a2928859394d58a5110ba1bc60e319703fc Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Wed, 23 Oct 2024 13:27:41 -0600 Subject: [PATCH 332/495] Attempt to fix the coverage-pr-base workflow (#4642) --- .github/workflows/coverage-pr-base.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/coverage-pr-base.yml b/.github/workflows/coverage-pr-base.yml index d4f04fc6e69..acc645ea876 100644 --- a/.github/workflows/coverage-pr-base.yml +++ b/.github/workflows/coverage-pr-base.yml @@ -22,9 +22,9 @@ jobs: BASE_REF=refs/heads/${{ github.event.pull_request.base.ref }} MERGE_REF=refs/pull/${{ github.event.pull_request.number }}/merge - git fetch --progress --depth=1 origin "+$BASE_REF:$BASE_REF" "+$MERGE_REF:$MERGE_REF" + git fetch -u --progress --depth=1 origin "+$BASE_REF:$BASE_REF" "+$MERGE_REF:$MERGE_REF" while [ -z "$(git merge-base "$BASE_REF" "$MERGE_REF")" ]; do - git fetch -q --deepen="10" origin "$BASE_REF" "$MERGE_REF"; + git fetch -u -q --deepen="10" origin "$BASE_REF" "$MERGE_REF"; done MERGE_BASE=$(git merge-base "$BASE_REF" "$MERGE_REF") From 96c31e7126c3bc638a760dc136aa2cd65f14f130 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Wed, 23 Oct 2024 22:21:20 -0600 Subject: [PATCH 333/495] Add example Cargo.toml and build.rs for exposing pyo3 cfgs (#4641) * Add example Cargo.toml and build.rs for exposing pyo3 cfgs * Document all of the config attributes * fix pyo3-ffi tests * ignore the code examples instead of adding a dev dependency --- pyo3-ffi/src/lib.rs | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/pyo3-ffi/src/lib.rs b/pyo3-ffi/src/lib.rs index c6157401124..293c5171eb5 100644 --- a/pyo3-ffi/src/lib.rs +++ b/pyo3-ffi/src/lib.rs @@ -43,10 +43,39 @@ //! PyO3 uses `rustc`'s `--cfg` flags to enable or disable code used for different Python versions. //! If you want to do this for your own crate, you can do so with the [`pyo3-build-config`] crate. //! -//! - `Py_3_7`, `Py_3_8`, `Py_3_9`, `Py_3_10`: Marks code that is only enabled when -//! compiling for a given minimum Python version. +//! - `Py_3_7`, `Py_3_8`, `Py_3_9`, `Py_3_10`, `Py_3_11`, `Py_3_12`, `Py_3_13`: Marks code that is +//! only enabled when compiling for a given minimum Python version. //! - `Py_LIMITED_API`: Marks code enabled when the `abi3` feature flag is enabled. +//! - `Py_GIL_DISABLED`: Marks code that runs only in the free-threaded build of CPython. //! - `PyPy` - Marks code enabled when compiling for PyPy. +//! - `GraalPy` - Marks code enabled when compiling for GraalPy. +//! +//! Additionally, you can query for the values `Py_DEBUG`, `Py_REF_DEBUG`, +//! `Py_TRACE_REFS`, and `COUNT_ALLOCS` from `py_sys_config` to query for the +//! corresponding C build-time defines. For example, to conditionally define +//! debug code using `Py_DEBUG`, you could do: +//! +//! ```rust,ignore +//! #[cfg(py_sys_config = "Py_DEBUG")] +//! println!("only runs if python was compiled with Py_DEBUG") +//! ``` +//! +//! To use these attributes, add [`pyo3-build-config`] as a build dependency in +//! your `Cargo.toml`: +//! +//! ```toml +//! [build-dependency] +//! pyo3-build-config = "VER" +//! ``` +//! +//! And then either create a new `build.rs` file in the project root or modify +//! the existing `build.rs` file to call `use_pyo3_cfgs()`: +//! +//! ```rust,ignore +//! fn main() { +//! pyo3_build_config::use_pyo3_cfgs(); +//! } +//! ``` //! //! # Minimum supported Rust and Python versions //! From 602fbbab44b7de0e0fbfde95ba3fb1ebace3685c Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers <7943856+bschoenmaeckers@users.noreply.github.com> Date: Thu, 24 Oct 2024 09:14:03 +0200 Subject: [PATCH 334/495] Make `PyDict` iterator compatible with free-threaded build (#4439) * Iterate over dict items in `DictIterator` * Use python instead of C API to get dict items * Use plain dict iterator when not on free-threaded & not a subclass * Copy dict on free-threaded builds to prevent concurrent modifications * Add test for dict subclass iters * Implement `PyDict::locked_for_each` * Lock `BoundDictIterator::next` on each iteration * Implement locked `fold` & `try_fold` * Implement `all`,`any`,`find`,`find_map`,`position` when not on nightly * Add changelog * Use critical section wrapper * Make `dict::locked_for_each` available on all builds * Remove item() iter * Add tests for `PyDict::iter()` reducers * Add more docs to locked_for_each * Move iter implementation into inner struct --- newsfragments/4439.changed.md | 3 + src/lib.rs | 5 +- src/types/dict.rs | 404 +++++++++++++++++++++++++++++----- 3 files changed, 355 insertions(+), 57 deletions(-) create mode 100644 newsfragments/4439.changed.md diff --git a/newsfragments/4439.changed.md b/newsfragments/4439.changed.md new file mode 100644 index 00000000000..9cb01a4d2b8 --- /dev/null +++ b/newsfragments/4439.changed.md @@ -0,0 +1,3 @@ +* Make `PyDict` iterator compatible with free-threaded build +* Added `PyDict::locked_for_each` method to iterate on free-threaded builds to prevent the dict being mutated during iteration +* Iterate over `dict.items()` when dict is subclassed from `PyDict` diff --git a/src/lib.rs b/src/lib.rs index 247b42ac372..73a22e94103 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,8 @@ #![warn(missing_docs)] -#![cfg_attr(feature = "nightly", feature(auto_traits, negative_impls))] +#![cfg_attr( + feature = "nightly", + feature(auto_traits, negative_impls, try_trait_v2) +)] #![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] // Deny some lints in doctests. // Use `#[allow(...)]` locally to override. diff --git a/src/types/dict.rs b/src/types/dict.rs index 9b7d8697d20..6987e4525ec 100644 --- a/src/types/dict.rs +++ b/src/types/dict.rs @@ -1,11 +1,9 @@ -use super::PyMapping; use crate::err::{self, PyErr, PyResult}; use crate::ffi::Py_ssize_t; use crate::ffi_ptr_ext::FfiPtrExt; use crate::instance::{Borrowed, Bound}; use crate::py_result_ext::PyResultExt; -use crate::types::any::PyAnyMethods; -use crate::types::{PyAny, PyList}; +use crate::types::{PyAny, PyAnyMethods, PyList, PyMapping}; use crate::{ffi, BoundObject, IntoPyObject, Python}; /// Represents a Python `dict`. @@ -180,6 +178,19 @@ pub trait PyDictMethods<'py>: crate::sealed::Sealed { /// so long as the set of keys does not change. fn iter(&self) -> BoundDictIterator<'py>; + /// Iterates over the contents of this dictionary while holding a critical section on the dict. + /// This is useful when the GIL is disabled and the dictionary is shared between threads. + /// It is not guaranteed that the dictionary will not be modified during iteration when the + /// closure calls arbitrary Python code that releases the current critical section. + /// + /// This method is a small performance optimization over `.iter().try_for_each()` when the + /// nightly feature is not enabled because we cannot implement an optimised version of + /// `iter().try_fold()` on stable yet. If your iteration is infallible then this method has the + /// same performance as `.iter().for_each()`. + fn locked_for_each(&self, closure: F) -> PyResult<()> + where + F: Fn(Bound<'py, PyAny>, Bound<'py, PyAny>) -> PyResult<()>; + /// Returns `self` cast as a `PyMapping`. fn as_mapping(&self) -> &Bound<'py, PyMapping>; @@ -357,6 +368,25 @@ impl<'py> PyDictMethods<'py> for Bound<'py, PyDict> { BoundDictIterator::new(self.clone()) } + fn locked_for_each(&self, f: F) -> PyResult<()> + where + F: Fn(Bound<'py, PyAny>, Bound<'py, PyAny>) -> PyResult<()>, + { + #[cfg(feature = "nightly")] + { + // We don't need a critical section when the nightly feature is enabled because + // try_for_each is locked by the implementation of try_fold. + self.iter().try_for_each(|(key, value)| f(key, value)) + } + + #[cfg(not(feature = "nightly"))] + { + crate::sync::with_critical_section(self, || { + self.iter().try_for_each(|(key, value)| f(key, value)) + }) + } + } + fn as_mapping(&self) -> &Bound<'py, PyMapping> { unsafe { self.downcast_unchecked() } } @@ -403,9 +433,86 @@ fn dict_len(dict: &Bound<'_, PyDict>) -> Py_ssize_t { /// PyO3 implementation of an iterator for a Python `dict` object. pub struct BoundDictIterator<'py> { dict: Bound<'py, PyDict>, - ppos: ffi::Py_ssize_t, - di_used: ffi::Py_ssize_t, - len: ffi::Py_ssize_t, + inner: DictIterImpl, +} + +enum DictIterImpl { + DictIter { + ppos: ffi::Py_ssize_t, + di_used: ffi::Py_ssize_t, + remaining: ffi::Py_ssize_t, + }, +} + +impl DictIterImpl { + #[inline] + fn next<'py>( + &mut self, + dict: &Bound<'py, PyDict>, + ) -> Option<(Bound<'py, PyAny>, Bound<'py, PyAny>)> { + match self { + Self::DictIter { + di_used, + remaining, + ppos, + .. + } => crate::sync::with_critical_section(dict, || { + let ma_used = dict_len(dict); + + // These checks are similar to what CPython does. + // + // If the dimension of the dict changes e.g. key-value pairs are removed + // or added during iteration, this will panic next time when `next` is called + if *di_used != ma_used { + *di_used = -1; + panic!("dictionary changed size during iteration"); + }; + + // If the dict is changed in such a way that the length remains constant + // then this will panic at the end of iteration - similar to this: + // + // d = {"a":1, "b":2, "c": 3} + // + // for k, v in d.items(): + // d[f"{k}_"] = 4 + // del d[k] + // print(k) + // + if *remaining == -1 { + *di_used = -1; + panic!("dictionary keys changed during iteration"); + }; + + let mut key: *mut ffi::PyObject = std::ptr::null_mut(); + let mut value: *mut ffi::PyObject = std::ptr::null_mut(); + + if unsafe { ffi::PyDict_Next(dict.as_ptr(), ppos, &mut key, &mut value) } != 0 { + *remaining -= 1; + let py = dict.py(); + // Safety: + // - PyDict_Next returns borrowed values + // - we have already checked that `PyDict_Next` succeeded, so we can assume these to be non-null + Some(( + unsafe { key.assume_borrowed_unchecked(py) }.to_owned(), + unsafe { value.assume_borrowed_unchecked(py) }.to_owned(), + )) + } else { + None + } + }), + } + } + + #[cfg(Py_GIL_DISABLED)] + #[inline] + fn with_critical_section(&mut self, dict: &Bound<'_, PyDict>, f: F) -> R + where + F: FnOnce(&mut Self) -> R, + { + match self { + Self::DictIter { .. } => crate::sync::with_critical_section(dict, || f(self)), + } + } } impl<'py> Iterator for BoundDictIterator<'py> { @@ -413,50 +520,7 @@ impl<'py> Iterator for BoundDictIterator<'py> { #[inline] fn next(&mut self) -> Option { - let ma_used = dict_len(&self.dict); - - // These checks are similar to what CPython does. - // - // If the dimension of the dict changes e.g. key-value pairs are removed - // or added during iteration, this will panic next time when `next` is called - if self.di_used != ma_used { - self.di_used = -1; - panic!("dictionary changed size during iteration"); - }; - - // If the dict is changed in such a way that the length remains constant - // then this will panic at the end of iteration - similar to this: - // - // d = {"a":1, "b":2, "c": 3} - // - // for k, v in d.items(): - // d[f"{k}_"] = 4 - // del d[k] - // print(k) - // - if self.len == -1 { - self.di_used = -1; - panic!("dictionary keys changed during iteration"); - }; - - let mut key: *mut ffi::PyObject = std::ptr::null_mut(); - let mut value: *mut ffi::PyObject = std::ptr::null_mut(); - - if unsafe { ffi::PyDict_Next(self.dict.as_ptr(), &mut self.ppos, &mut key, &mut value) } - != 0 - { - self.len -= 1; - let py = self.dict.py(); - // Safety: - // - PyDict_Next returns borrowed values - // - we have already checked that `PyDict_Next` succeeded, so we can assume these to be non-null - Some(( - unsafe { key.assume_borrowed_unchecked(py) }.to_owned(), - unsafe { value.assume_borrowed_unchecked(py) }.to_owned(), - )) - } else { - None - } + self.inner.next(&self.dict) } #[inline] @@ -464,22 +528,147 @@ impl<'py> Iterator for BoundDictIterator<'py> { let len = self.len(); (len, Some(len)) } + + #[inline] + #[cfg(Py_GIL_DISABLED)] + fn fold(mut self, init: B, mut f: F) -> B + where + Self: Sized, + F: FnMut(B, Self::Item) -> B, + { + self.inner.with_critical_section(&self.dict, |inner| { + let mut accum = init; + while let Some(x) = inner.next(&self.dict) { + accum = f(accum, x); + } + accum + }) + } + + #[inline] + #[cfg(all(Py_GIL_DISABLED, feature = "nightly"))] + fn try_fold(&mut self, init: B, mut f: F) -> R + where + Self: Sized, + F: FnMut(B, Self::Item) -> R, + R: std::ops::Try, + { + self.inner.with_critical_section(&self.dict, |inner| { + let mut accum = init; + while let Some(x) = inner.next(&self.dict) { + accum = f(accum, x)? + } + R::from_output(accum) + }) + } + + #[inline] + #[cfg(all(Py_GIL_DISABLED, not(feature = "nightly")))] + fn all(&mut self, mut f: F) -> bool + where + Self: Sized, + F: FnMut(Self::Item) -> bool, + { + self.inner.with_critical_section(&self.dict, |inner| { + while let Some(x) = inner.next(&self.dict) { + if !f(x) { + return false; + } + } + true + }) + } + + #[inline] + #[cfg(all(Py_GIL_DISABLED, not(feature = "nightly")))] + fn any(&mut self, mut f: F) -> bool + where + Self: Sized, + F: FnMut(Self::Item) -> bool, + { + self.inner.with_critical_section(&self.dict, |inner| { + while let Some(x) = inner.next(&self.dict) { + if f(x) { + return true; + } + } + false + }) + } + + #[inline] + #[cfg(all(Py_GIL_DISABLED, not(feature = "nightly")))] + fn find

(&mut self, mut predicate: P) -> Option + where + Self: Sized, + P: FnMut(&Self::Item) -> bool, + { + self.inner.with_critical_section(&self.dict, |inner| { + while let Some(x) = inner.next(&self.dict) { + if predicate(&x) { + return Some(x); + } + } + None + }) + } + + #[inline] + #[cfg(all(Py_GIL_DISABLED, not(feature = "nightly")))] + fn find_map(&mut self, mut f: F) -> Option + where + Self: Sized, + F: FnMut(Self::Item) -> Option, + { + self.inner.with_critical_section(&self.dict, |inner| { + while let Some(x) = inner.next(&self.dict) { + if let found @ Some(_) = f(x) { + return found; + } + } + None + }) + } + + #[inline] + #[cfg(all(Py_GIL_DISABLED, not(feature = "nightly")))] + fn position

(&mut self, mut predicate: P) -> Option + where + Self: Sized, + P: FnMut(Self::Item) -> bool, + { + self.inner.with_critical_section(&self.dict, |inner| { + let mut acc = 0; + while let Some(x) = inner.next(&self.dict) { + if predicate(x) { + return Some(acc); + } + acc += 1; + } + None + }) + } } impl ExactSizeIterator for BoundDictIterator<'_> { fn len(&self) -> usize { - self.len as usize + match self.inner { + DictIterImpl::DictIter { remaining, .. } => remaining as usize, + } } } impl<'py> BoundDictIterator<'py> { fn new(dict: Bound<'py, PyDict>) -> Self { - let len = dict_len(&dict); - BoundDictIterator { + let remaining = dict_len(&dict); + + Self { dict, - ppos: 0, - di_used: len, - len, + inner: DictIterImpl::DictIter { + ppos: 0, + di_used: remaining, + remaining, + }, } } } @@ -1360,4 +1549,107 @@ mod tests { ); }) } + + #[test] + fn test_iter_all() { + Python::with_gil(|py| { + let dict = [(1, true), (2, true), (3, true)].into_py_dict(py).unwrap(); + assert!(dict.iter().all(|(_, v)| v.extract::().unwrap())); + + let dict = [(1, true), (2, false), (3, true)].into_py_dict(py).unwrap(); + assert!(!dict.iter().all(|(_, v)| v.extract::().unwrap())); + }); + } + + #[test] + fn test_iter_any() { + Python::with_gil(|py| { + let dict = [(1, true), (2, false), (3, false)] + .into_py_dict(py) + .unwrap(); + assert!(dict.iter().any(|(_, v)| v.extract::().unwrap())); + + let dict = [(1, false), (2, false), (3, false)] + .into_py_dict(py) + .unwrap(); + assert!(!dict.iter().any(|(_, v)| v.extract::().unwrap())); + }); + } + + #[test] + #[allow(clippy::search_is_some)] + fn test_iter_find() { + Python::with_gil(|py| { + let dict = [(1, false), (2, true), (3, false)] + .into_py_dict(py) + .unwrap(); + + assert_eq!( + Some((2, true)), + dict.iter() + .find(|(_, v)| v.extract::().unwrap()) + .map(|(k, v)| (k.extract().unwrap(), v.extract().unwrap())) + ); + + let dict = [(1, false), (2, false), (3, false)] + .into_py_dict(py) + .unwrap(); + + assert!(dict + .iter() + .find(|(_, v)| v.extract::().unwrap()) + .is_none()); + }); + } + + #[test] + #[allow(clippy::search_is_some)] + fn test_iter_position() { + Python::with_gil(|py| { + let dict = [(1, false), (2, false), (3, true)] + .into_py_dict(py) + .unwrap(); + assert_eq!( + Some(2), + dict.iter().position(|(_, v)| v.extract::().unwrap()) + ); + + let dict = [(1, false), (2, false), (3, false)] + .into_py_dict(py) + .unwrap(); + assert!(dict + .iter() + .position(|(_, v)| v.extract::().unwrap()) + .is_none()); + }); + } + + #[test] + fn test_iter_fold() { + Python::with_gil(|py| { + let dict = [(1, 1), (2, 2), (3, 3)].into_py_dict(py).unwrap(); + let sum = dict + .iter() + .fold(0, |acc, (_, v)| acc + v.extract::().unwrap()); + assert_eq!(sum, 6); + }); + } + + #[test] + fn test_iter_try_fold() { + Python::with_gil(|py| { + let dict = [(1, 1), (2, 2), (3, 3)].into_py_dict(py).unwrap(); + let sum = dict + .iter() + .try_fold(0, |acc, (_, v)| PyResult::Ok(acc + v.extract::()?)) + .unwrap(); + assert_eq!(sum, 6); + + let dict = [(1, "foo"), (2, "bar")].into_py_dict(py).unwrap(); + assert!(dict + .iter() + .try_fold(0, |acc, (_, v)| PyResult::Ok(acc + v.extract::()?)) + .is_err()); + }); + } } From 6aa5e6b334a6fd2075c0f7c93fa2bfdb6bd4aa18 Mon Sep 17 00:00:00 2001 From: Jonas Pleyer <59249415+jonaspleyer@users.noreply.github.com> Date: Fri, 25 Oct 2024 10:18:46 +0200 Subject: [PATCH 335/495] add cellular_raza to examples list (#4646) --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 8e2aa89c0b8..4f89d5a518d 100644 --- a/README.md +++ b/README.md @@ -189,6 +189,7 @@ about this topic. - [ballista-python](https://github.com/apache/arrow-ballista-python) _A Python library that binds to Apache Arrow distributed query engine Ballista._ - [bed-reader](https://github.com/fastlmm/bed-reader) _Read and write the PLINK BED format, simply and efficiently._ - Shows Rayon/ndarray::parallel (including capturing errors, controlling thread num), Python types to Rust generics, Github Actions +- [cellular_raza](https://cellular-raza.com) A cellular agent-based simulation framework for building complex models from a clean slate._ - [connector-x](https://github.com/sfu-db/connector-x) _Fastest library to load data from DB to DataFrames in Rust and Python._ - [cryptography](https://github.com/pyca/cryptography/tree/main/src/rust) _Python cryptography library with some functionality in Rust._ - [css-inline](https://github.com/Stranger6667/css-inline/tree/master/bindings/python) _CSS inlining for Python implemented in Rust._ From b3bb6674d66e296d6b9c536d78527ac19ac28d6e Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Fri, 25 Oct 2024 11:09:18 +0200 Subject: [PATCH 336/495] fix `#[derive(FromPyObject)]` expansion with trait bounds (#4645) * fix `#[derive(FromPyObject)]` expansion with trait bounds * add newsfragment --- newsfragments/4645.fixed.md | 1 + pyo3-macros-backend/src/frompyobject.rs | 21 ++++++++++++--------- tests/test_frompyobject.rs | 17 ++++++++++++++++- 3 files changed, 29 insertions(+), 10 deletions(-) create mode 100644 newsfragments/4645.fixed.md diff --git a/newsfragments/4645.fixed.md b/newsfragments/4645.fixed.md new file mode 100644 index 00000000000..ec4352d6693 --- /dev/null +++ b/newsfragments/4645.fixed.md @@ -0,0 +1 @@ +fix `#[derive(FromPyObject)]` expansion on generic with trait bounds \ No newline at end of file diff --git a/pyo3-macros-backend/src/frompyobject.rs b/pyo3-macros-backend/src/frompyobject.rs index a20eeec9ffd..14c8755e9be 100644 --- a/pyo3-macros-backend/src/frompyobject.rs +++ b/pyo3-macros-backend/src/frompyobject.rs @@ -572,24 +572,27 @@ fn verify_and_get_lifetime(generics: &syn::Generics) -> Result Foo(T)` /// adds `T: FromPyObject` on the derived implementation. pub fn build_derive_from_pyobject(tokens: &DeriveInput) -> Result { + let options = ContainerOptions::from_attrs(&tokens.attrs)?; + let ctx = &Ctx::new(&options.krate, None); + let Ctx { pyo3_path, .. } = &ctx; + + let (_, ty_generics, _) = tokens.generics.split_for_impl(); let mut trait_generics = tokens.generics.clone(); - let generics = &tokens.generics; - let lt_param = if let Some(lt) = verify_and_get_lifetime(generics)? { + let lt_param = if let Some(lt) = verify_and_get_lifetime(&trait_generics)? { lt.clone() } else { trait_generics.params.push(parse_quote!('py)); parse_quote!('py) }; - let mut where_clause: syn::WhereClause = parse_quote!(where); - for param in generics.type_params() { + let (impl_generics, _, where_clause) = trait_generics.split_for_impl(); + + let mut where_clause = where_clause.cloned().unwrap_or_else(|| parse_quote!(where)); + for param in trait_generics.type_params() { let gen_ident = ¶m.ident; where_clause .predicates - .push(parse_quote!(#gen_ident: FromPyObject<#lt_param>)) + .push(parse_quote!(#gen_ident: #pyo3_path::FromPyObject<'py>)) } - let options = ContainerOptions::from_attrs(&tokens.attrs)?; - let ctx = &Ctx::new(&options.krate, None); - let Ctx { pyo3_path, .. } = &ctx; let derives = match &tokens.data { syn::Data::Enum(en) => { @@ -616,7 +619,7 @@ pub fn build_derive_from_pyobject(tokens: &DeriveInput) -> Result { let ident = &tokens.ident; Ok(quote!( #[automatically_derived] - impl #trait_generics #pyo3_path::FromPyObject<#lt_param> for #ident #generics #where_clause { + impl #impl_generics #pyo3_path::FromPyObject<#lt_param> for #ident #ty_generics #where_clause { fn extract_bound(obj: &#pyo3_path::Bound<#lt_param, #pyo3_path::PyAny>) -> #pyo3_path::PyResult { #derives } diff --git a/tests/test_frompyobject.rs b/tests/test_frompyobject.rs index a1b91c25128..6093b774733 100644 --- a/tests/test_frompyobject.rs +++ b/tests/test_frompyobject.rs @@ -2,7 +2,7 @@ use pyo3::exceptions::PyValueError; use pyo3::prelude::*; -use pyo3::types::{PyDict, PyList, PyString, PyTuple}; +use pyo3::types::{IntoPyDict, PyDict, PyList, PyString, PyTuple}; #[macro_use] #[path = "../src/tests/common.rs"] @@ -109,6 +109,21 @@ fn test_generic_transparent_named_field_struct() { }); } +#[derive(Debug, FromPyObject)] +pub struct GenericWithBound(std::collections::HashMap); + +#[test] +fn test_generic_with_bound() { + Python::with_gil(|py| { + let dict = [("1", 1), ("2", 2)].into_py_dict(py).unwrap(); + let map = dict.extract::>().unwrap().0; + assert_eq!(map.len(), 2); + assert_eq!(map["1"], 1); + assert_eq!(map["2"], 2); + assert!(!map.contains_key("3")); + }); +} + #[derive(Debug, FromPyObject)] pub struct E { test: T, From 9ac89a92ca4ec98a58a5891875e8b7ab3c48db7d Mon Sep 17 00:00:00 2001 From: Jonas Pleyer <59249415+jonaspleyer@users.noreply.github.com> Date: Fri, 25 Oct 2024 14:32:56 +0200 Subject: [PATCH 337/495] fixup! add cellular_raza to examples list (#4646) (#4648) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4f89d5a518d..b5225dbfe16 100644 --- a/README.md +++ b/README.md @@ -189,7 +189,7 @@ about this topic. - [ballista-python](https://github.com/apache/arrow-ballista-python) _A Python library that binds to Apache Arrow distributed query engine Ballista._ - [bed-reader](https://github.com/fastlmm/bed-reader) _Read and write the PLINK BED format, simply and efficiently._ - Shows Rayon/ndarray::parallel (including capturing errors, controlling thread num), Python types to Rust generics, Github Actions -- [cellular_raza](https://cellular-raza.com) A cellular agent-based simulation framework for building complex models from a clean slate._ +- [cellular_raza](https://cellular-raza.com) _A cellular agent-based simulation framework for building complex models from a clean slate._ - [connector-x](https://github.com/sfu-db/connector-x) _Fastest library to load data from DB to DataFrames in Rust and Python._ - [cryptography](https://github.com/pyca/cryptography/tree/main/src/rust) _Python cryptography library with some functionality in Rust._ - [css-inline](https://github.com/Stranger6667/css-inline/tree/master/bindings/python) _CSS inlining for Python implemented in Rust._ From 5266a72d63a1c77321631527921fbbdd580bd72d Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Fri, 25 Oct 2024 22:06:51 +0100 Subject: [PATCH 338/495] refactor `PyErr` state to reduce blast radius of threading challenge (#4650) * refactor `PyErr` state to reduce blast radius of threading challenge * restrict visibility --- src/err/err_state.rs | 187 ++++++++++++++++++++++++++++++------------- src/err/mod.rs | 96 +++++++--------------- 2 files changed, 162 insertions(+), 121 deletions(-) diff --git a/src/err/err_state.rs b/src/err/err_state.rs index dc07294a0fa..58303b46f32 100644 --- a/src/err/err_state.rs +++ b/src/err/err_state.rs @@ -1,10 +1,109 @@ +use std::cell::UnsafeCell; + use crate::{ exceptions::{PyBaseException, PyTypeError}, ffi, types::{PyTraceback, PyType}, - Bound, IntoPy, Py, PyAny, PyObject, PyTypeInfo, Python, + Bound, Py, PyAny, PyErrArguments, PyObject, PyTypeInfo, Python, }; +pub(crate) struct PyErrState { + // Safety: can only hand out references when in the "normalized" state. Will never change + // after normalization. + // + // The state is temporarily removed from the PyErr during normalization, to avoid + // concurrent modifications. + inner: UnsafeCell>, +} + +// The inner value is only accessed through ways that require the gil is held. +unsafe impl Send for PyErrState {} +unsafe impl Sync for PyErrState {} + +impl PyErrState { + pub(crate) fn lazy(f: Box) -> Self { + Self::from_inner(PyErrStateInner::Lazy(f)) + } + + pub(crate) fn lazy_arguments(ptype: Py, args: impl PyErrArguments + 'static) -> Self { + Self::from_inner(PyErrStateInner::Lazy(Box::new(move |py| { + PyErrStateLazyFnOutput { + ptype, + pvalue: args.arguments(py), + } + }))) + } + + #[cfg(not(Py_3_12))] + pub(crate) fn ffi_tuple( + ptype: PyObject, + pvalue: Option, + ptraceback: Option, + ) -> Self { + Self::from_inner(PyErrStateInner::FfiTuple { + ptype, + pvalue, + ptraceback, + }) + } + + pub(crate) fn normalized(normalized: PyErrStateNormalized) -> Self { + Self::from_inner(PyErrStateInner::Normalized(normalized)) + } + + pub(crate) fn restore(self, py: Python<'_>) { + self.inner + .into_inner() + .expect("PyErr state should never be invalid outside of normalization") + .restore(py) + } + + fn from_inner(inner: PyErrStateInner) -> Self { + Self { + inner: UnsafeCell::new(Some(inner)), + } + } + + #[inline] + pub(crate) fn as_normalized(&self, py: Python<'_>) -> &PyErrStateNormalized { + if let Some(PyErrStateInner::Normalized(n)) = unsafe { + // Safety: self.inner will never be written again once normalized. + &*self.inner.get() + } { + return n; + } + + self.make_normalized(py) + } + + #[cold] + fn make_normalized(&self, py: Python<'_>) -> &PyErrStateNormalized { + // This process is safe because: + // - Access is guaranteed not to be concurrent thanks to `Python` GIL token + // - Write happens only once, and then never will change again. + // - State is set to None during the normalization process, so that a second + // concurrent normalization attempt will panic before changing anything. + + // FIXME: this needs to be rewritten to deal with free-threaded Python + // see https://github.com/PyO3/pyo3/issues/4584 + + let state = unsafe { + (*self.inner.get()) + .take() + .expect("Cannot normalize a PyErr while already normalizing it.") + }; + + unsafe { + let self_state = &mut *self.inner.get(); + *self_state = Some(PyErrStateInner::Normalized(state.normalize(py))); + match self_state { + Some(PyErrStateInner::Normalized(n)) => n, + _ => unreachable!(), + } + } + } +} + pub(crate) struct PyErrStateNormalized { #[cfg(not(Py_3_12))] ptype: Py, @@ -14,6 +113,24 @@ pub(crate) struct PyErrStateNormalized { } impl PyErrStateNormalized { + pub(crate) fn new(pvalue: Bound<'_, PyBaseException>) -> Self { + #[cfg(not(Py_3_12))] + use crate::types::any::PyAnyMethods; + + Self { + #[cfg(not(Py_3_12))] + ptype: pvalue.get_type().into(), + #[cfg(not(Py_3_12))] + ptraceback: unsafe { + Py::from_owned_ptr_or_opt( + pvalue.py(), + ffi::PyException_GetTraceback(pvalue.as_ptr()), + ) + }, + pvalue: pvalue.into(), + } + } + #[cfg(not(Py_3_12))] pub(crate) fn ptype<'py>(&self, py: Python<'py>) -> Bound<'py, PyType> { self.ptype.bind(py).clone() @@ -85,7 +202,7 @@ pub(crate) struct PyErrStateLazyFnOutput { pub(crate) type PyErrStateLazyFn = dyn for<'py> FnOnce(Python<'py>) -> PyErrStateLazyFnOutput + Send + Sync; -pub(crate) enum PyErrState { +enum PyErrStateInner { Lazy(Box), #[cfg(not(Py_3_12))] FfiTuple { @@ -96,58 +213,18 @@ pub(crate) enum PyErrState { Normalized(PyErrStateNormalized), } -/// Helper conversion trait that allows to use custom arguments for lazy exception construction. -pub trait PyErrArguments: Send + Sync { - /// Arguments for exception - fn arguments(self, py: Python<'_>) -> PyObject; -} - -impl PyErrArguments for T -where - T: IntoPy + Send + Sync, -{ - fn arguments(self, py: Python<'_>) -> PyObject { - self.into_py(py) - } -} - -impl PyErrState { - pub(crate) fn lazy(ptype: Py, args: impl PyErrArguments + 'static) -> Self { - PyErrState::Lazy(Box::new(move |py| PyErrStateLazyFnOutput { - ptype, - pvalue: args.arguments(py), - })) - } - - pub(crate) fn normalized(pvalue: Bound<'_, PyBaseException>) -> Self { - #[cfg(not(Py_3_12))] - use crate::types::any::PyAnyMethods; - - Self::Normalized(PyErrStateNormalized { - #[cfg(not(Py_3_12))] - ptype: pvalue.get_type().into(), - #[cfg(not(Py_3_12))] - ptraceback: unsafe { - Py::from_owned_ptr_or_opt( - pvalue.py(), - ffi::PyException_GetTraceback(pvalue.as_ptr()), - ) - }, - pvalue: pvalue.into(), - }) - } - - pub(crate) fn normalize(self, py: Python<'_>) -> PyErrStateNormalized { +impl PyErrStateInner { + fn normalize(self, py: Python<'_>) -> PyErrStateNormalized { match self { #[cfg(not(Py_3_12))] - PyErrState::Lazy(lazy) => { + PyErrStateInner::Lazy(lazy) => { let (ptype, pvalue, ptraceback) = lazy_into_normalized_ffi_tuple(py, lazy); unsafe { PyErrStateNormalized::from_normalized_ffi_tuple(py, ptype, pvalue, ptraceback) } } #[cfg(Py_3_12)] - PyErrState::Lazy(lazy) => { + PyErrStateInner::Lazy(lazy) => { // To keep the implementation simple, just write the exception into the interpreter, // which will cause it to be normalized raise_lazy(py, lazy); @@ -155,7 +232,7 @@ impl PyErrState { .expect("exception missing after writing to the interpreter") } #[cfg(not(Py_3_12))] - PyErrState::FfiTuple { + PyErrStateInner::FfiTuple { ptype, pvalue, ptraceback, @@ -168,15 +245,15 @@ impl PyErrState { PyErrStateNormalized::from_normalized_ffi_tuple(py, ptype, pvalue, ptraceback) } } - PyErrState::Normalized(normalized) => normalized, + PyErrStateInner::Normalized(normalized) => normalized, } } #[cfg(not(Py_3_12))] - pub(crate) fn restore(self, py: Python<'_>) { + fn restore(self, py: Python<'_>) { let (ptype, pvalue, ptraceback) = match self { - PyErrState::Lazy(lazy) => lazy_into_normalized_ffi_tuple(py, lazy), - PyErrState::FfiTuple { + PyErrStateInner::Lazy(lazy) => lazy_into_normalized_ffi_tuple(py, lazy), + PyErrStateInner::FfiTuple { ptype, pvalue, ptraceback, @@ -185,7 +262,7 @@ impl PyErrState { pvalue.map_or(std::ptr::null_mut(), Py::into_ptr), ptraceback.map_or(std::ptr::null_mut(), Py::into_ptr), ), - PyErrState::Normalized(PyErrStateNormalized { + PyErrStateInner::Normalized(PyErrStateNormalized { ptype, pvalue, ptraceback, @@ -199,10 +276,10 @@ impl PyErrState { } #[cfg(Py_3_12)] - pub(crate) fn restore(self, py: Python<'_>) { + fn restore(self, py: Python<'_>) { match self { - PyErrState::Lazy(lazy) => raise_lazy(py, lazy), - PyErrState::Normalized(PyErrStateNormalized { pvalue }) => unsafe { + PyErrStateInner::Lazy(lazy) => raise_lazy(py, lazy), + PyErrStateInner::Normalized(PyErrStateNormalized { pvalue }) => unsafe { ffi::PyErr_SetRaisedException(pvalue.into_ptr()) }, } diff --git a/src/err/mod.rs b/src/err/mod.rs index 59d99f72c06..14c368938f1 100644 --- a/src/err/mod.rs +++ b/src/err/mod.rs @@ -11,14 +11,12 @@ use crate::{ }; use crate::{Borrowed, BoundObject, IntoPy, Py, PyAny, PyObject, Python}; use std::borrow::Cow; -use std::cell::UnsafeCell; use std::ffi::{CStr, CString}; mod err_state; mod impls; use crate::conversion::IntoPyObject; -pub use err_state::PyErrArguments; use err_state::{PyErrState, PyErrStateLazyFnOutput, PyErrStateNormalized}; use std::convert::Infallible; @@ -32,19 +30,12 @@ use std::convert::Infallible; /// [`get_type_bound`](PyErr::get_type_bound), or [`is_instance_bound`](PyErr::is_instance_bound) /// will create the full exception object if it was not already created. pub struct PyErr { - // Safety: can only hand out references when in the "normalized" state. Will never change - // after normalization. - // - // The state is temporarily removed from the PyErr during normalization, to avoid - // concurrent modifications. - state: UnsafeCell>, + state: PyErrState, } // The inner value is only accessed through ways that require proving the gil is held #[cfg(feature = "nightly")] unsafe impl crate::marker::Ungil for PyErr {} -unsafe impl Send for PyErr {} -unsafe impl Sync for PyErr {} /// Represents the result of a Python call. pub type PyResult = Result; @@ -102,6 +93,21 @@ impl<'py> DowncastIntoError<'py> { } } +/// Helper conversion trait that allows to use custom arguments for lazy exception construction. +pub trait PyErrArguments: Send + Sync { + /// Arguments for exception + fn arguments(self, py: Python<'_>) -> PyObject; +} + +impl PyErrArguments for T +where + T: IntoPy + Send + Sync, +{ + fn arguments(self, py: Python<'_>) -> PyObject { + self.into_py(py) + } +} + impl PyErr { /// Creates a new PyErr of type `T`. /// @@ -160,7 +166,7 @@ impl PyErr { T: PyTypeInfo, A: PyErrArguments + Send + Sync + 'static, { - PyErr::from_state(PyErrState::Lazy(Box::new(move |py| { + PyErr::from_state(PyErrState::lazy(Box::new(move |py| { PyErrStateLazyFnOutput { ptype: T::type_object(py).into(), pvalue: args.arguments(py), @@ -182,7 +188,7 @@ impl PyErr { where A: PyErrArguments + Send + Sync + 'static, { - PyErr::from_state(PyErrState::lazy(ty.unbind().into_any(), args)) + PyErr::from_state(PyErrState::lazy_arguments(ty.unbind().into_any(), args)) } /// Deprecated name for [`PyErr::from_type`]. @@ -230,13 +236,13 @@ impl PyErr { /// ``` pub fn from_value(obj: Bound<'_, PyAny>) -> PyErr { let state = match obj.downcast_into::() { - Ok(obj) => PyErrState::normalized(obj), + Ok(obj) => PyErrState::normalized(PyErrStateNormalized::new(obj)), Err(err) => { // Assume obj is Type[Exception]; let later normalization handle if this // is not the case let obj = err.into_inner(); let py = obj.py(); - PyErrState::lazy(obj.into_py(py), py.None()) + PyErrState::lazy_arguments(obj.into_py(py), py.None()) } }; @@ -392,19 +398,13 @@ impl PyErr { .map(|py_str| py_str.to_string_lossy().into()) .unwrap_or_else(|| String::from("Unwrapped panic from Python code")); - let state = PyErrState::FfiTuple { - ptype, - pvalue, - ptraceback, - }; + let state = PyErrState::ffi_tuple(ptype, pvalue, ptraceback); Self::print_panic_and_unwind(py, state, msg) } - Some(PyErr::from_state(PyErrState::FfiTuple { - ptype, - pvalue, - ptraceback, - })) + Some(PyErr::from_state(PyErrState::ffi_tuple( + ptype, pvalue, ptraceback, + ))) } #[cfg(Py_3_12)] @@ -416,10 +416,10 @@ impl PyErr { .str() .map(|py_str| py_str.to_string_lossy().into()) .unwrap_or_else(|_| String::from("Unwrapped panic from Python code")); - Self::print_panic_and_unwind(py, PyErrState::Normalized(state), msg) + Self::print_panic_and_unwind(py, PyErrState::normalized(state), msg) } - Some(PyErr::from_state(PyErrState::Normalized(state))) + Some(PyErr::from_state(PyErrState::normalized(state))) } fn print_panic_and_unwind(py: Python<'_>, state: PyErrState, msg: String) -> ! { @@ -596,10 +596,7 @@ impl PyErr { /// This is the opposite of `PyErr::fetch()`. #[inline] pub fn restore(self, py: Python<'_>) { - self.state - .into_inner() - .expect("PyErr state should never be invalid outside of normalization") - .restore(py) + self.state.restore(py) } /// Reports the error as unraisable. @@ -774,7 +771,7 @@ impl PyErr { /// ``` #[inline] pub fn clone_ref(&self, py: Python<'_>) -> PyErr { - PyErr::from_state(PyErrState::Normalized(self.normalized(py).clone_ref(py))) + PyErr::from_state(PyErrState::normalized(self.normalized(py).clone_ref(py))) } /// Return the cause (either an exception instance, or None, set by `raise ... from ...`) @@ -808,45 +805,12 @@ impl PyErr { #[inline] fn from_state(state: PyErrState) -> PyErr { - PyErr { - state: UnsafeCell::new(Some(state)), - } + PyErr { state } } #[inline] fn normalized(&self, py: Python<'_>) -> &PyErrStateNormalized { - if let Some(PyErrState::Normalized(n)) = unsafe { - // Safety: self.state will never be written again once normalized. - &*self.state.get() - } { - return n; - } - - self.make_normalized(py) - } - - #[cold] - fn make_normalized(&self, py: Python<'_>) -> &PyErrStateNormalized { - // This process is safe because: - // - Access is guaranteed not to be concurrent thanks to `Python` GIL token - // - Write happens only once, and then never will change again. - // - State is set to None during the normalization process, so that a second - // concurrent normalization attempt will panic before changing anything. - - let state = unsafe { - (*self.state.get()) - .take() - .expect("Cannot normalize a PyErr while already normalizing it.") - }; - - unsafe { - let self_state = &mut *self.state.get(); - *self_state = Some(PyErrState::Normalized(state.normalize(py))); - match self_state { - Some(PyErrState::Normalized(n)) => n, - _ => unreachable!(), - } - } + self.state.as_normalized(py) } } From c8eb45bea6ce44b077109249e01c4c04d6db680a Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Sat, 26 Oct 2024 06:10:36 +0200 Subject: [PATCH 339/495] Derive macro for `IntoPyObject` (#4495) * initial `IntoPyObject` derive (transparent newtype structs) * derive intopyobject for structs and tuple structs * support lifetimes * support enums * fix spans * compile error tests * add some tests * add hygiene tests * add newsfragment * don't drop additional trait bound from the struct/enum definition * add migration guide entry * fix typo Co-authored-by: David Hewitt * improve unit variant spans * update gudie * apply doc suggestions Co-authored-by: David Hewitt * fix tuple expansion * parse `pyo3(item)` and `pyo3(attribute)` --------- Co-authored-by: David Hewitt --- guide/src/conversions/traits.md | 118 +++++ guide/src/migration.md | 19 +- newsfragments/4495.added.md | 1 + pyo3-macros-backend/src/intopyobject.rs | 587 ++++++++++++++++++++++++ pyo3-macros-backend/src/lib.rs | 2 + pyo3-macros/src/lib.rs | 16 +- src/lib.rs | 2 +- src/prelude.rs | 2 +- src/tests/hygiene/misc.rs | 36 ++ tests/test_compile_error.rs | 1 + tests/test_frompy_intopy_roundtrip.rs | 180 ++++++++ tests/test_intopyobject.rs | 201 ++++++++ tests/ui/invalid_intopy_derive.rs | 109 +++++ tests/ui/invalid_intopy_derive.stderr | 127 +++++ 14 files changed, 1395 insertions(+), 6 deletions(-) create mode 100644 newsfragments/4495.added.md create mode 100644 pyo3-macros-backend/src/intopyobject.rs create mode 100644 tests/test_frompy_intopy_roundtrip.rs create mode 100644 tests/test_intopyobject.rs create mode 100644 tests/ui/invalid_intopy_derive.rs create mode 100644 tests/ui/invalid_intopy_derive.stderr diff --git a/guide/src/conversions/traits.md b/guide/src/conversions/traits.md index 9f163df9ed2..def32ecc614 100644 --- a/guide/src/conversions/traits.md +++ b/guide/src/conversions/traits.md @@ -489,8 +489,118 @@ If the input is neither a string nor an integer, the error message will be: - the argument must be the name of the function as a string. - the function signature must be `fn(&Bound) -> PyResult` where `T` is the Rust type of the argument. +### `IntoPyObject` +This trait defines the to-python conversion for a Rust type. All types in PyO3 implement this trait, +as does a `#[pyclass]` which doesn't use `extends`. + +Occasionally you may choose to implement this for custom types which are mapped to Python types +_without_ having a unique python type. + +#### derive macro + +`IntoPyObject` can be implemented using our derive macro. Both `struct`s and `enum`s are supported. + +`struct`s will turn into a `PyDict` using the field names as keys, tuple `struct`s will turn convert +into `PyTuple` with the fields in declaration order. +```rust +# #![allow(dead_code)] +# use pyo3::prelude::*; +# use std::collections::HashMap; +# use std::hash::Hash; + +// structs convert into `PyDict` with field names as keys +#[derive(IntoPyObject)] +struct Struct { + count: usize, + obj: Py, +} + +// tuple structs convert into `PyTuple` +// lifetimes and generics are supported, the impl will be bounded by +// `K: IntoPyObject, V: IntoPyObject` +#[derive(IntoPyObject)] +struct Tuple<'a, K: Hash + Eq, V>(&'a str, HashMap); +``` + +For structs with a single field (newtype pattern) the `#[pyo3(transparent)]` option can be used to +forward the implementation to the inner type. + + +```rust +# #![allow(dead_code)] +# use pyo3::prelude::*; + +// newtype tuple structs are implicitly `transparent` +#[derive(IntoPyObject)] +struct TransparentTuple(PyObject); + +#[derive(IntoPyObject)] +#[pyo3(transparent)] +struct TransparentStruct<'py> { + inner: Bound<'py, PyAny>, // `'py` lifetime will be used as the Python lifetime +} +``` + +For `enum`s each variant is converted according to the rules for `struct`s above. + +```rust +# #![allow(dead_code)] +# use pyo3::prelude::*; +# use std::collections::HashMap; +# use std::hash::Hash; + +#[derive(IntoPyObject)] +enum Enum<'a, 'py, K: Hash + Eq, V> { // enums are supported and convert using the same + TransparentTuple(PyObject), // rules on the variants as the structs above + #[pyo3(transparent)] + TransparentStruct { inner: Bound<'py, PyAny> }, + Tuple(&'a str, HashMap), + Struct { count: usize, obj: Py } +} +``` + +#### manual implementation + +If the derive macro is not suitable for your use case, `IntoPyObject` can be implemented manually as +demonstrated below. + +```rust +# use pyo3::prelude::*; +# #[allow(dead_code)] +struct MyPyObjectWrapper(PyObject); + +impl<'py> IntoPyObject<'py> for MyPyObjectWrapper { + type Target = PyAny; // the Python type + type Output = Bound<'py, Self::Target>; // in most cases this will be `Bound` + type Error = std::convert::Infallible; // the conversion error type, has to be convertable to `PyErr` + + fn into_pyobject(self, py: Python<'py>) -> Result { + Ok(self.0.into_bound(py)) + } +} + +// equivalent to former `ToPyObject` implementations +impl<'a, 'py> IntoPyObject<'py> for &'a MyPyObjectWrapper { + type Target = PyAny; + type Output = Borrowed<'a, 'py, Self::Target>; // `Borrowed` can be used to optimized reference counting + type Error = std::convert::Infallible; + + fn into_pyobject(self, py: Python<'py>) -> Result { + Ok(self.0.bind_borrowed(py)) + } +} +``` + ### `IntoPy` +

+ +⚠️ Warning: API update in progress 🛠️ + +PyO3 0.23 has introduced `IntoPyObject` as the new trait for to-python conversions. While `#[pymethods]` and `#[pyfunction]` contain a compatibility layer to allow `IntoPy` as a return type, all Python API have been migrated to use `IntoPyObject`. To migrate implement `IntoPyObject` for your type. +
+ + This trait defines the to-python conversion for a Rust type. It is usually implemented as `IntoPy`, which is the trait needed for returning a value from `#[pyfunction]` and `#[pymethods]`. @@ -514,6 +624,14 @@ impl IntoPy for MyPyObjectWrapper { ### The `ToPyObject` trait +
+ +⚠️ Warning: API update in progress 🛠️ + +PyO3 0.23 has introduced `IntoPyObject` as the new trait for to-python conversions. To migrate +implement `IntoPyObject` on a referece of your type (`impl<'py> IntoPyObject<'py> for &Type { ... }`). +
+ [`ToPyObject`] is a conversion trait that allows various objects to be converted into [`PyObject`]. `IntoPy` serves the same purpose, except that it consumes `self`. diff --git a/guide/src/migration.md b/guide/src/migration.md index ac20408a522..6a5b44a6c00 100644 --- a/guide/src/migration.md +++ b/guide/src/migration.md @@ -153,11 +153,28 @@ Notable features of this new trait: - `()` (unit) is now only special handled in return position and otherwise converts into an empty `PyTuple` All PyO3 provided types as well as `#[pyclass]`es already implement `IntoPyObject`. Other types will -need to adapt an implementation of `IntoPyObject` to stay compatible with the Python APIs. +need to adapt an implementation of `IntoPyObject` to stay compatible with the Python APIs. In many cases +the new [`#[derive(IntoPyObject)]`](#intopyobject-derive-macro) macro can be used instead of +[manual implementations](#intopyobject-manual-implementation). Together with the introduction of `IntoPyObject` the old conversion traits `ToPyObject` and `IntoPy` are deprecated and will be removed in a future PyO3 version. +#### `IntoPyObject` derive macro + +To migrate you may use the new `IntoPyObject` derive macro as below. + +```rust +# use pyo3::prelude::*; +#[derive(IntoPyObject)] +struct Struct { + count: usize, + obj: Py, +} +``` + + +#### `IntoPyObject` manual implementation Before: ```rust,ignore diff --git a/newsfragments/4495.added.md b/newsfragments/4495.added.md new file mode 100644 index 00000000000..2cbe2a85bbf --- /dev/null +++ b/newsfragments/4495.added.md @@ -0,0 +1 @@ +Added `IntoPyObject` derive macro \ No newline at end of file diff --git a/pyo3-macros-backend/src/intopyobject.rs b/pyo3-macros-backend/src/intopyobject.rs new file mode 100644 index 00000000000..3b4b2d376bb --- /dev/null +++ b/pyo3-macros-backend/src/intopyobject.rs @@ -0,0 +1,587 @@ +use crate::attributes::{self, get_pyo3_options, CrateAttribute}; +use crate::utils::Ctx; +use proc_macro2::{Span, TokenStream}; +use quote::{format_ident, quote, quote_spanned}; +use syn::ext::IdentExt; +use syn::parse::{Parse, ParseStream}; +use syn::spanned::Spanned as _; +use syn::{ + parenthesized, parse_quote, Attribute, DataEnum, DeriveInput, Fields, Ident, Index, Result, + Token, +}; + +/// Attributes for deriving `IntoPyObject` scoped on containers. +enum ContainerPyO3Attribute { + /// Treat the Container as a Wrapper, directly convert its field into the output object. + Transparent(attributes::kw::transparent), + /// Change the path for the pyo3 crate + Crate(CrateAttribute), +} + +impl Parse for ContainerPyO3Attribute { + fn parse(input: ParseStream<'_>) -> Result { + let lookahead = input.lookahead1(); + if lookahead.peek(attributes::kw::transparent) { + let kw: attributes::kw::transparent = input.parse()?; + Ok(ContainerPyO3Attribute::Transparent(kw)) + } else if lookahead.peek(Token![crate]) { + input.parse().map(ContainerPyO3Attribute::Crate) + } else { + Err(lookahead.error()) + } + } +} + +#[derive(Default)] +struct ContainerOptions { + /// Treat the Container as a Wrapper, directly convert its field into the output object. + transparent: Option, + /// Change the path for the pyo3 crate + krate: Option, +} + +impl ContainerOptions { + fn from_attrs(attrs: &[Attribute]) -> Result { + let mut options = ContainerOptions::default(); + + for attr in attrs { + if let Some(pyo3_attrs) = get_pyo3_options(attr)? { + pyo3_attrs + .into_iter() + .try_for_each(|opt| options.set_option(opt))?; + } + } + Ok(options) + } + + fn set_option(&mut self, option: ContainerPyO3Attribute) -> syn::Result<()> { + macro_rules! set_option { + ($key:ident) => { + { + ensure_spanned!( + self.$key.is_none(), + $key.span() => concat!("`", stringify!($key), "` may only be specified once") + ); + self.$key = Some($key); + } + }; + } + + match option { + ContainerPyO3Attribute::Transparent(transparent) => set_option!(transparent), + ContainerPyO3Attribute::Crate(krate) => set_option!(krate), + } + Ok(()) + } +} + +#[derive(Debug, Clone)] +struct ItemOption { + field: Option, + span: Span, +} + +impl ItemOption { + fn span(&self) -> Span { + self.span + } +} + +enum FieldAttribute { + Item(ItemOption), +} + +impl Parse for FieldAttribute { + fn parse(input: ParseStream<'_>) -> Result { + let lookahead = input.lookahead1(); + if lookahead.peek(attributes::kw::attribute) { + let attr: attributes::kw::attribute = input.parse()?; + bail_spanned!(attr.span => "`attribute` is not supported by `IntoPyObject`"); + } else if lookahead.peek(attributes::kw::item) { + let attr: attributes::kw::item = input.parse()?; + if input.peek(syn::token::Paren) { + let content; + let _ = parenthesized!(content in input); + let key = content.parse()?; + if !content.is_empty() { + return Err( + content.error("expected at most one argument: `item` or `item(key)`") + ); + } + Ok(FieldAttribute::Item(ItemOption { + field: Some(key), + span: attr.span, + })) + } else { + Ok(FieldAttribute::Item(ItemOption { + field: None, + span: attr.span, + })) + } + } else { + Err(lookahead.error()) + } + } +} + +#[derive(Clone, Debug, Default)] +struct FieldAttributes { + item: Option, +} + +impl FieldAttributes { + /// Extract the field attributes. + fn from_attrs(attrs: &[Attribute]) -> Result { + let mut options = FieldAttributes::default(); + + for attr in attrs { + if let Some(pyo3_attrs) = get_pyo3_options(attr)? { + pyo3_attrs + .into_iter() + .try_for_each(|opt| options.set_option(opt))?; + } + } + Ok(options) + } + + fn set_option(&mut self, option: FieldAttribute) -> syn::Result<()> { + macro_rules! set_option { + ($key:ident) => { + { + ensure_spanned!( + self.$key.is_none(), + $key.span() => concat!("`", stringify!($key), "` may only be specified once") + ); + self.$key = Some($key); + } + }; + } + + match option { + FieldAttribute::Item(item) => set_option!(item), + } + Ok(()) + } +} + +struct IntoPyObjectImpl { + target: TokenStream, + output: TokenStream, + error: TokenStream, + body: TokenStream, +} + +struct NamedStructField<'a> { + ident: &'a syn::Ident, + field: &'a syn::Field, + item: Option, +} + +struct TupleStructField<'a> { + field: &'a syn::Field, +} + +/// Container Style +/// +/// Covers Structs, Tuplestructs and corresponding Newtypes. +enum ContainerType<'a> { + /// Struct Container, e.g. `struct Foo { a: String }` + /// + /// Variant contains the list of field identifiers and the corresponding extraction call. + Struct(Vec>), + /// Newtype struct container, e.g. `#[transparent] struct Foo { a: String }` + /// + /// The field specified by the identifier is extracted directly from the object. + StructNewtype(&'a syn::Field), + /// Tuple struct, e.g. `struct Foo(String)`. + /// + /// Variant contains a list of conversion methods for each of the fields that are directly + /// extracted from the tuple. + Tuple(Vec>), + /// Tuple newtype, e.g. `#[transparent] struct Foo(String)` + /// + /// The wrapped field is directly extracted from the object. + TupleNewtype(&'a syn::Field), +} + +/// Data container +/// +/// Either describes a struct or an enum variant. +struct Container<'a> { + path: syn::Path, + receiver: Option, + ty: ContainerType<'a>, +} + +/// Construct a container based on fields, identifier and attributes. +impl<'a> Container<'a> { + /// + /// Fails if the variant has no fields or incompatible attributes. + fn new( + receiver: Option, + fields: &'a Fields, + path: syn::Path, + options: ContainerOptions, + ) -> Result { + let style = match fields { + Fields::Unnamed(unnamed) if !unnamed.unnamed.is_empty() => { + let mut tuple_fields = unnamed + .unnamed + .iter() + .map(|field| { + let attrs = FieldAttributes::from_attrs(&field.attrs)?; + ensure_spanned!( + attrs.item.is_none(), + attrs.item.unwrap().span() => "`item` is not permitted on tuple struct elements." + ); + Ok(TupleStructField { field }) + }) + .collect::>>()?; + if tuple_fields.len() == 1 { + // Always treat a 1-length tuple struct as "transparent", even without the + // explicit annotation. + let TupleStructField { field } = tuple_fields.pop().unwrap(); + ContainerType::TupleNewtype(field) + } else if options.transparent.is_some() { + bail_spanned!( + fields.span() => "transparent structs and variants can only have 1 field" + ); + } else { + ContainerType::Tuple(tuple_fields) + } + } + Fields::Named(named) if !named.named.is_empty() => { + if options.transparent.is_some() { + ensure_spanned!( + named.named.iter().count() == 1, + fields.span() => "transparent structs and variants can only have 1 field" + ); + + let field = named.named.iter().next().unwrap(); + let attrs = FieldAttributes::from_attrs(&field.attrs)?; + ensure_spanned!( + attrs.item.is_none(), + attrs.item.unwrap().span() => "`transparent` structs may not have `item` for the inner field" + ); + ContainerType::StructNewtype(field) + } else { + let struct_fields = named + .named + .iter() + .map(|field| { + let ident = field + .ident + .as_ref() + .expect("Named fields should have identifiers"); + + let attrs = FieldAttributes::from_attrs(&field.attrs)?; + + Ok(NamedStructField { + ident, + field, + item: attrs.item, + }) + }) + .collect::>>()?; + ContainerType::Struct(struct_fields) + } + } + _ => bail_spanned!( + fields.span() => "cannot derive `IntoPyObject` for empty structs" + ), + }; + + let v = Container { + path, + receiver, + ty: style, + }; + Ok(v) + } + + fn match_pattern(&self) -> TokenStream { + let path = &self.path; + let pattern = match &self.ty { + ContainerType::Struct(fields) => fields + .iter() + .enumerate() + .map(|(i, f)| { + let ident = f.ident; + let new_ident = format_ident!("arg{i}"); + quote! {#ident: #new_ident,} + }) + .collect::(), + ContainerType::StructNewtype(field) => { + let ident = field.ident.as_ref().unwrap(); + quote!(#ident: arg0) + } + ContainerType::Tuple(fields) => { + let i = (0..fields.len()).map(Index::from); + let idents = (0..fields.len()).map(|i| format_ident!("arg{i}")); + quote! { #(#i: #idents,)* } + } + ContainerType::TupleNewtype(_) => quote!(0: arg0), + }; + + quote! { #path{ #pattern } } + } + + /// Build derivation body for a struct. + fn build(&self, ctx: &Ctx) -> IntoPyObjectImpl { + match &self.ty { + ContainerType::StructNewtype(field) | ContainerType::TupleNewtype(field) => { + self.build_newtype_struct(field, ctx) + } + ContainerType::Tuple(fields) => self.build_tuple_struct(fields, ctx), + ContainerType::Struct(fields) => self.build_struct(fields, ctx), + } + } + + fn build_newtype_struct(&self, field: &syn::Field, ctx: &Ctx) -> IntoPyObjectImpl { + let Ctx { pyo3_path, .. } = ctx; + let ty = &field.ty; + + let unpack = self + .receiver + .as_ref() + .map(|i| { + let pattern = self.match_pattern(); + quote! { let #pattern = #i;} + }) + .unwrap_or_default(); + + IntoPyObjectImpl { + target: quote! {<#ty as #pyo3_path::conversion::IntoPyObject<'py>>::Target}, + output: quote! {<#ty as #pyo3_path::conversion::IntoPyObject<'py>>::Output}, + error: quote! {<#ty as #pyo3_path::conversion::IntoPyObject<'py>>::Error}, + body: quote_spanned! { ty.span() => + #unpack + <#ty as #pyo3_path::conversion::IntoPyObject<'py>>::into_pyobject(arg0, py) + }, + } + } + + fn build_struct(&self, fields: &[NamedStructField<'_>], ctx: &Ctx) -> IntoPyObjectImpl { + let Ctx { pyo3_path, .. } = ctx; + + let unpack = self + .receiver + .as_ref() + .map(|i| { + let pattern = self.match_pattern(); + quote! { let #pattern = #i;} + }) + .unwrap_or_default(); + + let setter = fields + .iter() + .enumerate() + .map(|(i, f)| { + let key = f + .item + .as_ref() + .and_then(|item| item.field.as_ref()) + .map(|item| item.value()) + .unwrap_or_else(|| f.ident.unraw().to_string()); + let value = Ident::new(&format!("arg{i}"), f.field.ty.span()); + quote! { + #pyo3_path::types::PyDictMethods::set_item(&dict, #key, #value)?; + } + }) + .collect::(); + + IntoPyObjectImpl { + target: quote!(#pyo3_path::types::PyDict), + output: quote!(#pyo3_path::Bound<'py, Self::Target>), + error: quote!(#pyo3_path::PyErr), + body: quote! { + #unpack + let dict = #pyo3_path::types::PyDict::new(py); + #setter + ::std::result::Result::Ok::<_, Self::Error>(dict) + }, + } + } + + fn build_tuple_struct(&self, fields: &[TupleStructField<'_>], ctx: &Ctx) -> IntoPyObjectImpl { + let Ctx { pyo3_path, .. } = ctx; + + let unpack = self + .receiver + .as_ref() + .map(|i| { + let pattern = self.match_pattern(); + quote! { let #pattern = #i;} + }) + .unwrap_or_default(); + + let setter = fields + .iter() + .enumerate() + .map(|(i, f)| { + let ty = &f.field.ty; + let value = Ident::new(&format!("arg{i}"), f.field.ty.span()); + quote_spanned! { f.field.ty.span() => + <#ty as #pyo3_path::conversion::IntoPyObject>::into_pyobject(#value, py) + .map(#pyo3_path::BoundObject::into_any) + .map(#pyo3_path::BoundObject::into_bound)?, + } + }) + .collect::(); + + IntoPyObjectImpl { + target: quote!(#pyo3_path::types::PyTuple), + output: quote!(#pyo3_path::Bound<'py, Self::Target>), + error: quote!(#pyo3_path::PyErr), + body: quote! { + #unpack + #pyo3_path::types::PyTuple::new(py, [#setter]) + }, + } + } +} + +/// Describes derivation input of an enum. +struct Enum<'a> { + variants: Vec>, +} + +impl<'a> Enum<'a> { + /// Construct a new enum representation. + /// + /// `data_enum` is the `syn` representation of the input enum, `ident` is the + /// `Identifier` of the enum. + fn new(data_enum: &'a DataEnum, ident: &'a Ident) -> Result { + ensure_spanned!( + !data_enum.variants.is_empty(), + ident.span() => "cannot derive `IntoPyObject` for empty enum" + ); + let variants = data_enum + .variants + .iter() + .map(|variant| { + let attrs = ContainerOptions::from_attrs(&variant.attrs)?; + let var_ident = &variant.ident; + + ensure_spanned!( + !variant.fields.is_empty(), + variant.ident.span() => "cannot derive `IntoPyObject` for empty variants" + ); + + Container::new( + None, + &variant.fields, + parse_quote!(#ident::#var_ident), + attrs, + ) + }) + .collect::>>()?; + + Ok(Enum { variants }) + } + + /// Build derivation body for enums. + fn build(&self, ctx: &Ctx) -> IntoPyObjectImpl { + let Ctx { pyo3_path, .. } = ctx; + + let variants = self + .variants + .iter() + .map(|v| { + let IntoPyObjectImpl { body, .. } = v.build(ctx); + let pattern = v.match_pattern(); + quote! { + #pattern => { + {#body} + .map(#pyo3_path::BoundObject::into_any) + .map(#pyo3_path::BoundObject::into_bound) + .map_err(::std::convert::Into::<#pyo3_path::PyErr>::into) + } + } + }) + .collect::(); + + IntoPyObjectImpl { + target: quote!(#pyo3_path::types::PyAny), + output: quote!(#pyo3_path::Bound<'py, Self::Target>), + error: quote!(#pyo3_path::PyErr), + body: quote! { + match self { + #variants + } + }, + } + } +} + +// if there is a `'py` lifetime, we treat it as the `Python<'py>` lifetime +fn verify_and_get_lifetime(generics: &syn::Generics) -> Option<&syn::LifetimeParam> { + let mut lifetimes = generics.lifetimes(); + lifetimes.find(|l| l.lifetime.ident == "py") +} + +pub fn build_derive_into_pyobject(tokens: &DeriveInput) -> Result { + let options = ContainerOptions::from_attrs(&tokens.attrs)?; + let ctx = &Ctx::new(&options.krate, None); + let Ctx { pyo3_path, .. } = &ctx; + + let (_, ty_generics, _) = tokens.generics.split_for_impl(); + let mut trait_generics = tokens.generics.clone(); + let lt_param = if let Some(lt) = verify_and_get_lifetime(&trait_generics) { + lt.clone() + } else { + trait_generics.params.push(parse_quote!('py)); + parse_quote!('py) + }; + let (impl_generics, _, where_clause) = trait_generics.split_for_impl(); + + let mut where_clause = where_clause.cloned().unwrap_or_else(|| parse_quote!(where)); + for param in trait_generics.type_params() { + let gen_ident = ¶m.ident; + where_clause + .predicates + .push(parse_quote!(#gen_ident: #pyo3_path::conversion::IntoPyObject<'py>)) + } + + let IntoPyObjectImpl { + target, + output, + error, + body, + } = match &tokens.data { + syn::Data::Enum(en) => { + if options.transparent.is_some() { + bail_spanned!(tokens.span() => "`transparent` is not supported at top level for enums"); + } + let en = Enum::new(en, &tokens.ident)?; + en.build(ctx) + } + syn::Data::Struct(st) => { + let ident = &tokens.ident; + let st = Container::new( + Some(Ident::new("self", Span::call_site())), + &st.fields, + parse_quote!(#ident), + options, + )?; + st.build(ctx) + } + syn::Data::Union(_) => bail_spanned!( + tokens.span() => "#[derive(`IntoPyObject`)] is not supported for unions" + ), + }; + + let ident = &tokens.ident; + Ok(quote!( + #[automatically_derived] + impl #impl_generics #pyo3_path::conversion::IntoPyObject<#lt_param> for #ident #ty_generics #where_clause { + type Target = #target; + type Output = #output; + type Error = #error; + + fn into_pyobject(self, py: #pyo3_path::Python<#lt_param>) -> ::std::result::Result { + #body + } + } + )) +} diff --git a/pyo3-macros-backend/src/lib.rs b/pyo3-macros-backend/src/lib.rs index 5d7437a4295..d6c8f287332 100644 --- a/pyo3-macros-backend/src/lib.rs +++ b/pyo3-macros-backend/src/lib.rs @@ -11,6 +11,7 @@ mod utils; mod attributes; mod deprecations; mod frompyobject; +mod intopyobject; mod konst; mod method; mod module; @@ -23,6 +24,7 @@ mod pyversions; mod quotes; pub use frompyobject::build_derive_from_pyobject; +pub use intopyobject::build_derive_into_pyobject; pub use module::{pymodule_function_impl, pymodule_module_impl, PyModuleOptions}; pub use pyclass::{build_py_class, build_py_enum, PyClassArgs}; pub use pyfunction::{build_py_function, PyFunctionOptions}; diff --git a/pyo3-macros/src/lib.rs b/pyo3-macros/src/lib.rs index 08b2af3cd6f..7c43c55dcd7 100644 --- a/pyo3-macros/src/lib.rs +++ b/pyo3-macros/src/lib.rs @@ -5,9 +5,9 @@ use proc_macro::TokenStream; use proc_macro2::TokenStream as TokenStream2; use pyo3_macros_backend::{ - build_derive_from_pyobject, build_py_class, build_py_enum, build_py_function, build_py_methods, - pymodule_function_impl, pymodule_module_impl, PyClassArgs, PyClassMethodsType, - PyFunctionOptions, PyModuleOptions, + build_derive_from_pyobject, build_derive_into_pyobject, build_py_class, build_py_enum, + build_py_function, build_py_methods, pymodule_function_impl, pymodule_module_impl, PyClassArgs, + PyClassMethodsType, PyFunctionOptions, PyModuleOptions, }; use quote::quote; use syn::{parse_macro_input, Item}; @@ -153,6 +153,16 @@ pub fn pyfunction(attr: TokenStream, input: TokenStream) -> TokenStream { .into() } +#[proc_macro_derive(IntoPyObject, attributes(pyo3))] +pub fn derive_into_py_object(item: TokenStream) -> TokenStream { + let ast = parse_macro_input!(item as syn::DeriveInput); + let expanded = build_derive_into_pyobject(&ast).unwrap_or_compile_error(); + quote!( + #expanded + ) + .into() +} + #[proc_macro_derive(FromPyObject, attributes(pyo3))] pub fn derive_from_py_object(item: TokenStream) -> TokenStream { let ast = parse_macro_input!(item as syn::DeriveInput); diff --git a/src/lib.rs b/src/lib.rs index 73a22e94103..d6bf1b374c3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -446,7 +446,7 @@ mod version; pub use crate::conversions::*; #[cfg(feature = "macros")] -pub use pyo3_macros::{pyfunction, pymethods, pymodule, FromPyObject}; +pub use pyo3_macros::{pyfunction, pymethods, pymodule, FromPyObject, IntoPyObject}; /// A proc macro used to expose Rust structs and fieldless enums as Python objects. /// diff --git a/src/prelude.rs b/src/prelude.rs index 97f3e35afa1..cc44a199611 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -19,7 +19,7 @@ pub use crate::pyclass_init::PyClassInitializer; pub use crate::types::{PyAny, PyModule}; #[cfg(feature = "macros")] -pub use pyo3_macros::{pyclass, pyfunction, pymethods, pymodule, FromPyObject}; +pub use pyo3_macros::{pyclass, pyfunction, pymethods, pymodule, FromPyObject, IntoPyObject}; #[cfg(feature = "macros")] pub use crate::wrap_pyfunction; diff --git a/src/tests/hygiene/misc.rs b/src/tests/hygiene/misc.rs index 3e1cd51422a..1790c65961d 100644 --- a/src/tests/hygiene/misc.rs +++ b/src/tests/hygiene/misc.rs @@ -56,3 +56,39 @@ macro_rules! macro_rules_hygiene { } macro_rules_hygiene!(MyClass1, MyClass2); + +#[derive(crate::IntoPyObject)] +#[pyo3(crate = "crate")] +struct IntoPyObject1(i32); // transparent newtype case + +#[derive(crate::IntoPyObject)] +#[pyo3(crate = "crate", transparent)] +struct IntoPyObject2<'a> { + inner: &'a str, // transparent newtype case +} + +#[derive(crate::IntoPyObject)] +#[pyo3(crate = "crate")] +struct IntoPyObject3<'py>(i32, crate::Bound<'py, crate::PyAny>); // tuple case + +#[derive(crate::IntoPyObject)] +#[pyo3(crate = "crate")] +struct IntoPyObject4<'a, 'py> { + callable: &'a crate::Bound<'py, crate::PyAny>, // struct case + num: usize, +} + +#[derive(crate::IntoPyObject)] +#[pyo3(crate = "crate")] +enum IntoPyObject5<'a, 'py> { + TransparentTuple(i32), + #[pyo3(transparent)] + TransparentStruct { + f: crate::Py, + }, + Tuple(crate::Bound<'py, crate::types::PyString>, usize), + Struct { + f: i32, + g: &'a str, + }, +} // enum case diff --git a/tests/test_compile_error.rs b/tests/test_compile_error.rs index 012d759a99d..b6cf5065371 100644 --- a/tests/test_compile_error.rs +++ b/tests/test_compile_error.rs @@ -25,6 +25,7 @@ fn test_compile_errors() { t.compile_fail("tests/ui/invalid_closure.rs"); t.compile_fail("tests/ui/pyclass_send.rs"); t.compile_fail("tests/ui/invalid_argument_attributes.rs"); + t.compile_fail("tests/ui/invalid_intopy_derive.rs"); t.compile_fail("tests/ui/invalid_frompy_derive.rs"); t.compile_fail("tests/ui/static_ref.rs"); t.compile_fail("tests/ui/wrong_aspyref_lifetimes.rs"); diff --git a/tests/test_frompy_intopy_roundtrip.rs b/tests/test_frompy_intopy_roundtrip.rs new file mode 100644 index 00000000000..6b3718693d7 --- /dev/null +++ b/tests/test_frompy_intopy_roundtrip.rs @@ -0,0 +1,180 @@ +#![cfg(feature = "macros")] + +use pyo3::types::{PyDict, PyString}; +use pyo3::{prelude::*, IntoPyObject}; +use std::collections::HashMap; +use std::hash::Hash; + +#[macro_use] +#[path = "../src/tests/common.rs"] +mod common; + +#[derive(Debug, Clone, IntoPyObject, FromPyObject)] +pub struct A<'py> { + #[pyo3(item)] + s: String, + #[pyo3(item)] + t: Bound<'py, PyString>, + #[pyo3(item("foo"))] + p: Bound<'py, PyAny>, +} + +#[test] +fn test_named_fields_struct() { + Python::with_gil(|py| { + let a = A { + s: "Hello".into(), + t: PyString::new(py, "World"), + p: 42i32.into_pyobject(py).unwrap().into_any(), + }; + let pya = a.clone().into_pyobject(py).unwrap(); + let new_a = pya.extract::>().unwrap(); + + assert_eq!(a.s, new_a.s); + assert_eq!(a.t.to_cow().unwrap(), new_a.t.to_cow().unwrap()); + assert_eq!( + a.p.extract::().unwrap(), + new_a.p.extract::().unwrap() + ); + }); +} + +#[derive(Debug, Clone, PartialEq, IntoPyObject, FromPyObject)] +#[pyo3(transparent)] +pub struct B { + test: String, +} + +#[test] +fn test_transparent_named_field_struct() { + Python::with_gil(|py| { + let b = B { + test: "test".into(), + }; + let pyb = b.clone().into_pyobject(py).unwrap(); + let new_b = pyb.extract::().unwrap(); + assert_eq!(b, new_b); + }); +} + +#[derive(Debug, Clone, PartialEq, IntoPyObject, FromPyObject)] +#[pyo3(transparent)] +pub struct D { + test: T, +} + +#[test] +fn test_generic_transparent_named_field_struct() { + Python::with_gil(|py| { + let d = D { + test: String::from("test"), + }; + let pyd = d.clone().into_pyobject(py).unwrap(); + let new_d = pyd.extract::>().unwrap(); + assert_eq!(d, new_d); + + let d = D { test: 1usize }; + let pyd = d.clone().into_pyobject(py).unwrap(); + let new_d = pyd.extract::>().unwrap(); + assert_eq!(d, new_d); + }); +} + +#[derive(Debug, IntoPyObject, FromPyObject)] +pub struct GenericWithBound(HashMap); + +#[test] +fn test_generic_with_bound() { + Python::with_gil(|py| { + let mut hash_map = HashMap::::new(); + hash_map.insert("1".into(), 1); + hash_map.insert("2".into(), 2); + let map = GenericWithBound(hash_map).into_pyobject(py).unwrap(); + assert_eq!(map.len(), 2); + assert_eq!( + map.get_item("1") + .unwrap() + .unwrap() + .extract::() + .unwrap(), + 1 + ); + assert_eq!( + map.get_item("2") + .unwrap() + .unwrap() + .extract::() + .unwrap(), + 2 + ); + assert!(map.get_item("3").unwrap().is_none()); + }); +} + +#[derive(Debug, Clone, PartialEq, IntoPyObject, FromPyObject)] +pub struct Tuple(String, usize); + +#[test] +fn test_tuple_struct() { + Python::with_gil(|py| { + let tup = Tuple(String::from("test"), 1); + let tuple = tup.clone().into_pyobject(py).unwrap(); + let new_tup = tuple.extract::().unwrap(); + assert_eq!(tup, new_tup); + }); +} + +#[derive(Debug, Clone, PartialEq, IntoPyObject, FromPyObject)] +pub struct TransparentTuple(String); + +#[test] +fn test_transparent_tuple_struct() { + Python::with_gil(|py| { + let tup = TransparentTuple(String::from("test")); + let tuple = tup.clone().into_pyobject(py).unwrap(); + let new_tup = tuple.extract::().unwrap(); + assert_eq!(tup, new_tup); + }); +} + +#[derive(Debug, Clone, PartialEq, IntoPyObject, FromPyObject)] +pub enum Foo { + TupleVar(usize, String), + StructVar { + #[pyo3(item)] + test: char, + }, + #[pyo3(transparent)] + TransparentTuple(usize), + #[pyo3(transparent)] + TransparentStructVar { + a: Option, + }, +} + +#[test] +fn test_enum() { + Python::with_gil(|py| { + let tuple_var = Foo::TupleVar(1, "test".into()); + let foo = tuple_var.clone().into_pyobject(py).unwrap(); + assert_eq!(tuple_var, foo.extract::().unwrap()); + + let struct_var = Foo::StructVar { test: 'b' }; + let foo = struct_var + .clone() + .into_pyobject(py) + .unwrap() + .downcast_into::() + .unwrap(); + + assert_eq!(struct_var, foo.extract::().unwrap()); + + let transparent_tuple = Foo::TransparentTuple(1); + let foo = transparent_tuple.clone().into_pyobject(py).unwrap(); + assert_eq!(transparent_tuple, foo.extract::().unwrap()); + + let transparent_struct_var = Foo::TransparentStructVar { a: None }; + let foo = transparent_struct_var.clone().into_pyobject(py).unwrap(); + assert_eq!(transparent_struct_var, foo.extract::().unwrap()); + }); +} diff --git a/tests/test_intopyobject.rs b/tests/test_intopyobject.rs new file mode 100644 index 00000000000..971663b05d7 --- /dev/null +++ b/tests/test_intopyobject.rs @@ -0,0 +1,201 @@ +#![cfg(feature = "macros")] + +use pyo3::types::{PyDict, PyString}; +use pyo3::{prelude::*, IntoPyObject}; +use std::collections::HashMap; +use std::hash::Hash; + +#[macro_use] +#[path = "../src/tests/common.rs"] +mod common; + +#[derive(Debug, IntoPyObject)] +pub struct A<'py> { + s: String, + t: Bound<'py, PyString>, + p: Bound<'py, PyAny>, +} + +#[test] +fn test_named_fields_struct() { + Python::with_gil(|py| { + let a = A { + s: "Hello".into(), + t: PyString::new(py, "World"), + p: 42i32.into_pyobject(py).unwrap().into_any(), + }; + let pya = a.into_pyobject(py).unwrap(); + assert_eq!( + pya.get_item("s") + .unwrap() + .unwrap() + .downcast::() + .unwrap(), + "Hello" + ); + assert_eq!( + pya.get_item("t") + .unwrap() + .unwrap() + .downcast::() + .unwrap(), + "World" + ); + assert_eq!( + pya.get_item("p") + .unwrap() + .unwrap() + .extract::() + .unwrap(), + 42 + ); + }); +} + +#[derive(Debug, IntoPyObject)] +#[pyo3(transparent)] +pub struct B<'a> { + test: &'a str, +} + +#[test] +fn test_transparent_named_field_struct() { + Python::with_gil(|py| { + let pyb = B { test: "test" }.into_pyobject(py).unwrap(); + let b = pyb.extract::().unwrap(); + assert_eq!(b, "test"); + }); +} + +#[derive(Debug, IntoPyObject)] +#[pyo3(transparent)] +pub struct D { + test: T, +} + +#[test] +fn test_generic_transparent_named_field_struct() { + Python::with_gil(|py| { + let pyd = D { + test: String::from("test"), + } + .into_pyobject(py) + .unwrap(); + let d = pyd.extract::().unwrap(); + assert_eq!(d, "test"); + + let pyd = D { test: 1usize }.into_pyobject(py).unwrap(); + let d = pyd.extract::().unwrap(); + assert_eq!(d, 1); + }); +} + +#[derive(Debug, IntoPyObject)] +pub struct GenericWithBound(HashMap); + +#[test] +fn test_generic_with_bound() { + Python::with_gil(|py| { + let mut hash_map = HashMap::::new(); + hash_map.insert("1".into(), 1); + hash_map.insert("2".into(), 2); + let map = GenericWithBound(hash_map).into_pyobject(py).unwrap(); + assert_eq!(map.len(), 2); + assert_eq!( + map.get_item("1") + .unwrap() + .unwrap() + .extract::() + .unwrap(), + 1 + ); + assert_eq!( + map.get_item("2") + .unwrap() + .unwrap() + .extract::() + .unwrap(), + 2 + ); + assert!(map.get_item("3").unwrap().is_none()); + }); +} + +#[derive(Debug, IntoPyObject)] +pub struct Tuple(String, usize); + +#[test] +fn test_tuple_struct() { + Python::with_gil(|py| { + let tup = Tuple(String::from("test"), 1).into_pyobject(py).unwrap(); + assert!(tup.extract::<(usize, String)>().is_err()); + let tup = tup.extract::<(String, usize)>().unwrap(); + assert_eq!(tup.0, "test"); + assert_eq!(tup.1, 1); + }); +} + +#[derive(Debug, IntoPyObject)] +pub struct TransparentTuple(String); + +#[test] +fn test_transparent_tuple_struct() { + Python::with_gil(|py| { + let tup = TransparentTuple(String::from("test")) + .into_pyobject(py) + .unwrap(); + assert!(tup.extract::<(String,)>().is_err()); + let tup = tup.extract::().unwrap(); + assert_eq!(tup, "test"); + }); +} + +#[derive(Debug, IntoPyObject)] +pub enum Foo<'py> { + TupleVar(usize, String), + StructVar { + test: Bound<'py, PyString>, + }, + #[pyo3(transparent)] + TransparentTuple(usize), + #[pyo3(transparent)] + TransparentStructVar { + a: Option, + }, +} + +#[test] +fn test_enum() { + Python::with_gil(|py| { + let foo = Foo::TupleVar(1, "test".into()).into_pyobject(py).unwrap(); + assert_eq!( + foo.extract::<(usize, String)>().unwrap(), + (1, String::from("test")) + ); + + let foo = Foo::StructVar { + test: PyString::new(py, "test"), + } + .into_pyobject(py) + .unwrap() + .downcast_into::() + .unwrap(); + + assert_eq!( + foo.get_item("test") + .unwrap() + .unwrap() + .downcast_into::() + .unwrap(), + "test" + ); + + let foo = Foo::TransparentTuple(1).into_pyobject(py).unwrap(); + assert_eq!(foo.extract::().unwrap(), 1); + + let foo = Foo::TransparentStructVar { a: None } + .into_pyobject(py) + .unwrap(); + assert!(foo.is_none()); + }); +} diff --git a/tests/ui/invalid_intopy_derive.rs b/tests/ui/invalid_intopy_derive.rs new file mode 100644 index 00000000000..310309992d4 --- /dev/null +++ b/tests/ui/invalid_intopy_derive.rs @@ -0,0 +1,109 @@ +use pyo3::IntoPyObject; + +#[derive(IntoPyObject)] +struct Foo(); + +#[derive(IntoPyObject)] +struct Foo2 {} + +#[derive(IntoPyObject)] +enum EmptyEnum {} + +#[derive(IntoPyObject)] +enum EnumWithEmptyTupleVar { + EmptyTuple(), + Valid(String), +} + +#[derive(IntoPyObject)] +enum EnumWithEmptyStructVar { + EmptyStruct {}, + Valid(String), +} + +#[derive(IntoPyObject)] +#[pyo3(transparent)] +struct EmptyTransparentTup(); + +#[derive(IntoPyObject)] +#[pyo3(transparent)] +struct EmptyTransparentStruct {} + +#[derive(IntoPyObject)] +enum EnumWithTransparentEmptyTupleVar { + #[pyo3(transparent)] + EmptyTuple(), + Valid(String), +} + +#[derive(IntoPyObject)] +enum EnumWithTransparentEmptyStructVar { + #[pyo3(transparent)] + EmptyStruct {}, + Valid(String), +} + +#[derive(IntoPyObject)] +#[pyo3(transparent)] +struct TransparentTupTooManyFields(String, String); + +#[derive(IntoPyObject)] +#[pyo3(transparent)] +struct TransparentStructTooManyFields { + foo: String, + bar: String, +} + +#[derive(IntoPyObject)] +enum EnumWithTransparentTupleTooMany { + #[pyo3(transparent)] + EmptyTuple(String, String), + Valid(String), +} + +#[derive(IntoPyObject)] +enum EnumWithTransparentStructTooMany { + #[pyo3(transparent)] + EmptyStruct { + foo: String, + bar: String, + }, + Valid(String), +} + +#[derive(IntoPyObject)] +#[pyo3(unknown = "should not work")] +struct UnknownContainerAttr { + a: String, +} + +#[derive(IntoPyObject)] +union Union { + a: usize, +} + +#[derive(IntoPyObject)] +enum UnitEnum { + Unit, +} + +#[derive(IntoPyObject)] +struct TupleAttribute(#[pyo3(attribute)] String, usize); + +#[derive(IntoPyObject)] +struct TupleItem(#[pyo3(item)] String, usize); + +#[derive(IntoPyObject)] +struct StructAttribute { + #[pyo3(attribute)] + foo: String, +} + +#[derive(IntoPyObject)] +#[pyo3(transparent)] +struct StructTransparentItem { + #[pyo3(item)] + foo: String, +} + +fn main() {} diff --git a/tests/ui/invalid_intopy_derive.stderr b/tests/ui/invalid_intopy_derive.stderr new file mode 100644 index 00000000000..cf125d9c073 --- /dev/null +++ b/tests/ui/invalid_intopy_derive.stderr @@ -0,0 +1,127 @@ +error: cannot derive `IntoPyObject` for empty structs + --> tests/ui/invalid_intopy_derive.rs:4:11 + | +4 | struct Foo(); + | ^^ + +error: cannot derive `IntoPyObject` for empty structs + --> tests/ui/invalid_intopy_derive.rs:7:13 + | +7 | struct Foo2 {} + | ^^ + +error: cannot derive `IntoPyObject` for empty enum + --> tests/ui/invalid_intopy_derive.rs:10:6 + | +10 | enum EmptyEnum {} + | ^^^^^^^^^ + +error: cannot derive `IntoPyObject` for empty variants + --> tests/ui/invalid_intopy_derive.rs:14:5 + | +14 | EmptyTuple(), + | ^^^^^^^^^^ + +error: cannot derive `IntoPyObject` for empty variants + --> tests/ui/invalid_intopy_derive.rs:20:5 + | +20 | EmptyStruct {}, + | ^^^^^^^^^^^ + +error: cannot derive `IntoPyObject` for empty structs + --> tests/ui/invalid_intopy_derive.rs:26:27 + | +26 | struct EmptyTransparentTup(); + | ^^ + +error: cannot derive `IntoPyObject` for empty structs + --> tests/ui/invalid_intopy_derive.rs:30:31 + | +30 | struct EmptyTransparentStruct {} + | ^^ + +error: cannot derive `IntoPyObject` for empty variants + --> tests/ui/invalid_intopy_derive.rs:35:5 + | +35 | EmptyTuple(), + | ^^^^^^^^^^ + +error: cannot derive `IntoPyObject` for empty variants + --> tests/ui/invalid_intopy_derive.rs:42:5 + | +42 | EmptyStruct {}, + | ^^^^^^^^^^^ + +error: transparent structs and variants can only have 1 field + --> tests/ui/invalid_intopy_derive.rs:48:35 + | +48 | struct TransparentTupTooManyFields(String, String); + | ^^^^^^^^^^^^^^^^ + +error: transparent structs and variants can only have 1 field + --> tests/ui/invalid_intopy_derive.rs:52:39 + | +52 | struct TransparentStructTooManyFields { + | _______________________________________^ +53 | | foo: String, +54 | | bar: String, +55 | | } + | |_^ + +error: transparent structs and variants can only have 1 field + --> tests/ui/invalid_intopy_derive.rs:60:15 + | +60 | EmptyTuple(String, String), + | ^^^^^^^^^^^^^^^^ + +error: transparent structs and variants can only have 1 field + --> tests/ui/invalid_intopy_derive.rs:67:17 + | +67 | EmptyStruct { + | _________________^ +68 | | foo: String, +69 | | bar: String, +70 | | }, + | |_____^ + +error: expected `transparent` or `crate` + --> tests/ui/invalid_intopy_derive.rs:75:8 + | +75 | #[pyo3(unknown = "should not work")] + | ^^^^^^^ + +error: #[derive(`IntoPyObject`)] is not supported for unions + --> tests/ui/invalid_intopy_derive.rs:81:1 + | +81 | union Union { + | ^^^^^ + +error: cannot derive `IntoPyObject` for empty variants + --> tests/ui/invalid_intopy_derive.rs:87:5 + | +87 | Unit, + | ^^^^ + +error: `attribute` is not supported by `IntoPyObject` + --> tests/ui/invalid_intopy_derive.rs:91:30 + | +91 | struct TupleAttribute(#[pyo3(attribute)] String, usize); + | ^^^^^^^^^ + +error: `item` is not permitted on tuple struct elements. + --> tests/ui/invalid_intopy_derive.rs:94:25 + | +94 | struct TupleItem(#[pyo3(item)] String, usize); + | ^^^^ + +error: `attribute` is not supported by `IntoPyObject` + --> tests/ui/invalid_intopy_derive.rs:98:12 + | +98 | #[pyo3(attribute)] + | ^^^^^^^^^ + +error: `transparent` structs may not have `item` for the inner field + --> tests/ui/invalid_intopy_derive.rs:105:12 + | +105 | #[pyo3(item)] + | ^^^^ From 869c4bf200f498ca54ac3f1b0cae587d2d19c4b3 Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Sat, 26 Oct 2024 06:12:52 +0200 Subject: [PATCH 340/495] fix compile error for `#[classmethod]`/`#[staticmethod]` on magic methods (#4654) * fix compile error for `#[classmethod]`/`#[staticmethod]` on magic methods * add newsfragment --- newsfragments/4654.fixed.md | 1 + pyo3-macros-backend/src/method.rs | 12 ++++-------- src/tests/hygiene/pymethods.rs | 12 +++++++++++- 3 files changed, 16 insertions(+), 9 deletions(-) create mode 100644 newsfragments/4654.fixed.md diff --git a/newsfragments/4654.fixed.md b/newsfragments/4654.fixed.md new file mode 100644 index 00000000000..5e470178b55 --- /dev/null +++ b/newsfragments/4654.fixed.md @@ -0,0 +1 @@ +fix compile error for `#[classmethod]`/`#[staticmethod]` on magic methods \ No newline at end of file diff --git a/pyo3-macros-backend/src/method.rs b/pyo3-macros-backend/src/method.rs index 50f70d8440a..019fb5e644b 100644 --- a/pyo3-macros-backend/src/method.rs +++ b/pyo3-macros-backend/src/method.rs @@ -263,12 +263,12 @@ impl FnType { } FnType::FnClass(span) | FnType::FnNewClass(span) => { let py = syn::Ident::new("py", Span::call_site()); - let slf: Ident = syn::Ident::new("_slf_ref", Span::call_site()); + let slf: Ident = syn::Ident::new("_slf", Span::call_site()); let pyo3_path = pyo3_path.to_tokens_spanned(*span); let ret = quote_spanned! { *span => #[allow(clippy::useless_conversion)] ::std::convert::Into::into( - #pyo3_path::impl_::pymethods::BoundRef::ref_from_ptr(#py, &*(#slf as *const _ as *const *mut _)) + #pyo3_path::impl_::pymethods::BoundRef::ref_from_ptr(#py, &*(&#slf as *const _ as *const *mut _)) .downcast_unchecked::<#pyo3_path::types::PyType>() ), }; @@ -276,12 +276,12 @@ impl FnType { } FnType::FnModule(span) => { let py = syn::Ident::new("py", Span::call_site()); - let slf: Ident = syn::Ident::new("_slf_ref", Span::call_site()); + let slf: Ident = syn::Ident::new("_slf", Span::call_site()); let pyo3_path = pyo3_path.to_tokens_spanned(*span); let ret = quote_spanned! { *span => #[allow(clippy::useless_conversion)] ::std::convert::Into::into( - #pyo3_path::impl_::pymethods::BoundRef::ref_from_ptr(#py, &*(#slf as *const _ as *const *mut _)) + #pyo3_path::impl_::pymethods::BoundRef::ref_from_ptr(#py, &*(&#slf as *const _ as *const *mut _)) .downcast_unchecked::<#pyo3_path::types::PyModule>() ), }; @@ -766,7 +766,6 @@ impl<'a> FnSpec<'a> { _slf: *mut #pyo3_path::ffi::PyObject, ) -> #pyo3_path::PyResult<*mut #pyo3_path::ffi::PyObject> { #deprecation - let _slf_ref = &_slf; let function = #rust_name; // Shadow the function name to avoid #3017 #init_holders let result = #call; @@ -789,7 +788,6 @@ impl<'a> FnSpec<'a> { _kwnames: *mut #pyo3_path::ffi::PyObject ) -> #pyo3_path::PyResult<*mut #pyo3_path::ffi::PyObject> { #deprecation - let _slf_ref = &_slf; let function = #rust_name; // Shadow the function name to avoid #3017 #arg_convert #init_holders @@ -812,7 +810,6 @@ impl<'a> FnSpec<'a> { _kwargs: *mut #pyo3_path::ffi::PyObject ) -> #pyo3_path::PyResult<*mut #pyo3_path::ffi::PyObject> { #deprecation - let _slf_ref = &_slf; let function = #rust_name; // Shadow the function name to avoid #3017 #arg_convert #init_holders @@ -838,7 +835,6 @@ impl<'a> FnSpec<'a> { ) -> #pyo3_path::PyResult<*mut #pyo3_path::ffi::PyObject> { use #pyo3_path::impl_::callback::IntoPyCallbackOutput; #deprecation - let _slf_ref = &_slf; let function = #rust_name; // Shadow the function name to avoid #3017 #arg_convert #init_holders diff --git a/src/tests/hygiene/pymethods.rs b/src/tests/hygiene/pymethods.rs index d6d294c558d..a5856e6413e 100644 --- a/src/tests/hygiene/pymethods.rs +++ b/src/tests/hygiene/pymethods.rs @@ -439,4 +439,14 @@ impl Clear { struct Dummy2; #[crate::pymethods(crate = "crate")] -impl Dummy2 {} +impl Dummy2 { + #[classmethod] + fn __len__(cls: &crate::Bound<'_, crate::types::PyType>) -> crate::PyResult { + ::std::result::Result::Ok(0) + } + + #[staticmethod] + fn __repr__() -> &'static str { + "Dummy" + } +} From fbeb2b66f42d2635d092039f28537aede87a40c3 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers <7943856+bschoenmaeckers@users.noreply.github.com> Date: Sat, 26 Oct 2024 06:16:34 +0200 Subject: [PATCH 341/495] Use `try_for_each` in `into_py_dict` (#4647) * Use `try_for_each` in `into_py_dict` * Use `try_fold` in `PyList::try_new_from_iter` * Use `try_for_each` in `PySet::try_new_from_iter` --- src/types/dict.rs | 6 +++--- src/types/list.rs | 22 +++++++++++----------- src/types/set.rs | 8 ++++---- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/types/dict.rs b/src/types/dict.rs index 6987e4525ec..129f32dc9e1 100644 --- a/src/types/dict.rs +++ b/src/types/dict.rs @@ -771,10 +771,10 @@ where { fn into_py_dict(self, py: Python<'py>) -> PyResult> { let dict = PyDict::new(py); - for item in self { + self.into_iter().try_for_each(|item| { let (key, value) = item.unpack(); - dict.set_item(key, value)?; - } + dict.set_item(key, value) + })?; Ok(dict) } } diff --git a/src/types/list.rs b/src/types/list.rs index ae5eda63a11..f00c194739f 100644 --- a/src/types/list.rs +++ b/src/types/list.rs @@ -5,7 +5,7 @@ use crate::ffi::{self, Py_ssize_t}; use crate::ffi_ptr_ext::FfiPtrExt; use crate::internal_tricks::get_ssize_index; use crate::types::{PySequence, PyTuple}; -use crate::{Borrowed, Bound, BoundObject, IntoPyObject, PyAny, PyObject, Python}; +use crate::{Borrowed, Bound, BoundObject, IntoPyObject, PyAny, PyErr, PyObject, Python}; use crate::types::any::PyAnyMethods; use crate::types::sequence::PySequenceMethods; @@ -51,18 +51,18 @@ pub(crate) fn try_new_from_iter<'py>( // - its Drop cleans up the list if user code or the asserts panic. let list = ptr.assume_owned(py).downcast_into_unchecked(); - let mut counter: Py_ssize_t = 0; - - for obj in (&mut elements).take(len as usize) { - #[cfg(not(Py_LIMITED_API))] - ffi::PyList_SET_ITEM(ptr, counter, obj?.into_ptr()); - #[cfg(Py_LIMITED_API)] - ffi::PyList_SetItem(ptr, counter, obj?.into_ptr()); - counter += 1; - } + let count = (&mut elements) + .take(len as usize) + .try_fold(0, |count, item| { + #[cfg(not(Py_LIMITED_API))] + ffi::PyList_SET_ITEM(ptr, count, item?.into_ptr()); + #[cfg(Py_LIMITED_API)] + ffi::PyList_SetItem(ptr, count, item?.into_ptr()); + Ok::<_, PyErr>(count + 1) + })?; assert!(elements.next().is_none(), "Attempted to create PyList but `elements` was larger than reported by its `ExactSizeIterator` implementation."); - assert_eq!(len, counter, "Attempted to create PyList but `elements` was smaller than reported by its `ExactSizeIterator` implementation."); + assert_eq!(len, count, "Attempted to create PyList but `elements` was smaller than reported by its `ExactSizeIterator` implementation."); Ok(list) } diff --git a/src/types/set.rs b/src/types/set.rs index 622020dfb11..e7c24f5b1ea 100644 --- a/src/types/set.rs +++ b/src/types/set.rs @@ -315,10 +315,10 @@ where }; let ptr = set.as_ptr(); - for e in elements { - let obj = e.into_pyobject(py).map_err(Into::into)?; - err::error_on_minusone(py, unsafe { ffi::PySet_Add(ptr, obj.as_ptr()) })?; - } + elements.into_iter().try_for_each(|element| { + let obj = element.into_pyobject(py).map_err(Into::into)?; + err::error_on_minusone(py, unsafe { ffi::PySet_Add(ptr, obj.as_ptr()) }) + })?; Ok(set) } From b56806ac00732cebd07da3824972d310afc16ee2 Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Sat, 26 Oct 2024 10:54:04 +0200 Subject: [PATCH 342/495] migrate `call` API to `IntoPyObject` (#4653) --- src/conversion.rs | 136 +-------------------------------------------- src/instance.rs | 16 +++--- src/types/any.rs | 73 ++++++++++++------------ src/types/tuple.rs | 109 +----------------------------------- 4 files changed, 49 insertions(+), 285 deletions(-) diff --git a/src/conversion.rs b/src/conversion.rs index 4e0de44b4a1..dcf07a61eb3 100644 --- a/src/conversion.rs +++ b/src/conversion.rs @@ -1,11 +1,10 @@ //! Defines conversions between Rust and Python types. use crate::err::PyResult; -use crate::ffi_ptr_ext::FfiPtrExt; #[cfg(feature = "experimental-inspect")] use crate::inspect::types::TypeInfo; use crate::pyclass::boolean_struct::False; use crate::types::any::PyAnyMethods; -use crate::types::{PyDict, PyString, PyTuple}; +use crate::types::PyTuple; use crate::{ ffi, Borrowed, Bound, BoundObject, Py, PyAny, PyClass, PyErr, PyObject, PyRef, PyRefMut, Python, }; @@ -177,93 +176,6 @@ pub trait IntoPy: Sized { fn type_output() -> TypeInfo { TypeInfo::Any } - - // The following methods are helpers to use the vectorcall API where possible. - // They are overridden on tuples to perform a vectorcall. - // Be careful when you're implementing these: they can never refer to `Bound` call methods, - // as those refer to these methods, so this will create an infinite recursion. - #[doc(hidden)] - #[inline] - fn __py_call_vectorcall1<'py>( - self, - py: Python<'py>, - function: Borrowed<'_, 'py, PyAny>, - _: private::Token, - ) -> PyResult> - where - Self: IntoPy>, - { - #[inline] - fn inner<'py>( - py: Python<'py>, - function: Borrowed<'_, 'py, PyAny>, - args: Bound<'py, PyTuple>, - ) -> PyResult> { - unsafe { - ffi::PyObject_Call(function.as_ptr(), args.as_ptr(), std::ptr::null_mut()) - .assume_owned_or_err(py) - } - } - inner( - py, - function, - >>::into_py(self, py).into_bound(py), - ) - } - - #[doc(hidden)] - #[inline] - fn __py_call_vectorcall<'py>( - self, - py: Python<'py>, - function: Borrowed<'_, 'py, PyAny>, - kwargs: Option>, - _: private::Token, - ) -> PyResult> - where - Self: IntoPy>, - { - #[inline] - fn inner<'py>( - py: Python<'py>, - function: Borrowed<'_, 'py, PyAny>, - args: Bound<'py, PyTuple>, - kwargs: Option>, - ) -> PyResult> { - unsafe { - ffi::PyObject_Call( - function.as_ptr(), - args.as_ptr(), - kwargs.map_or_else(std::ptr::null_mut, |kwargs| kwargs.as_ptr()), - ) - .assume_owned_or_err(py) - } - } - inner( - py, - function, - >>::into_py(self, py).into_bound(py), - kwargs, - ) - } - - #[doc(hidden)] - #[inline] - fn __py_call_method_vectorcall1<'py>( - self, - _py: Python<'py>, - object: Borrowed<'_, 'py, PyAny>, - method_name: Borrowed<'_, 'py, PyString>, - _: private::Token, - ) -> PyResult> - where - Self: IntoPy>, - { - // Don't `self.into_py()`! This will lose the optimization of vectorcall. - object - .getattr(method_name) - .and_then(|method| method.call1(self)) - } } /// Defines a conversion from a Rust type to a Python object, which may fail. @@ -593,52 +505,6 @@ impl IntoPy> for () { fn into_py(self, py: Python<'_>) -> Py { PyTuple::empty(py).unbind() } - - #[inline] - fn __py_call_vectorcall1<'py>( - self, - py: Python<'py>, - function: Borrowed<'_, 'py, PyAny>, - _: private::Token, - ) -> PyResult> { - unsafe { ffi::compat::PyObject_CallNoArgs(function.as_ptr()).assume_owned_or_err(py) } - } - - #[inline] - fn __py_call_vectorcall<'py>( - self, - py: Python<'py>, - function: Borrowed<'_, 'py, PyAny>, - kwargs: Option>, - _: private::Token, - ) -> PyResult> { - unsafe { - match kwargs { - Some(kwargs) => ffi::PyObject_Call( - function.as_ptr(), - PyTuple::empty(py).as_ptr(), - kwargs.as_ptr(), - ) - .assume_owned_or_err(py), - None => ffi::compat::PyObject_CallNoArgs(function.as_ptr()).assume_owned_or_err(py), - } - } - } - - #[inline] - #[allow(clippy::used_underscore_binding)] - fn __py_call_method_vectorcall1<'py>( - self, - py: Python<'py>, - object: Borrowed<'_, 'py, PyAny>, - method_name: Borrowed<'_, 'py, PyString>, - _: private::Token, - ) -> PyResult> { - unsafe { - ffi::compat::PyObject_CallMethodNoArgs(object.as_ptr(), method_name.as_ptr()) - .assume_owned_or_err(py) - } - } } impl<'py> IntoPyObject<'py> for () { diff --git a/src/instance.rs b/src/instance.rs index a86bed94513..6b191abd5a2 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -1495,7 +1495,7 @@ impl Py { kwargs: Option<&Bound<'py, PyDict>>, ) -> PyResult where - A: IntoPy>, + A: IntoPyObject<'py, Target = PyTuple>, { self.bind(py).as_any().call(args, kwargs).map(Bound::unbind) } @@ -1509,15 +1509,15 @@ impl Py { args: impl IntoPy>, kwargs: Option<&Bound<'_, PyDict>>, ) -> PyResult { - self.call(py, args, kwargs) + self.call(py, args.into_py(py), kwargs) } /// Calls the object with only positional arguments. /// /// This is equivalent to the Python expression `self(*args)`. - pub fn call1(&self, py: Python<'_>, args: N) -> PyResult + pub fn call1<'py, N>(&self, py: Python<'py>, args: N) -> PyResult where - N: IntoPy>, + N: IntoPyObject<'py, Target = PyTuple>, { self.bind(py).as_any().call1(args).map(Bound::unbind) } @@ -1540,11 +1540,11 @@ impl Py { py: Python<'py>, name: N, args: A, - kwargs: Option<&Bound<'_, PyDict>>, + kwargs: Option<&Bound<'py, PyDict>>, ) -> PyResult where N: IntoPyObject<'py, Target = PyString>, - A: IntoPy>, + A: IntoPyObject<'py, Target = PyTuple>, { self.bind(py) .as_any() @@ -1566,7 +1566,7 @@ impl Py { N: IntoPy>, A: IntoPy>, { - self.call_method(py, name.into_py(py), args, kwargs) + self.call_method(py, name.into_py(py), args.into_py(py), kwargs) } /// Calls a method on the object with only positional arguments. @@ -1578,7 +1578,7 @@ impl Py { pub fn call_method1<'py, N, A>(&self, py: Python<'py>, name: N, args: A) -> PyResult where N: IntoPyObject<'py, Target = PyString>, - A: IntoPy>, + A: IntoPyObject<'py, Target = PyTuple>, { self.bind(py) .as_any() diff --git a/src/types/any.rs b/src/types/any.rs index 63c38e11c51..62f7cdfc27e 100644 --- a/src/types/any.rs +++ b/src/types/any.rs @@ -1,5 +1,5 @@ use crate::class::basic::CompareOp; -use crate::conversion::{private, AsPyPointer, FromPyObjectBound, IntoPy, IntoPyObject}; +use crate::conversion::{AsPyPointer, FromPyObjectBound, IntoPyObject}; use crate::err::{DowncastError, DowncastIntoError, PyErr, PyResult}; use crate::exceptions::{PyAttributeError, PyTypeError}; use crate::ffi_ptr_ext::FfiPtrExt; @@ -11,7 +11,7 @@ use crate::type_object::{PyTypeCheck, PyTypeInfo}; #[cfg(not(any(PyPy, GraalPy)))] use crate::types::PySuper; use crate::types::{PyDict, PyIterator, PyList, PyString, PyTuple, PyType}; -use crate::{err, ffi, Borrowed, BoundObject, Py, Python}; +use crate::{err, ffi, Borrowed, BoundObject, Python}; use std::cell::UnsafeCell; use std::cmp::Ordering; use std::os::raw::c_int; @@ -435,9 +435,9 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed { /// }) /// # } /// ``` - fn call
(&self, args: A, kwargs: Option<&Bound<'_, PyDict>>) -> PyResult> + fn call(&self, args: A, kwargs: Option<&Bound<'py, PyDict>>) -> PyResult> where - A: IntoPy>; + A: IntoPyObject<'py, Target = PyTuple>; /// Calls the object without arguments. /// @@ -492,7 +492,7 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed { /// ``` fn call1(&self, args: A) -> PyResult> where - A: IntoPy>; + A: IntoPyObject<'py, Target = PyTuple>; /// Calls a method on the object. /// @@ -535,11 +535,11 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed { &self, name: N, args: A, - kwargs: Option<&Bound<'_, PyDict>>, + kwargs: Option<&Bound<'py, PyDict>>, ) -> PyResult> where N: IntoPyObject<'py, Target = PyString>, - A: IntoPy>; + A: IntoPyObject<'py, Target = PyTuple>; /// Calls a method on the object without arguments. /// @@ -615,7 +615,7 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed { fn call_method1(&self, name: N, args: A) -> PyResult> where N: IntoPyObject<'py, Target = PyString>, - A: IntoPy>; + A: IntoPyObject<'py, Target = PyTuple>; /// Returns whether the object is considered to be true. /// @@ -1245,15 +1245,30 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { unsafe { ffi::PyCallable_Check(self.as_ptr()) != 0 } } - fn call(&self, args: A, kwargs: Option<&Bound<'_, PyDict>>) -> PyResult> + fn call(&self, args: A, kwargs: Option<&Bound<'py, PyDict>>) -> PyResult> where - A: IntoPy>, + A: IntoPyObject<'py, Target = PyTuple>, { - args.__py_call_vectorcall( - self.py(), - self.as_borrowed(), - kwargs.map(Bound::as_borrowed), - private::Token, + fn inner<'py>( + any: &Bound<'py, PyAny>, + args: Borrowed<'_, 'py, PyTuple>, + kwargs: Option<&Bound<'py, PyDict>>, + ) -> PyResult> { + unsafe { + ffi::PyObject_Call( + any.as_ptr(), + args.as_ptr(), + kwargs.map_or(std::ptr::null_mut(), |dict| dict.as_ptr()), + ) + .assume_owned_or_err(any.py()) + } + } + + let py = self.py(); + inner( + self, + args.into_pyobject(py).map_err(Into::into)?.as_borrowed(), + kwargs, ) } @@ -1264,9 +1279,9 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { fn call1(&self, args: A) -> PyResult> where - A: IntoPy>, + A: IntoPyObject<'py, Target = PyTuple>, { - args.__py_call_vectorcall1(self.py(), self.as_borrowed(), private::Token) + self.call(args, None) } #[inline] @@ -1274,19 +1289,14 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { &self, name: N, args: A, - kwargs: Option<&Bound<'_, PyDict>>, + kwargs: Option<&Bound<'py, PyDict>>, ) -> PyResult> where N: IntoPyObject<'py, Target = PyString>, - A: IntoPy>, + A: IntoPyObject<'py, Target = PyTuple>, { - // Don't `args.into_py()`! This will lose the optimization of vectorcall. - match kwargs { - Some(_) => self - .getattr(name) - .and_then(|method| method.call(args, kwargs)), - None => self.call_method1(name, args), - } + self.getattr(name) + .and_then(|method| method.call(args, kwargs)) } #[inline] @@ -1305,16 +1315,9 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { fn call_method1(&self, name: N, args: A) -> PyResult> where N: IntoPyObject<'py, Target = PyString>, - A: IntoPy>, + A: IntoPyObject<'py, Target = PyTuple>, { - args.__py_call_method_vectorcall1( - self.py(), - self.as_borrowed(), - name.into_pyobject(self.py()) - .map_err(Into::into)? - .as_borrowed(), - private::Token, - ) + self.call_method(name, args, None) } fn is_truthy(&self) -> PyResult { diff --git a/src/types/tuple.rs b/src/types/tuple.rs index b2944741256..189cec126bd 100644 --- a/src/types/tuple.rs +++ b/src/types/tuple.rs @@ -1,15 +1,13 @@ use std::iter::FusedIterator; -use crate::conversion::{private, IntoPyObject}; +use crate::conversion::IntoPyObject; use crate::ffi::{self, Py_ssize_t}; use crate::ffi_ptr_ext::FfiPtrExt; #[cfg(feature = "experimental-inspect")] use crate::inspect::types::TypeInfo; use crate::instance::Borrowed; use crate::internal_tricks::get_ssize_index; -use crate::types::{ - any::PyAnyMethods, sequence::PySequenceMethods, PyDict, PyList, PySequence, PyString, -}; +use crate::types::{any::PyAnyMethods, sequence::PySequenceMethods, PyList, PySequence}; #[allow(deprecated)] use crate::ToPyObject; use crate::{ @@ -573,109 +571,6 @@ macro_rules! tuple_conversion ({$length:expr,$(($refN:ident, $n:tt, $T:ident)),+ fn type_output() -> TypeInfo { TypeInfo::Tuple(Some(vec![$( $T::type_output() ),+])) } - - #[inline] - fn __py_call_vectorcall1<'py>( - self, - py: Python<'py>, - function: Borrowed<'_, 'py, PyAny>, - _: private::Token, - ) -> PyResult> { - cfg_if::cfg_if! { - if #[cfg(all(Py_3_9, not(any(PyPy, GraalPy, Py_LIMITED_API))))] { - // We need this to drop the arguments correctly. - let args_bound = [$(self.$n.into_py(py).into_bound(py),)*]; - if $length == 1 { - unsafe { - ffi::PyObject_CallOneArg(function.as_ptr(), args_bound[0].as_ptr()).assume_owned_or_err(py) - } - } else { - // Prepend one null argument for `PY_VECTORCALL_ARGUMENTS_OFFSET`. - let mut args = [std::ptr::null_mut(), $(args_bound[$n].as_ptr()),*]; - unsafe { - ffi::PyObject_Vectorcall( - function.as_ptr(), - args.as_mut_ptr().add(1), - $length + ffi::PY_VECTORCALL_ARGUMENTS_OFFSET, - std::ptr::null_mut(), - ) - .assume_owned_or_err(py) - } - } - } else { - function.call1(>>::into_py(self, py).into_bound(py)) - } - } - } - - #[inline] - fn __py_call_vectorcall<'py>( - self, - py: Python<'py>, - function: Borrowed<'_, 'py, PyAny>, - kwargs: Option>, - _: private::Token, - ) -> PyResult> { - cfg_if::cfg_if! { - if #[cfg(all(Py_3_9, not(any(PyPy, GraalPy, Py_LIMITED_API))))] { - // We need this to drop the arguments correctly. - let args_bound = [$(self.$n.into_py(py).into_bound(py),)*]; - // Prepend one null argument for `PY_VECTORCALL_ARGUMENTS_OFFSET`. - let mut args = [std::ptr::null_mut(), $(args_bound[$n].as_ptr()),*]; - unsafe { - ffi::PyObject_VectorcallDict( - function.as_ptr(), - args.as_mut_ptr().add(1), - $length + ffi::PY_VECTORCALL_ARGUMENTS_OFFSET, - kwargs.map_or_else(std::ptr::null_mut, |kwargs| kwargs.as_ptr()), - ) - .assume_owned_or_err(py) - } - } else { - function.call(>>::into_py(self, py).into_bound(py), kwargs.as_deref()) - } - } - } - - #[inline] - fn __py_call_method_vectorcall1<'py>( - self, - py: Python<'py>, - object: Borrowed<'_, 'py, PyAny>, - method_name: Borrowed<'_, 'py, PyString>, - _: private::Token, - ) -> PyResult> { - cfg_if::cfg_if! { - if #[cfg(all(Py_3_9, not(any(PyPy, GraalPy, Py_LIMITED_API))))] { - // We need this to drop the arguments correctly. - let args_bound = [$(self.$n.into_py(py).into_bound(py),)*]; - if $length == 1 { - unsafe { - ffi::PyObject_CallMethodOneArg( - object.as_ptr(), - method_name.as_ptr(), - args_bound[0].as_ptr(), - ) - .assume_owned_or_err(py) - } - } else { - let mut args = [object.as_ptr(), $(args_bound[$n].as_ptr()),*]; - unsafe { - ffi::PyObject_VectorcallMethod( - method_name.as_ptr(), - args.as_mut_ptr(), - // +1 for the receiver. - 1 + $length + ffi::PY_VECTORCALL_ARGUMENTS_OFFSET, - std::ptr::null_mut(), - ) - .assume_owned_or_err(py) - } - } - } else { - object.call_method1(method_name, >>::into_py(self, py).into_bound(py)) - } - } - } } impl<'py, $($T: FromPyObject<'py>),+> FromPyObject<'py> for ($($T,)+) { From 8dfbb414ab68906093be27c1da30dc950887fc43 Mon Sep 17 00:00:00 2001 From: JHM Darbyshire <24256554+attack68@users.noreply.github.com> Date: Sat, 26 Oct 2024 18:24:20 +0200 Subject: [PATCH 343/495] docs: add an example library to README (#4659) * DOC: add example to README.md * fix italics --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index b5225dbfe16..1094a489999 100644 --- a/README.md +++ b/README.md @@ -220,6 +220,7 @@ about this topic. - Quite easy to follow as there's not much code. - [pyre](https://github.com/Project-Dream-Weaver/pyre-http) _Fast Python HTTP server written in Rust._ - [primp](https://github.com/deedy5/primp) _The fastest python HTTP client that can impersonate web browsers by mimicking their headers and TLS/JA3/JA4/HTTP2 fingerprints._ +- [rateslib](https://github.com/attack68/rateslib) _A fixed income library for Python using Rust extensions._ - [ril-py](https://github.com/Cryptex-github/ril-py) _A performant and high-level image processing library for Python written in Rust._ - [river](https://github.com/online-ml/river) _Online machine learning in python, the computationally heavy statistics algorithms are implemented in Rust._ - [robyn](https://github.com/sparckles/Robyn) A Super Fast Async Python Web Framework with a Rust runtime. From f536ecef283e0ffe23e074629d269021a4169340 Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Sat, 26 Oct 2024 18:33:38 +0200 Subject: [PATCH 344/495] move `IntoPy::type_output` to `IntoPyObject::type_output` (#4657) * move `IntoPy::type_output` to `IntoPyObject::type_output` * add newsfragment * fix MSRV --- newsfragments/4657.changed.md | 1 + src/conversion.rs | 34 ++++++------ src/conversions/smallvec.rs | 16 ++++-- src/conversions/std/map.rs | 34 ++++++++---- src/conversions/std/num.rs | 99 +++++++++++++++++++++++++---------- src/conversions/std/set.rs | 32 +++++++---- src/conversions/std/slice.rs | 16 +++--- src/conversions/std/string.rs | 70 ++++++++++++++----------- src/conversions/std/vec.rs | 16 ++++-- src/inspect/types.rs | 6 ++- src/types/boolobject.rs | 15 ++++-- src/types/float.rs | 30 +++++++---- src/types/tuple.rs | 21 ++++---- 13 files changed, 252 insertions(+), 138 deletions(-) create mode 100644 newsfragments/4657.changed.md diff --git a/newsfragments/4657.changed.md b/newsfragments/4657.changed.md new file mode 100644 index 00000000000..38018916001 --- /dev/null +++ b/newsfragments/4657.changed.md @@ -0,0 +1 @@ +`IntoPy::type_output` moved to `IntoPyObject::type_output` \ No newline at end of file diff --git a/src/conversion.rs b/src/conversion.rs index dcf07a61eb3..e551e7c1133 100644 --- a/src/conversion.rs +++ b/src/conversion.rs @@ -164,18 +164,6 @@ pub trait ToPyObject { pub trait IntoPy: Sized { /// Performs the conversion. fn into_py(self, py: Python<'_>) -> T; - - /// Extracts the type hint information for this type when it appears as a return value. - /// - /// For example, `Vec` would return `List[int]`. - /// The default implementation returns `Any`, which is correct for any type. - /// - /// For most types, the return value for this method will be identical to that of [`FromPyObject::type_input`]. - /// It may be different for some types, such as `Dict`, to allow duck-typing: functions return `Dict` but take `Mapping` as argument. - #[cfg(feature = "experimental-inspect")] - fn type_output() -> TypeInfo { - TypeInfo::Any - } } /// Defines a conversion from a Rust type to a Python object, which may fail. @@ -205,6 +193,18 @@ pub trait IntoPyObject<'py>: Sized { /// Performs the conversion. fn into_pyobject(self, py: Python<'py>) -> Result; + /// Extracts the type hint information for this type when it appears as a return value. + /// + /// For example, `Vec` would return `List[int]`. + /// The default implementation returns `Any`, which is correct for any type. + /// + /// For most types, the return value for this method will be identical to that of [`FromPyObject::type_input`]. + /// It may be different for some types, such as `Dict`, to allow duck-typing: functions return `Dict` but take `Mapping` as argument. + #[cfg(feature = "experimental-inspect")] + fn type_output() -> TypeInfo { + TypeInfo::Any + } + /// Converts sequence of Self into a Python object. Used to specialize `Vec`, `[u8; N]` /// and `SmallVec<[u8; N]>` as a sequence of bytes into a `bytes` object. #[doc(hidden)] @@ -379,8 +379,9 @@ pub trait FromPyObject<'py>: Sized { /// For example, `Vec` would return `Sequence[int]`. /// The default implementation returns `Any`, which is correct for any type. /// - /// For most types, the return value for this method will be identical to that of [`IntoPy::type_output`]. - /// It may be different for some types, such as `Dict`, to allow duck-typing: functions return `Dict` but take `Mapping` as argument. + /// For most types, the return value for this method will be identical to that of + /// [`IntoPyObject::type_output`]. It may be different for some types, such as `Dict`, + /// to allow duck-typing: functions return `Dict` but take `Mapping` as argument. #[cfg(feature = "experimental-inspect")] fn type_input() -> TypeInfo { TypeInfo::Any @@ -440,8 +441,9 @@ pub trait FromPyObjectBound<'a, 'py>: Sized + from_py_object_bound_sealed::Seale /// For example, `Vec` would return `Sequence[int]`. /// The default implementation returns `Any`, which is correct for any type. /// - /// For most types, the return value for this method will be identical to that of [`IntoPy::type_output`]. - /// It may be different for some types, such as `Dict`, to allow duck-typing: functions return `Dict` but take `Mapping` as argument. + /// For most types, the return value for this method will be identical to that of + /// [`IntoPyObject::type_output`]. It may be different for some types, such as `Dict`, + /// to allow duck-typing: functions return `Dict` but take `Mapping` as argument. #[cfg(feature = "experimental-inspect")] fn type_input() -> TypeInfo { TypeInfo::Any diff --git a/src/conversions/smallvec.rs b/src/conversions/smallvec.rs index dc8b18437a5..a01f204b73d 100644 --- a/src/conversions/smallvec.rs +++ b/src/conversions/smallvec.rs @@ -51,11 +51,6 @@ where 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<'py, A> IntoPyObject<'py> for SmallVec @@ -75,12 +70,18 @@ where fn into_pyobject(self, py: Python<'py>) -> Result { ::owned_sequence_into_pyobject(self, py, crate::conversion::private::Token) } + + #[cfg(feature = "experimental-inspect")] + fn type_output() -> TypeInfo { + TypeInfo::list_of(A::Item::type_output()) + } } impl<'a, 'py, A> IntoPyObject<'py> for &'a SmallVec where A: Array, &'a A::Item: IntoPyObject<'py>, + A::Item: 'a, // MSRV { type Target = PyAny; type Output = Bound<'py, Self::Target>; @@ -90,6 +91,11 @@ where fn into_pyobject(self, py: Python<'py>) -> Result { self.as_slice().into_pyobject(py) } + + #[cfg(feature = "experimental-inspect")] + fn type_output() -> TypeInfo { + TypeInfo::list_of(<&A::Item>::type_output()) + } } impl<'py, A> FromPyObject<'py> for SmallVec diff --git a/src/conversions/std/map.rs b/src/conversions/std/map.rs index 888bd13c180..388c50b2f3e 100644 --- a/src/conversions/std/map.rs +++ b/src/conversions/std/map.rs @@ -55,11 +55,6 @@ where } dict.into_any().unbind() } - - #[cfg(feature = "experimental-inspect")] - fn type_output() -> TypeInfo { - TypeInfo::dict_of(K::type_output(), V::type_output()) - } } impl<'py, K, V, H> IntoPyObject<'py> for collections::HashMap @@ -79,12 +74,19 @@ where } Ok(dict) } + + #[cfg(feature = "experimental-inspect")] + fn type_output() -> TypeInfo { + TypeInfo::dict_of(K::type_output(), V::type_output()) + } } impl<'a, 'py, K, V, H> IntoPyObject<'py> for &'a collections::HashMap where &'a K: IntoPyObject<'py> + cmp::Eq + hash::Hash, &'a V: IntoPyObject<'py>, + K: 'a, // MSRV + V: 'a, // MSRV H: hash::BuildHasher, { type Target = PyDict; @@ -98,6 +100,11 @@ where } Ok(dict) } + + #[cfg(feature = "experimental-inspect")] + fn type_output() -> TypeInfo { + TypeInfo::dict_of(<&K>::type_output(), <&V>::type_output()) + } } impl IntoPy for collections::BTreeMap @@ -112,11 +119,6 @@ where } dict.into_any().unbind() } - - #[cfg(feature = "experimental-inspect")] - fn type_output() -> TypeInfo { - TypeInfo::dict_of(K::type_output(), V::type_output()) - } } impl<'py, K, V> IntoPyObject<'py> for collections::BTreeMap @@ -135,12 +137,19 @@ where } Ok(dict) } + + #[cfg(feature = "experimental-inspect")] + fn type_output() -> TypeInfo { + TypeInfo::dict_of(K::type_output(), V::type_output()) + } } impl<'a, 'py, K, V> IntoPyObject<'py> for &'a collections::BTreeMap where &'a K: IntoPyObject<'py> + cmp::Eq, &'a V: IntoPyObject<'py>, + K: 'a, + V: 'a, { type Target = PyDict; type Output = Bound<'py, Self::Target>; @@ -153,6 +162,11 @@ where } Ok(dict) } + + #[cfg(feature = "experimental-inspect")] + fn type_output() -> TypeInfo { + TypeInfo::dict_of(<&K>::type_output(), <&V>::type_output()) + } } impl<'py, K, V, S> FromPyObject<'py> for collections::HashMap diff --git a/src/conversions/std/num.rs b/src/conversions/std/num.rs index e7b8addac82..ded970ef9a1 100644 --- a/src/conversions/std/num.rs +++ b/src/conversions/std/num.rs @@ -31,11 +31,6 @@ macro_rules! int_fits_larger_int { fn into_py(self, py: Python<'_>) -> PyObject { self.into_pyobject(py).unwrap().into_any().unbind() } - - #[cfg(feature = "experimental-inspect")] - fn type_output() -> TypeInfo { - <$larger_type>::type_output() - } } impl<'py> IntoPyObject<'py> for $rust_type { @@ -46,6 +41,11 @@ macro_rules! int_fits_larger_int { fn into_pyobject(self, py: Python<'py>) -> Result { (self as $larger_type).into_pyobject(py) } + + #[cfg(feature = "experimental-inspect")] + fn type_output() -> TypeInfo { + <$larger_type>::type_output() + } } impl<'py> IntoPyObject<'py> for &$rust_type { @@ -56,6 +56,11 @@ macro_rules! int_fits_larger_int { fn into_pyobject(self, py: Python<'py>) -> Result { (*self).into_pyobject(py) } + + #[cfg(feature = "experimental-inspect")] + fn type_output() -> TypeInfo { + <$larger_type>::type_output() + } } impl FromPyObject<'_> for $rust_type { @@ -112,11 +117,6 @@ macro_rules! int_convert_u64_or_i64 { fn into_py(self, py: Python<'_>) -> PyObject { self.into_pyobject(py).unwrap().into_any().unbind() } - - #[cfg(feature = "experimental-inspect")] - fn type_output() -> TypeInfo { - TypeInfo::builtin("int") - } } impl<'py> IntoPyObject<'py> for $rust_type { type Target = PyInt; @@ -130,6 +130,11 @@ macro_rules! int_convert_u64_or_i64 { .downcast_into_unchecked()) } } + + #[cfg(feature = "experimental-inspect")] + fn type_output() -> TypeInfo { + TypeInfo::builtin("int") + } } impl<'py> IntoPyObject<'py> for &$rust_type { type Target = PyInt; @@ -140,6 +145,11 @@ macro_rules! int_convert_u64_or_i64 { fn into_pyobject(self, py: Python<'py>) -> Result { (*self).into_pyobject(py) } + + #[cfg(feature = "experimental-inspect")] + fn type_output() -> TypeInfo { + TypeInfo::builtin("int") + } } impl FromPyObject<'_> for $rust_type { fn extract_bound(obj: &Bound<'_, PyAny>) -> PyResult<$rust_type> { @@ -168,11 +178,6 @@ macro_rules! int_fits_c_long { fn into_py(self, py: Python<'_>) -> PyObject { self.into_pyobject(py).unwrap().into_any().unbind() } - - #[cfg(feature = "experimental-inspect")] - fn type_output() -> TypeInfo { - TypeInfo::builtin("int") - } } impl<'py> IntoPyObject<'py> for $rust_type { @@ -187,6 +192,11 @@ macro_rules! int_fits_c_long { .downcast_into_unchecked()) } } + + #[cfg(feature = "experimental-inspect")] + fn type_output() -> TypeInfo { + TypeInfo::builtin("int") + } } impl<'py> IntoPyObject<'py> for &$rust_type { @@ -198,6 +208,11 @@ macro_rules! int_fits_c_long { fn into_pyobject(self, py: Python<'py>) -> Result { (*self).into_pyobject(py) } + + #[cfg(feature = "experimental-inspect")] + fn type_output() -> TypeInfo { + TypeInfo::builtin("int") + } } impl<'py> FromPyObject<'py> for $rust_type { @@ -227,10 +242,6 @@ impl IntoPy for u8 { fn into_py(self, py: Python<'_>) -> PyObject { self.into_pyobject(py).unwrap().into_any().unbind() } - #[cfg(feature = "experimental-inspect")] - fn type_output() -> TypeInfo { - TypeInfo::builtin("int") - } } impl<'py> IntoPyObject<'py> for u8 { type Target = PyInt; @@ -245,6 +256,11 @@ impl<'py> IntoPyObject<'py> for u8 { } } + #[cfg(feature = "experimental-inspect")] + fn type_output() -> TypeInfo { + TypeInfo::builtin("int") + } + #[inline] fn owned_sequence_into_pyobject( iter: I, @@ -267,6 +283,11 @@ impl<'py> IntoPyObject<'py> for &'_ u8 { u8::into_pyobject(*self, py) } + #[cfg(feature = "experimental-inspect")] + fn type_output() -> TypeInfo { + TypeInfo::builtin("int") + } + #[inline] fn borrowed_sequence_into_pyobject( iter: I, @@ -345,11 +366,6 @@ mod fast_128bit_int_conversion { fn into_py(self, py: Python<'_>) -> PyObject { self.into_pyobject(py).unwrap().into_any().unbind() } - - #[cfg(feature = "experimental-inspect")] - fn type_output() -> TypeInfo { - TypeInfo::builtin("int") - } } impl<'py> IntoPyObject<'py> for $rust_type { @@ -399,6 +415,11 @@ mod fast_128bit_int_conversion { } } } + + #[cfg(feature = "experimental-inspect")] + fn type_output() -> TypeInfo { + TypeInfo::builtin("int") + } } impl<'py> IntoPyObject<'py> for &$rust_type { @@ -410,6 +431,11 @@ mod fast_128bit_int_conversion { fn into_pyobject(self, py: Python<'py>) -> Result { (*self).into_pyobject(py) } + + #[cfg(feature = "experimental-inspect")] + fn type_output() -> TypeInfo { + TypeInfo::builtin("int") + } } impl FromPyObject<'_> for $rust_type { @@ -493,11 +519,6 @@ mod slow_128bit_int_conversion { fn into_py(self, py: Python<'_>) -> PyObject { self.into_pyobject(py).unwrap().into_any().unbind() } - - #[cfg(feature = "experimental-inspect")] - fn type_output() -> TypeInfo { - TypeInfo::builtin("int") - } } impl<'py> IntoPyObject<'py> for $rust_type { @@ -518,6 +539,11 @@ mod slow_128bit_int_conversion { .downcast_into_unchecked()) } } + + #[cfg(feature = "experimental-inspect")] + fn type_output() -> TypeInfo { + TypeInfo::builtin("int") + } } impl<'py> IntoPyObject<'py> for &$rust_type { @@ -529,6 +555,11 @@ mod slow_128bit_int_conversion { fn into_pyobject(self, py: Python<'py>) -> Result { (*self).into_pyobject(py) } + + #[cfg(feature = "experimental-inspect")] + fn type_output() -> TypeInfo { + TypeInfo::builtin("int") + } } impl FromPyObject<'_> for $rust_type { @@ -602,6 +633,11 @@ macro_rules! nonzero_int_impl { fn into_pyobject(self, py: Python<'py>) -> Result { self.get().into_pyobject(py) } + + #[cfg(feature = "experimental-inspect")] + fn type_output() -> TypeInfo { + TypeInfo::builtin("int") + } } impl<'py> IntoPyObject<'py> for &$nonzero_type { @@ -613,6 +649,11 @@ macro_rules! nonzero_int_impl { fn into_pyobject(self, py: Python<'py>) -> Result { (*self).into_pyobject(py) } + + #[cfg(feature = "experimental-inspect")] + fn type_output() -> TypeInfo { + TypeInfo::builtin("int") + } } impl FromPyObject<'_> for $nonzero_type { diff --git a/src/conversions/std/set.rs b/src/conversions/std/set.rs index ff5ab572bb5..0bd1b9d7ed6 100644 --- a/src/conversions/std/set.rs +++ b/src/conversions/std/set.rs @@ -51,11 +51,6 @@ where .expect("Failed to create Python set from HashSet") .into() } - - #[cfg(feature = "experimental-inspect")] - fn type_output() -> TypeInfo { - TypeInfo::set_of(K::type_output()) - } } impl<'py, K, S> IntoPyObject<'py> for collections::HashSet @@ -70,11 +65,17 @@ where fn into_pyobject(self, py: Python<'py>) -> Result { try_new_from_iter(py, self) } + + #[cfg(feature = "experimental-inspect")] + fn type_output() -> TypeInfo { + TypeInfo::set_of(K::type_output()) + } } impl<'a, 'py, K, H> IntoPyObject<'py> for &'a collections::HashSet where &'a K: IntoPyObject<'py> + Eq + hash::Hash, + K: 'a, // MSRV H: hash::BuildHasher, { type Target = PySet; @@ -84,6 +85,11 @@ where fn into_pyobject(self, py: Python<'py>) -> Result { try_new_from_iter(py, self.iter()) } + + #[cfg(feature = "experimental-inspect")] + fn type_output() -> TypeInfo { + TypeInfo::set_of(<&K>::type_output()) + } } impl<'py, K, S> FromPyObject<'py> for collections::HashSet @@ -119,11 +125,6 @@ where .expect("Failed to create Python set from BTreeSet") .into() } - - #[cfg(feature = "experimental-inspect")] - fn type_output() -> TypeInfo { - TypeInfo::set_of(K::type_output()) - } } impl<'py, K> IntoPyObject<'py> for collections::BTreeSet @@ -137,11 +138,17 @@ where fn into_pyobject(self, py: Python<'py>) -> Result { try_new_from_iter(py, self) } + + #[cfg(feature = "experimental-inspect")] + fn type_output() -> TypeInfo { + TypeInfo::set_of(K::type_output()) + } } impl<'a, 'py, K> IntoPyObject<'py> for &'a collections::BTreeSet where &'a K: IntoPyObject<'py> + cmp::Ord, + K: 'a, { type Target = PySet; type Output = Bound<'py, Self::Target>; @@ -150,6 +157,11 @@ where fn into_pyobject(self, py: Python<'py>) -> Result { try_new_from_iter(py, self.iter()) } + + #[cfg(feature = "experimental-inspect")] + fn type_output() -> TypeInfo { + TypeInfo::set_of(<&K>::type_output()) + } } impl<'py, K> FromPyObject<'py> for collections::BTreeSet diff --git a/src/conversions/std/slice.rs b/src/conversions/std/slice.rs index eb758271fcf..ea33cc8461b 100644 --- a/src/conversions/std/slice.rs +++ b/src/conversions/std/slice.rs @@ -14,16 +14,12 @@ impl IntoPy for &[u8] { fn into_py(self, py: Python<'_>) -> PyObject { PyBytes::new(py, self).unbind().into() } - - #[cfg(feature = "experimental-inspect")] - fn type_output() -> TypeInfo { - TypeInfo::builtin("bytes") - } } impl<'a, 'py, T> IntoPyObject<'py> for &'a [T] where &'a T: IntoPyObject<'py>, + T: 'a, // MSRV { type Target = PyAny; type Output = Bound<'py, Self::Target>; @@ -37,6 +33,14 @@ where fn into_pyobject(self, py: Python<'py>) -> Result { <&T>::borrowed_sequence_into_pyobject(self, py, crate::conversion::private::Token) } + + #[cfg(feature = "experimental-inspect")] + fn type_output() -> TypeInfo { + TypeInfo::union_of(&[ + TypeInfo::builtin("bytes"), + TypeInfo::list_of(<&T>::type_output()), + ]) + } } impl<'a> crate::conversion::FromPyObjectBound<'a, '_> for &'a [u8] { @@ -46,7 +50,7 @@ impl<'a> crate::conversion::FromPyObjectBound<'a, '_> for &'a [u8] { #[cfg(feature = "experimental-inspect")] fn type_input() -> TypeInfo { - Self::type_output() + TypeInfo::builtin("bytes") } } diff --git a/src/conversions/std/string.rs b/src/conversions/std/string.rs index 667edaaeb5f..b1eff507b7a 100644 --- a/src/conversions/std/string.rs +++ b/src/conversions/std/string.rs @@ -26,11 +26,6 @@ impl IntoPy for &str { fn into_py(self, py: Python<'_>) -> PyObject { self.into_pyobject(py).unwrap().into_any().unbind() } - - #[cfg(feature = "experimental-inspect")] - fn type_output() -> TypeInfo { - ::type_output() - } } impl IntoPy> for &str { @@ -38,11 +33,6 @@ impl IntoPy> for &str { fn into_py(self, py: Python<'_>) -> Py { self.into_pyobject(py).unwrap().unbind() } - - #[cfg(feature = "experimental-inspect")] - fn type_output() -> TypeInfo { - ::type_output() - } } impl<'py> IntoPyObject<'py> for &str { @@ -54,6 +44,11 @@ impl<'py> IntoPyObject<'py> for &str { fn into_pyobject(self, py: Python<'py>) -> Result { Ok(PyString::new(py, self)) } + + #[cfg(feature = "experimental-inspect")] + fn type_output() -> TypeInfo { + ::type_output() + } } impl<'py> IntoPyObject<'py> for &&str { @@ -65,6 +60,11 @@ impl<'py> IntoPyObject<'py> for &&str { fn into_pyobject(self, py: Python<'py>) -> Result { (*self).into_pyobject(py) } + + #[cfg(feature = "experimental-inspect")] + fn type_output() -> TypeInfo { + ::type_output() + } } /// Converts a Rust `Cow<'_, str>` to a Python object. @@ -82,11 +82,6 @@ impl IntoPy for Cow<'_, str> { fn into_py(self, py: Python<'_>) -> PyObject { self.into_pyobject(py).unwrap().into_any().unbind() } - - #[cfg(feature = "experimental-inspect")] - fn type_output() -> TypeInfo { - ::type_output() - } } impl<'py> IntoPyObject<'py> for Cow<'_, str> { @@ -98,6 +93,11 @@ impl<'py> IntoPyObject<'py> for Cow<'_, str> { fn into_pyobject(self, py: Python<'py>) -> Result { (*self).into_pyobject(py) } + + #[cfg(feature = "experimental-inspect")] + fn type_output() -> TypeInfo { + ::type_output() + } } impl<'py> IntoPyObject<'py> for &Cow<'_, str> { @@ -109,6 +109,11 @@ impl<'py> IntoPyObject<'py> for &Cow<'_, str> { fn into_pyobject(self, py: Python<'py>) -> Result { (&**self).into_pyobject(py) } + + #[cfg(feature = "experimental-inspect")] + fn type_output() -> TypeInfo { + ::type_output() + } } /// Converts a Rust `String` to a Python object. @@ -134,11 +139,6 @@ impl IntoPy for char { fn into_py(self, py: Python<'_>) -> PyObject { self.into_pyobject(py).unwrap().into_any().unbind() } - - #[cfg(feature = "experimental-inspect")] - fn type_output() -> TypeInfo { - ::type_output() - } } impl<'py> IntoPyObject<'py> for char { @@ -150,6 +150,11 @@ impl<'py> IntoPyObject<'py> for char { let mut bytes = [0u8; 4]; Ok(PyString::new(py, self.encode_utf8(&mut bytes))) } + + #[cfg(feature = "experimental-inspect")] + fn type_output() -> TypeInfo { + ::type_output() + } } impl<'py> IntoPyObject<'py> for &char { @@ -161,6 +166,11 @@ impl<'py> IntoPyObject<'py> for &char { fn into_pyobject(self, py: Python<'py>) -> Result { (*self).into_pyobject(py) } + + #[cfg(feature = "experimental-inspect")] + fn type_output() -> TypeInfo { + ::type_output() + } } impl IntoPy for String { @@ -168,11 +178,6 @@ impl IntoPy for String { fn into_py(self, py: Python<'_>) -> PyObject { self.into_pyobject(py).unwrap().into_any().unbind() } - - #[cfg(feature = "experimental-inspect")] - fn type_output() -> TypeInfo { - TypeInfo::builtin("str") - } } impl<'py> IntoPyObject<'py> for String { @@ -183,6 +188,11 @@ impl<'py> IntoPyObject<'py> for String { fn into_pyobject(self, py: Python<'py>) -> Result { Ok(PyString::new(py, &self)) } + + #[cfg(feature = "experimental-inspect")] + fn type_output() -> TypeInfo { + TypeInfo::builtin("str") + } } impl IntoPy for &String { @@ -190,11 +200,6 @@ impl IntoPy for &String { fn into_py(self, py: Python<'_>) -> PyObject { self.into_pyobject(py).unwrap().into_any().unbind() } - - #[cfg(feature = "experimental-inspect")] - fn type_output() -> TypeInfo { - ::type_output() - } } impl<'py> IntoPyObject<'py> for &String { @@ -206,6 +211,11 @@ impl<'py> IntoPyObject<'py> for &String { fn into_pyobject(self, py: Python<'py>) -> Result { Ok(PyString::new(py, self)) } + + #[cfg(feature = "experimental-inspect")] + fn type_output() -> TypeInfo { + ::type_output() + } } #[cfg(any(Py_3_10, not(Py_LIMITED_API)))] diff --git a/src/conversions/std/vec.rs b/src/conversions/std/vec.rs index ea3fff117c0..a667460fd82 100644 --- a/src/conversions/std/vec.rs +++ b/src/conversions/std/vec.rs @@ -37,11 +37,6 @@ where let list = new_from_iter(py, &mut iter); list.into() } - - #[cfg(feature = "experimental-inspect")] - fn type_output() -> TypeInfo { - TypeInfo::list_of(T::type_output()) - } } impl<'py, T> IntoPyObject<'py> for Vec @@ -60,11 +55,17 @@ where fn into_pyobject(self, py: Python<'py>) -> Result { T::owned_sequence_into_pyobject(self, py, crate::conversion::private::Token) } + + #[cfg(feature = "experimental-inspect")] + fn type_output() -> TypeInfo { + TypeInfo::list_of(T::type_output()) + } } impl<'a, 'py, T> IntoPyObject<'py> for &'a Vec where &'a T: IntoPyObject<'py>, + T: 'a, // MSRV { type Target = PyAny; type Output = Bound<'py, Self::Target>; @@ -77,6 +78,11 @@ where // above which always returns a `PyAny` for `Vec`. self.as_slice().into_pyobject(py).map(Bound::into_any) } + + #[cfg(feature = "experimental-inspect")] + fn type_output() -> TypeInfo { + TypeInfo::list_of(<&T>::type_output()) + } } #[cfg(test)] diff --git a/src/inspect/types.rs b/src/inspect/types.rs index 13ce62585b1..50674ce96f9 100644 --- a/src/inspect/types.rs +++ b/src/inspect/types.rs @@ -277,6 +277,7 @@ mod test { use crate::inspect::types::{ModuleName, TypeInfo}; + #[track_caller] pub fn assert_display(t: &TypeInfo, expected: &str) { assert_eq!(format!("{}", t), expected) } @@ -405,7 +406,7 @@ mod conversion { use std::collections::{HashMap, HashSet}; use crate::inspect::types::test::assert_display; - use crate::{FromPyObject, IntoPy}; + use crate::{FromPyObject, IntoPyObject}; #[test] fn unsigned_int() { @@ -463,7 +464,8 @@ mod conversion { assert_display(&String::type_output(), "str"); assert_display(&String::type_input(), "str"); - assert_display(&<&[u8]>::type_output(), "bytes"); + assert_display(&<&[u8]>::type_output(), "Union[bytes, List[int]]"); + assert_display(&<&[String]>::type_output(), "Union[bytes, List[str]]"); assert_display( &<&[u8] as crate::conversion::FromPyObjectBound>::type_input(), "bytes", diff --git a/src/types/boolobject.rs b/src/types/boolobject.rs index 6f90329900d..d54bddc8848 100644 --- a/src/types/boolobject.rs +++ b/src/types/boolobject.rs @@ -160,11 +160,6 @@ impl IntoPy for bool { fn into_py(self, py: Python<'_>) -> PyObject { self.into_pyobject(py).unwrap().into_any().unbind() } - - #[cfg(feature = "experimental-inspect")] - fn type_output() -> TypeInfo { - TypeInfo::builtin("bool") - } } impl<'py> IntoPyObject<'py> for bool { @@ -176,6 +171,11 @@ impl<'py> IntoPyObject<'py> for bool { fn into_pyobject(self, py: Python<'py>) -> Result { Ok(PyBool::new(py, self)) } + + #[cfg(feature = "experimental-inspect")] + fn type_output() -> TypeInfo { + TypeInfo::builtin("bool") + } } impl<'py> IntoPyObject<'py> for &bool { @@ -187,6 +187,11 @@ impl<'py> IntoPyObject<'py> for &bool { fn into_pyobject(self, py: Python<'py>) -> Result { (*self).into_pyobject(py) } + + #[cfg(feature = "experimental-inspect")] + fn type_output() -> TypeInfo { + TypeInfo::builtin("bool") + } } /// Converts a Python `bool` to a Rust `bool`. diff --git a/src/types/float.rs b/src/types/float.rs index f9bf673ac98..8438629e577 100644 --- a/src/types/float.rs +++ b/src/types/float.rs @@ -91,11 +91,6 @@ impl IntoPy for f64 { fn into_py(self, py: Python<'_>) -> PyObject { self.into_pyobject(py).unwrap().into_any().unbind() } - - #[cfg(feature = "experimental-inspect")] - fn type_output() -> TypeInfo { - TypeInfo::builtin("float") - } } impl<'py> IntoPyObject<'py> for f64 { @@ -107,6 +102,11 @@ impl<'py> IntoPyObject<'py> for f64 { fn into_pyobject(self, py: Python<'py>) -> Result { Ok(PyFloat::new(py, self)) } + + #[cfg(feature = "experimental-inspect")] + fn type_output() -> TypeInfo { + TypeInfo::builtin("float") + } } impl<'py> IntoPyObject<'py> for &f64 { @@ -118,6 +118,11 @@ impl<'py> IntoPyObject<'py> for &f64 { fn into_pyobject(self, py: Python<'py>) -> Result { (*self).into_pyobject(py) } + + #[cfg(feature = "experimental-inspect")] + fn type_output() -> TypeInfo { + TypeInfo::builtin("float") + } } impl<'py> FromPyObject<'py> for f64 { @@ -163,11 +168,6 @@ impl IntoPy for f32 { fn into_py(self, py: Python<'_>) -> PyObject { self.into_pyobject(py).unwrap().into_any().unbind() } - - #[cfg(feature = "experimental-inspect")] - fn type_output() -> TypeInfo { - TypeInfo::builtin("float") - } } impl<'py> IntoPyObject<'py> for f32 { @@ -179,6 +179,11 @@ impl<'py> IntoPyObject<'py> for f32 { fn into_pyobject(self, py: Python<'py>) -> Result { Ok(PyFloat::new(py, self.into())) } + + #[cfg(feature = "experimental-inspect")] + fn type_output() -> TypeInfo { + TypeInfo::builtin("float") + } } impl<'py> IntoPyObject<'py> for &f32 { @@ -190,6 +195,11 @@ impl<'py> IntoPyObject<'py> for &f32 { fn into_pyobject(self, py: Python<'py>) -> Result { (*self).into_pyobject(py) } + + #[cfg(feature = "experimental-inspect")] + fn type_output() -> TypeInfo { + TypeInfo::builtin("float") + } } impl<'py> FromPyObject<'py> for f32 { diff --git a/src/types/tuple.rs b/src/types/tuple.rs index 189cec126bd..1b6a95abb14 100644 --- a/src/types/tuple.rs +++ b/src/types/tuple.rs @@ -529,11 +529,6 @@ macro_rules! tuple_conversion ({$length:expr,$(($refN:ident, $n:tt, $T:ident)),+ fn into_py(self, py: Python<'_>) -> PyObject { array_into_tuple(py, [$(self.$n.into_py(py)),+]).into() } - - #[cfg(feature = "experimental-inspect")] - fn type_output() -> TypeInfo { - TypeInfo::Tuple(Some(vec![$( $T::type_output() ),+])) - } } impl <'py, $($T),+> IntoPyObject<'py> for ($($T,)+) @@ -547,11 +542,17 @@ macro_rules! tuple_conversion ({$length:expr,$(($refN:ident, $n:tt, $T:ident)),+ fn into_pyobject(self, py: Python<'py>) -> Result { Ok(array_into_tuple(py, [$(self.$n.into_pyobject(py).map_err(Into::into)?.into_any().unbind()),+]).into_bound(py)) } + + #[cfg(feature = "experimental-inspect")] + fn type_output() -> TypeInfo { + TypeInfo::Tuple(Some(vec![$( $T::type_output() ),+])) + } } impl <'a, 'py, $($T),+> IntoPyObject<'py> for &'a ($($T,)+) where $(&'a $T: IntoPyObject<'py>,)+ + $($T: 'a,)+ // MSRV { type Target = PyTuple; type Output = Bound<'py, Self::Target>; @@ -560,17 +561,17 @@ macro_rules! tuple_conversion ({$length:expr,$(($refN:ident, $n:tt, $T:ident)),+ fn into_pyobject(self, py: Python<'py>) -> Result { Ok(array_into_tuple(py, [$(self.$n.into_pyobject(py).map_err(Into::into)?.into_any().unbind()),+]).into_bound(py)) } + + #[cfg(feature = "experimental-inspect")] + fn type_output() -> TypeInfo { + TypeInfo::Tuple(Some(vec![$( <&$T>::type_output() ),+])) + } } impl <$($T: IntoPy),+> IntoPy> for ($($T,)+) { fn into_py(self, py: Python<'_>) -> Py { array_into_tuple(py, [$(self.$n.into_py(py)),+]) } - - #[cfg(feature = "experimental-inspect")] - fn type_output() -> TypeInfo { - TypeInfo::Tuple(Some(vec![$( $T::type_output() ),+])) - } } impl<'py, $($T: FromPyObject<'py>),+> FromPyObject<'py> for ($($T,)+) { From 81c5bdfdf3a81d0fe5758caf45a0904e5d53d4f9 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Sat, 26 Oct 2024 12:35:02 -0400 Subject: [PATCH 345/495] Update comments now that a std API is stable, but too high MSRV (#4658) --- src/buffer.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/buffer.rs b/src/buffer.rs index 85144f6ff99..2d94681a5c7 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -191,14 +191,14 @@ impl FromPyObject<'_> for PyBuffer { impl PyBuffer { /// Gets the underlying buffer from the specified python object. pub fn get(obj: &Bound<'_, PyAny>) -> PyResult> { - // TODO: use nightly API Box::new_uninit() once stable + // TODO: use nightly API Box::new_uninit() once our MSRV is 1.82 let mut buf = Box::new(mem::MaybeUninit::uninit()); let buf: Box = { err::error_on_minusone(obj.py(), unsafe { ffi::PyObject_GetBuffer(obj.as_ptr(), buf.as_mut_ptr(), ffi::PyBUF_FULL_RO) })?; // Safety: buf is initialized by PyObject_GetBuffer. - // TODO: use nightly API Box::assume_init() once stable + // TODO: use nightly API Box::assume_init() once our MSRV is 1.82 unsafe { mem::transmute(buf) } }; // Create PyBuffer immediately so that if validation checks fail, the PyBuffer::drop code From d168e60ca499f4b9746551c467c65f8657c574d9 Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Sat, 26 Oct 2024 21:31:09 +0200 Subject: [PATCH 346/495] switch `PyErrArguments` blanket impl to `IntoPyObject` (#4660) --- src/err/mod.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/err/mod.rs b/src/err/mod.rs index 14c368938f1..d65eac171f6 100644 --- a/src/err/mod.rs +++ b/src/err/mod.rs @@ -101,10 +101,14 @@ pub trait PyErrArguments: Send + Sync { impl PyErrArguments for T where - T: IntoPy + Send + Sync, + T: for<'py> IntoPyObject<'py> + Send + Sync, { fn arguments(self, py: Python<'_>) -> PyObject { - self.into_py(py) + // FIXME: `arguments` should become fallible + match self.into_pyobject(py) { + Ok(obj) => obj.into_any().unbind(), + Err(e) => panic!("Converting PyErr arguments failed: {}", e.into()), + } } } From 2d3bdc02e5e2b99bcd1399a9172f6ff8ea504d32 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Sun, 27 Oct 2024 20:20:11 +0000 Subject: [PATCH 347/495] eagerly normalize fetched exception on Python 3.11 and older (#4655) * eagerly normalize fetched exception on Python 3.11 and older * remove now-redundant import * remove another now-redundant import --- newsfragments/4655.changed.md | 1 + src/err/err_state.rs | 101 +++++++++++++++++----------------- src/err/mod.rs | 54 ------------------ 3 files changed, 50 insertions(+), 106 deletions(-) create mode 100644 newsfragments/4655.changed.md diff --git a/newsfragments/4655.changed.md b/newsfragments/4655.changed.md new file mode 100644 index 00000000000..544fac4973f --- /dev/null +++ b/newsfragments/4655.changed.md @@ -0,0 +1 @@ +Eagerly normalize exceptions in `PyErr::take()` and `PyErr::fetch()` on Python 3.11 and older. diff --git a/src/err/err_state.rs b/src/err/err_state.rs index 58303b46f32..2ba153b6ef8 100644 --- a/src/err/err_state.rs +++ b/src/err/err_state.rs @@ -3,7 +3,8 @@ use std::cell::UnsafeCell; use crate::{ exceptions::{PyBaseException, PyTypeError}, ffi, - types::{PyTraceback, PyType}, + ffi_ptr_ext::FfiPtrExt, + types::{PyAnyMethods, PyTraceback, PyType}, Bound, Py, PyAny, PyErrArguments, PyObject, PyTypeInfo, Python, }; @@ -34,19 +35,6 @@ impl PyErrState { }))) } - #[cfg(not(Py_3_12))] - pub(crate) fn ffi_tuple( - ptype: PyObject, - pvalue: Option, - ptraceback: Option, - ) -> Self { - Self::from_inner(PyErrStateInner::FfiTuple { - ptype, - pvalue, - ptraceback, - }) - } - pub(crate) fn normalized(normalized: PyErrStateNormalized) -> Self { Self::from_inner(PyErrStateInner::Normalized(normalized)) } @@ -114,9 +102,6 @@ pub(crate) struct PyErrStateNormalized { impl PyErrStateNormalized { pub(crate) fn new(pvalue: Bound<'_, PyBaseException>) -> Self { - #[cfg(not(Py_3_12))] - use crate::types::any::PyAnyMethods; - Self { #[cfg(not(Py_3_12))] ptype: pvalue.get_type().into(), @@ -138,7 +123,6 @@ impl PyErrStateNormalized { #[cfg(Py_3_12)] pub(crate) fn ptype<'py>(&self, py: Python<'py>) -> Bound<'py, PyType> { - use crate::types::any::PyAnyMethods; self.pvalue.bind(py).get_type() } @@ -151,8 +135,6 @@ impl PyErrStateNormalized { #[cfg(Py_3_12)] pub(crate) fn ptraceback<'py>(&self, py: Python<'py>) -> Option> { - use crate::ffi_ptr_ext::FfiPtrExt; - use crate::types::any::PyAnyMethods; unsafe { ffi::PyException_GetTraceback(self.pvalue.as_ptr()) .assume_owned_or_opt(py) @@ -160,10 +142,54 @@ impl PyErrStateNormalized { } } - #[cfg(Py_3_12)] pub(crate) fn take(py: Python<'_>) -> Option { - unsafe { Py::from_owned_ptr_or_opt(py, ffi::PyErr_GetRaisedException()) } - .map(|pvalue| PyErrStateNormalized { pvalue }) + #[cfg(Py_3_12)] + { + // Safety: PyErr_GetRaisedException can be called when attached to Python and + // returns either NULL or an owned reference. + unsafe { ffi::PyErr_GetRaisedException().assume_owned_or_opt(py) }.map(|pvalue| { + PyErrStateNormalized { + // Safety: PyErr_GetRaisedException returns a valid exception type. + pvalue: unsafe { pvalue.downcast_into_unchecked() }.unbind(), + } + }) + } + + #[cfg(not(Py_3_12))] + { + let (ptype, pvalue, ptraceback) = unsafe { + let mut ptype: *mut ffi::PyObject = std::ptr::null_mut(); + let mut pvalue: *mut ffi::PyObject = std::ptr::null_mut(); + let mut ptraceback: *mut ffi::PyObject = std::ptr::null_mut(); + + ffi::PyErr_Fetch(&mut ptype, &mut pvalue, &mut ptraceback); + + // Ensure that the exception coming from the interpreter is normalized. + if !ptype.is_null() { + ffi::PyErr_NormalizeException(&mut ptype, &mut pvalue, &mut ptraceback); + } + + // Safety: PyErr_NormalizeException will have produced up to three owned + // references of the correct types. + ( + ptype + .assume_owned_or_opt(py) + .map(|b| b.downcast_into_unchecked()), + pvalue + .assume_owned_or_opt(py) + .map(|b| b.downcast_into_unchecked()), + ptraceback + .assume_owned_or_opt(py) + .map(|b| b.downcast_into_unchecked()), + ) + }; + + ptype.map(|ptype| PyErrStateNormalized { + ptype: ptype.unbind(), + pvalue: pvalue.expect("normalized exception value missing").unbind(), + ptraceback: ptraceback.map(Bound::unbind), + }) + } } #[cfg(not(Py_3_12))] @@ -204,12 +230,6 @@ pub(crate) type PyErrStateLazyFn = enum PyErrStateInner { Lazy(Box), - #[cfg(not(Py_3_12))] - FfiTuple { - ptype: PyObject, - pvalue: Option, - ptraceback: Option, - }, Normalized(PyErrStateNormalized), } @@ -231,20 +251,6 @@ impl PyErrStateInner { PyErrStateNormalized::take(py) .expect("exception missing after writing to the interpreter") } - #[cfg(not(Py_3_12))] - PyErrStateInner::FfiTuple { - ptype, - pvalue, - ptraceback, - } => { - let mut ptype = ptype.into_ptr(); - let mut pvalue = pvalue.map_or(std::ptr::null_mut(), Py::into_ptr); - let mut ptraceback = ptraceback.map_or(std::ptr::null_mut(), Py::into_ptr); - unsafe { - ffi::PyErr_NormalizeException(&mut ptype, &mut pvalue, &mut ptraceback); - PyErrStateNormalized::from_normalized_ffi_tuple(py, ptype, pvalue, ptraceback) - } - } PyErrStateInner::Normalized(normalized) => normalized, } } @@ -253,15 +259,6 @@ impl PyErrStateInner { fn restore(self, py: Python<'_>) { let (ptype, pvalue, ptraceback) = match self { PyErrStateInner::Lazy(lazy) => lazy_into_normalized_ffi_tuple(py, lazy), - PyErrStateInner::FfiTuple { - ptype, - pvalue, - ptraceback, - } => ( - ptype.into_ptr(), - pvalue.map_or(std::ptr::null_mut(), Py::into_ptr), - ptraceback.map_or(std::ptr::null_mut(), Py::into_ptr), - ), PyErrStateInner::Normalized(PyErrStateNormalized { ptype, pvalue, diff --git a/src/err/mod.rs b/src/err/mod.rs index d65eac171f6..6d8259a2919 100644 --- a/src/err/mod.rs +++ b/src/err/mod.rs @@ -359,60 +359,6 @@ impl PyErr { /// expected to have been set, for example from [`PyErr::occurred`] or by an error return value /// from a C FFI function, use [`PyErr::fetch`]. pub fn take(py: Python<'_>) -> Option { - Self::_take(py) - } - - #[cfg(not(Py_3_12))] - fn _take(py: Python<'_>) -> Option { - let (ptype, pvalue, ptraceback) = unsafe { - let mut ptype: *mut ffi::PyObject = std::ptr::null_mut(); - let mut pvalue: *mut ffi::PyObject = std::ptr::null_mut(); - let mut ptraceback: *mut ffi::PyObject = std::ptr::null_mut(); - ffi::PyErr_Fetch(&mut ptype, &mut pvalue, &mut ptraceback); - - // Convert to Py immediately so that any references are freed by early return. - let ptype = PyObject::from_owned_ptr_or_opt(py, ptype); - let pvalue = PyObject::from_owned_ptr_or_opt(py, pvalue); - let ptraceback = PyObject::from_owned_ptr_or_opt(py, ptraceback); - - // A valid exception state should always have a non-null ptype, but the other two may be - // null. - let ptype = match ptype { - Some(ptype) => ptype, - None => { - debug_assert!( - pvalue.is_none(), - "Exception type was null but value was not null" - ); - debug_assert!( - ptraceback.is_none(), - "Exception type was null but traceback was not null" - ); - return None; - } - }; - - (ptype, pvalue, ptraceback) - }; - - if ptype.as_ptr() == PanicException::type_object_raw(py).cast() { - let msg = pvalue - .as_ref() - .and_then(|obj| obj.bind(py).str().ok()) - .map(|py_str| py_str.to_string_lossy().into()) - .unwrap_or_else(|| String::from("Unwrapped panic from Python code")); - - let state = PyErrState::ffi_tuple(ptype, pvalue, ptraceback); - Self::print_panic_and_unwind(py, state, msg) - } - - Some(PyErr::from_state(PyErrState::ffi_tuple( - ptype, pvalue, ptraceback, - ))) - } - - #[cfg(Py_3_12)] - fn _take(py: Python<'_>) -> Option { let state = PyErrStateNormalized::take(py)?; let pvalue = state.pvalue.bind(py); if pvalue.get_type().as_ptr() == PanicException::type_object_raw(py).cast() { From 0aa13c80063f7c62c96285313c6260b429060b1b Mon Sep 17 00:00:00 2001 From: Kevin Matlock Date: Mon, 28 Oct 2024 22:58:20 -0700 Subject: [PATCH 348/495] feat: Adds the Bound<'_, PyMappingProxy> type (#4644) * Mappingproxy (#1) Adds in the MappingProxy type. * Move over from `iter` to `try_iter`. * Added lifetime to `try_iter`, preventing need to clone when iterating. * Remove unneccessary borrow. * Add newsfragment * Newline to newsfragment. * Remove explicit lifetime, * Review comments (#2) * Addressing more comments * Remove extract_bound. * Remove extract methods. * Update comments for list return type. --------- Co-authored-by: Kevin Matlock --- newsfragments/4644.added.md | 1 + src/prelude.rs | 1 + src/sealed.rs | 5 +- src/types/mappingproxy.rs | 557 ++++++++++++++++++++++++++++++++++++ src/types/mod.rs | 2 + 5 files changed, 564 insertions(+), 2 deletions(-) create mode 100644 newsfragments/4644.added.md create mode 100644 src/types/mappingproxy.rs diff --git a/newsfragments/4644.added.md b/newsfragments/4644.added.md new file mode 100644 index 00000000000..4b4a277abf8 --- /dev/null +++ b/newsfragments/4644.added.md @@ -0,0 +1 @@ +New `PyMappingProxy` struct corresponing to the `mappingproxy` class in Python. diff --git a/src/prelude.rs b/src/prelude.rs index cc44a199611..7624d37a1e9 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -38,6 +38,7 @@ pub use crate::types::float::PyFloatMethods; pub use crate::types::frozenset::PyFrozenSetMethods; pub use crate::types::list::PyListMethods; pub use crate::types::mapping::PyMappingMethods; +pub use crate::types::mappingproxy::PyMappingProxyMethods; pub use crate::types::module::PyModuleMethods; pub use crate::types::sequence::PySequenceMethods; pub use crate::types::set::PySetMethods; diff --git a/src/sealed.rs b/src/sealed.rs index 62b47e131a7..cc835bee3b8 100644 --- a/src/sealed.rs +++ b/src/sealed.rs @@ -1,7 +1,7 @@ use crate::types::{ PyBool, PyByteArray, PyBytes, PyCapsule, PyComplex, PyDict, PyFloat, PyFrozenSet, PyList, - PyMapping, PyModule, PySequence, PySet, PySlice, PyString, PyTraceback, PyTuple, PyType, - PyWeakref, PyWeakrefProxy, PyWeakrefReference, + PyMapping, PyMappingProxy, PyModule, PySequence, PySet, PySlice, PyString, PyTraceback, + PyTuple, PyType, PyWeakref, PyWeakrefProxy, PyWeakrefReference, }; use crate::{ffi, Bound, PyAny, PyResult}; @@ -33,6 +33,7 @@ impl Sealed for Bound<'_, PyFloat> {} impl Sealed for Bound<'_, PyFrozenSet> {} impl Sealed for Bound<'_, PyList> {} impl Sealed for Bound<'_, PyMapping> {} +impl Sealed for Bound<'_, PyMappingProxy> {} impl Sealed for Bound<'_, PyModule> {} impl Sealed for Bound<'_, PySequence> {} impl Sealed for Bound<'_, PySet> {} diff --git a/src/types/mappingproxy.rs b/src/types/mappingproxy.rs new file mode 100644 index 00000000000..fc28687c561 --- /dev/null +++ b/src/types/mappingproxy.rs @@ -0,0 +1,557 @@ +// Copyright (c) 2017-present PyO3 Project and Contributors + +use super::PyMapping; +use crate::err::PyResult; +use crate::ffi_ptr_ext::FfiPtrExt; +use crate::instance::Bound; +use crate::types::any::PyAnyMethods; +use crate::types::{PyAny, PyIterator, PyList}; +use crate::{ffi, Python}; + +use std::os::raw::c_int; + +/// Represents a Python `mappingproxy`. +#[repr(transparent)] +pub struct PyMappingProxy(PyAny); + +#[inline] +unsafe fn dict_proxy_check(op: *mut ffi::PyObject) -> c_int { + ffi::Py_IS_TYPE(op, std::ptr::addr_of_mut!(ffi::PyDictProxy_Type)) +} + +pyobject_native_type_core!( + PyMappingProxy, + pyobject_native_static_type_object!(ffi::PyDictProxy_Type), + #checkfunction=dict_proxy_check +); + +impl PyMappingProxy { + /// Creates a mappingproxy from an object. + pub fn new<'py>( + py: Python<'py>, + elements: &Bound<'py, PyMapping>, + ) -> Bound<'py, PyMappingProxy> { + unsafe { + ffi::PyDictProxy_New(elements.as_ptr()) + .assume_owned(py) + .downcast_into_unchecked() + } + } +} + +/// Implementation of functionality for [`PyMappingProxy`]. +/// +/// These methods are defined for the `Bound<'py, PyMappingProxy>` smart pointer, so to use method call +/// syntax these methods are separated into a trait, because stable Rust does not yet support +/// `arbitrary_self_types`. +#[doc(alias = "PyMappingProxy")] +pub trait PyMappingProxyMethods<'py, 'a>: crate::sealed::Sealed { + /// Checks if the mappingproxy is empty, i.e. `len(self) == 0`. + fn is_empty(&self) -> PyResult; + + /// Returns a list containing all keys in the mapping. + fn keys(&self) -> PyResult>; + + /// Returns a list containing all values in the mapping. + fn values(&self) -> PyResult>; + + /// Returns a list of tuples of all (key, value) pairs in the mapping. + fn items(&self) -> PyResult>; + + /// Returns `self` cast as a `PyMapping`. + fn as_mapping(&self) -> &Bound<'py, PyMapping>; + + /// Takes an object and returns an iterator for it. Returns an error if the object is not + /// iterable. + fn try_iter(&'a self) -> PyResult>; +} + +impl<'py, 'a> PyMappingProxyMethods<'py, 'a> for Bound<'py, PyMappingProxy> { + fn is_empty(&self) -> PyResult { + Ok(self.len()? == 0) + } + + #[inline] + fn keys(&self) -> PyResult> { + unsafe { + Ok(ffi::PyMapping_Keys(self.as_ptr()) + .assume_owned_or_err(self.py())? + .downcast_into_unchecked()) + } + } + + #[inline] + fn values(&self) -> PyResult> { + unsafe { + Ok(ffi::PyMapping_Values(self.as_ptr()) + .assume_owned_or_err(self.py())? + .downcast_into_unchecked()) + } + } + + #[inline] + fn items(&self) -> PyResult> { + unsafe { + Ok(ffi::PyMapping_Items(self.as_ptr()) + .assume_owned_or_err(self.py())? + .downcast_into_unchecked()) + } + } + + fn as_mapping(&self) -> &Bound<'py, PyMapping> { + unsafe { self.downcast_unchecked() } + } + + fn try_iter(&'a self) -> PyResult> { + Ok(BoundMappingProxyIterator { + iterator: PyIterator::from_object(self)?, + mappingproxy: self, + }) + } +} + +pub struct BoundMappingProxyIterator<'py, 'a> { + iterator: Bound<'py, PyIterator>, + mappingproxy: &'a Bound<'py, PyMappingProxy>, +} + +impl<'py> Iterator for BoundMappingProxyIterator<'py, '_> { + type Item = PyResult<(Bound<'py, PyAny>, Bound<'py, PyAny>)>; + + #[inline] + fn next(&mut self) -> Option { + self.iterator.next().map(|key| match key { + Ok(key) => match self.mappingproxy.get_item(&key) { + Ok(value) => Ok((key, value)), + Err(e) => Err(e), + }, + Err(e) => Err(e), + }) + } +} + +#[cfg(test)] +mod tests { + + use super::*; + use crate::types::dict::*; + use crate::Python; + use crate::{ + exceptions::PyKeyError, + types::{PyInt, PyTuple}, + }; + use std::collections::{BTreeMap, HashMap}; + + #[test] + fn test_new() { + Python::with_gil(|py| { + let pydict = [(7, 32)].into_py_dict(py).unwrap(); + let mappingproxy = PyMappingProxy::new(py, pydict.as_mapping()); + mappingproxy.get_item(7i32).unwrap(); + assert_eq!( + 32, + mappingproxy + .get_item(7i32) + .unwrap() + .extract::() + .unwrap() + ); + assert!(mappingproxy + .get_item(8i32) + .unwrap_err() + .is_instance_of::(py)); + }); + } + + #[test] + fn test_len() { + Python::with_gil(|py| { + let mut v = HashMap::new(); + let dict = v.clone().into_py_dict(py).unwrap(); + let mappingproxy = PyMappingProxy::new(py, dict.as_mapping()); + assert_eq!(mappingproxy.len().unwrap(), 0); + v.insert(7, 32); + let dict2 = v.clone().into_py_dict(py).unwrap(); + let mp2 = PyMappingProxy::new(py, dict2.as_mapping()); + assert_eq!(mp2.len().unwrap(), 1); + }); + } + + #[test] + fn test_contains() { + Python::with_gil(|py| { + let mut v = HashMap::new(); + v.insert(7, 32); + let dict = v.clone().into_py_dict(py).unwrap(); + let mappingproxy = PyMappingProxy::new(py, dict.as_mapping()); + assert!(mappingproxy.contains(7i32).unwrap()); + assert!(!mappingproxy.contains(8i32).unwrap()); + }); + } + + #[test] + fn test_get_item() { + Python::with_gil(|py| { + let mut v = HashMap::new(); + v.insert(7, 32); + let dict = v.clone().into_py_dict(py).unwrap(); + let mappingproxy = PyMappingProxy::new(py, dict.as_mapping()); + assert_eq!( + 32, + mappingproxy + .get_item(7i32) + .unwrap() + .extract::() + .unwrap() + ); + assert!(mappingproxy + .get_item(8i32) + .unwrap_err() + .is_instance_of::(py)); + }); + } + + #[test] + fn test_set_item_refcnt() { + Python::with_gil(|py| { + let cnt; + { + let none = py.None(); + cnt = none.get_refcnt(py); + let dict = [(10, none)].into_py_dict(py).unwrap(); + let _mappingproxy = PyMappingProxy::new(py, dict.as_mapping()); + } + { + assert_eq!(cnt, py.None().get_refcnt(py)); + } + }); + } + + #[test] + fn test_isempty() { + Python::with_gil(|py| { + let map: HashMap = HashMap::new(); + let dict = map.into_py_dict(py).unwrap(); + let mappingproxy = PyMappingProxy::new(py, dict.as_mapping()); + assert!(mappingproxy.is_empty().unwrap()); + }); + } + + #[test] + fn test_keys() { + Python::with_gil(|py| { + let mut v = HashMap::new(); + v.insert(7, 32); + v.insert(8, 42); + v.insert(9, 123); + let dict = v.into_py_dict(py).unwrap(); + let mappingproxy = PyMappingProxy::new(py, dict.as_mapping()); + // Can't just compare against a vector of tuples since we don't have a guaranteed ordering. + let mut key_sum = 0; + for el in mappingproxy.keys().unwrap().try_iter().unwrap() { + key_sum += el.unwrap().extract::().unwrap(); + } + assert_eq!(7 + 8 + 9, key_sum); + }); + } + + #[test] + fn test_values() { + Python::with_gil(|py| { + let mut v: HashMap = HashMap::new(); + v.insert(7, 32); + v.insert(8, 42); + v.insert(9, 123); + let dict = v.into_py_dict(py).unwrap(); + let mappingproxy = PyMappingProxy::new(py, dict.as_mapping()); + // Can't just compare against a vector of tuples since we don't have a guaranteed ordering. + let mut values_sum = 0; + for el in mappingproxy.values().unwrap().try_iter().unwrap() { + values_sum += el.unwrap().extract::().unwrap(); + } + assert_eq!(32 + 42 + 123, values_sum); + }); + } + + #[test] + fn test_items() { + Python::with_gil(|py| { + let mut v = HashMap::new(); + v.insert(7, 32); + v.insert(8, 42); + v.insert(9, 123); + let dict = v.into_py_dict(py).unwrap(); + let mappingproxy = PyMappingProxy::new(py, dict.as_mapping()); + // Can't just compare against a vector of tuples since we don't have a guaranteed ordering. + let mut key_sum = 0; + let mut value_sum = 0; + for res in mappingproxy.items().unwrap().try_iter().unwrap() { + let el = res.unwrap(); + let tuple = el.downcast::().unwrap(); + key_sum += tuple.get_item(0).unwrap().extract::().unwrap(); + value_sum += tuple.get_item(1).unwrap().extract::().unwrap(); + } + assert_eq!(7 + 8 + 9, key_sum); + assert_eq!(32 + 42 + 123, value_sum); + }); + } + + #[test] + fn test_iter() { + Python::with_gil(|py| { + let mut v = HashMap::new(); + v.insert(7, 32); + v.insert(8, 42); + v.insert(9, 123); + let dict = v.into_py_dict(py).unwrap(); + let mappingproxy = PyMappingProxy::new(py, dict.as_mapping()); + let mut key_sum = 0; + let mut value_sum = 0; + for res in mappingproxy.try_iter().unwrap() { + let (key, value) = res.unwrap(); + key_sum += key.extract::().unwrap(); + value_sum += value.extract::().unwrap(); + } + assert_eq!(7 + 8 + 9, key_sum); + assert_eq!(32 + 42 + 123, value_sum); + }); + } + + #[test] + fn test_hashmap_into_python() { + Python::with_gil(|py| { + let mut map = HashMap::::new(); + map.insert(1, 1); + + let dict = map.clone().into_py_dict(py).unwrap(); + let py_map = PyMappingProxy::new(py, dict.as_mapping()); + + assert_eq!(py_map.len().unwrap(), 1); + assert_eq!(py_map.get_item(1).unwrap().extract::().unwrap(), 1); + }); + } + + #[test] + fn test_hashmap_into_mappingproxy() { + Python::with_gil(|py| { + let mut map = HashMap::::new(); + map.insert(1, 1); + + let dict = map.clone().into_py_dict(py).unwrap(); + let py_map = PyMappingProxy::new(py, dict.as_mapping()); + + assert_eq!(py_map.len().unwrap(), 1); + assert_eq!(py_map.get_item(1).unwrap().extract::().unwrap(), 1); + }); + } + + #[test] + fn test_btreemap_into_py() { + Python::with_gil(|py| { + let mut map = BTreeMap::::new(); + map.insert(1, 1); + + let dict = map.clone().into_py_dict(py).unwrap(); + let py_map = PyMappingProxy::new(py, dict.as_mapping()); + + assert_eq!(py_map.len().unwrap(), 1); + assert_eq!(py_map.get_item(1).unwrap().extract::().unwrap(), 1); + }); + } + + #[test] + fn test_btreemap_into_mappingproxy() { + Python::with_gil(|py| { + let mut map = BTreeMap::::new(); + map.insert(1, 1); + + let dict = map.clone().into_py_dict(py).unwrap(); + let py_map = PyMappingProxy::new(py, dict.as_mapping()); + + assert_eq!(py_map.len().unwrap(), 1); + assert_eq!(py_map.get_item(1).unwrap().extract::().unwrap(), 1); + }); + } + + #[test] + fn test_vec_into_mappingproxy() { + Python::with_gil(|py| { + let vec = vec![("a", 1), ("b", 2), ("c", 3)]; + let dict = vec.clone().into_py_dict(py).unwrap(); + let py_map = PyMappingProxy::new(py, dict.as_mapping()); + + assert_eq!(py_map.len().unwrap(), 3); + assert_eq!(py_map.get_item("b").unwrap().extract::().unwrap(), 2); + }); + } + + #[test] + fn test_slice_into_mappingproxy() { + Python::with_gil(|py| { + let arr = [("a", 1), ("b", 2), ("c", 3)]; + + let dict = arr.into_py_dict(py).unwrap(); + let py_map = PyMappingProxy::new(py, dict.as_mapping()); + + assert_eq!(py_map.len().unwrap(), 3); + assert_eq!(py_map.get_item("b").unwrap().extract::().unwrap(), 2); + }); + } + + #[test] + fn mappingproxy_as_mapping() { + Python::with_gil(|py| { + let mut map = HashMap::::new(); + map.insert(1, 1); + + let dict = map.clone().into_py_dict(py).unwrap(); + let py_map = PyMappingProxy::new(py, dict.as_mapping()); + + assert_eq!(py_map.as_mapping().len().unwrap(), 1); + assert_eq!( + py_map + .as_mapping() + .get_item(1) + .unwrap() + .extract::() + .unwrap(), + 1 + ); + }); + } + + #[cfg(not(PyPy))] + fn abc_mappingproxy(py: Python<'_>) -> Bound<'_, PyMappingProxy> { + let mut map = HashMap::<&'static str, i32>::new(); + map.insert("a", 1); + map.insert("b", 2); + map.insert("c", 3); + let dict = map.clone().into_py_dict(py).unwrap(); + PyMappingProxy::new(py, dict.as_mapping()) + } + + #[test] + #[cfg(not(PyPy))] + fn mappingproxy_keys_view() { + Python::with_gil(|py| { + let mappingproxy = abc_mappingproxy(py); + let keys = mappingproxy.call_method0("keys").unwrap(); + assert!(keys.is_instance(&py.get_type::()).unwrap()); + }) + } + + #[test] + #[cfg(not(PyPy))] + fn mappingproxy_values_view() { + Python::with_gil(|py| { + let mappingproxy = abc_mappingproxy(py); + let values = mappingproxy.call_method0("values").unwrap(); + assert!(values.is_instance(&py.get_type::()).unwrap()); + }) + } + + #[test] + #[cfg(not(PyPy))] + fn mappingproxy_items_view() { + Python::with_gil(|py| { + let mappingproxy = abc_mappingproxy(py); + let items = mappingproxy.call_method0("items").unwrap(); + assert!(items.is_instance(&py.get_type::()).unwrap()); + }) + } + + #[test] + fn get_value_from_mappingproxy_of_strings() { + Python::with_gil(|py: Python<'_>| { + let mut map = HashMap::new(); + map.insert("first key".to_string(), "first value".to_string()); + map.insert("second key".to_string(), "second value".to_string()); + map.insert("third key".to_string(), "third value".to_string()); + + let dict = map.clone().into_py_dict(py).unwrap(); + let mappingproxy = PyMappingProxy::new(py, dict.as_mapping()); + + assert_eq!( + map.into_iter().collect::>(), + mappingproxy + .try_iter() + .unwrap() + .map(|object| { + let tuple = object.unwrap(); + ( + tuple.0.extract::().unwrap(), + tuple.1.extract::().unwrap(), + ) + }) + .collect::>() + ); + }) + } + + #[test] + fn get_value_from_mappingproxy_of_integers() { + Python::with_gil(|py: Python<'_>| { + const LEN: usize = 10_000; + let items: Vec<(usize, usize)> = (1..LEN).map(|i| (i, i - 1)).collect(); + + let dict = items.clone().into_py_dict(py).unwrap(); + let mappingproxy = PyMappingProxy::new(py, dict.as_mapping()); + + assert_eq!( + items, + mappingproxy + .clone() + .try_iter() + .unwrap() + .map(|object| { + let tuple = object.unwrap(); + ( + tuple + .0 + .downcast::() + .unwrap() + .extract::() + .unwrap(), + tuple + .1 + .downcast::() + .unwrap() + .extract::() + .unwrap(), + ) + }) + .collect::>() + ); + for index in 1..LEN { + assert_eq!( + mappingproxy + .clone() + .get_item(index) + .unwrap() + .extract::() + .unwrap(), + index - 1 + ); + } + }) + } + + #[test] + fn iter_mappingproxy_nosegv() { + Python::with_gil(|py| { + const LEN: usize = 10_000_000; + let items = (0..LEN as u64).map(|i| (i, i * 2)); + + let dict = items.clone().into_py_dict(py).unwrap(); + let mappingproxy = PyMappingProxy::new(py, dict.as_mapping()); + + let mut sum = 0; + for result in mappingproxy.try_iter().unwrap() { + let (k, _v) = result.unwrap(); + let i: u64 = k.extract().unwrap(); + sum += i; + } + assert_eq!(sum, 49_999_995_000_000); + }) + } +} diff --git a/src/types/mod.rs b/src/types/mod.rs index c074196ccc1..d84f099e773 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -28,6 +28,7 @@ pub use self::function::PyFunction; pub use self::iterator::PyIterator; pub use self::list::{PyList, PyListMethods}; pub use self::mapping::{PyMapping, PyMappingMethods}; +pub use self::mappingproxy::PyMappingProxy; pub use self::memoryview::PyMemoryView; pub use self::module::{PyModule, PyModuleMethods}; pub use self::none::PyNone; @@ -248,6 +249,7 @@ mod function; pub(crate) mod iterator; pub(crate) mod list; pub(crate) mod mapping; +pub(crate) mod mappingproxy; mod memoryview; pub(crate) mod module; mod none; From ca2f639910c1d8eff0bbf2bccb48e8b314f7d0c7 Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Tue, 29 Oct 2024 20:14:18 +0100 Subject: [PATCH 349/495] deprecate `IntoPy` in favor or `IntoPyObject` (#4618) * deprecate `IntoPy` in favor of `IntoPyObject` * add newsfragment * apply review comments Co-authored-by: David Hewitt --------- Co-authored-by: David Hewitt --- guide/src/class.md | 11 ++-- guide/src/class/protocols.md | 10 ++-- guide/src/conversions/traits.md | 1 + guide/src/migration.md | 3 ++ newsfragments/4618.changed.md | 1 + pyo3-macros-backend/src/pyclass.rs | 44 +++++++++++----- pyo3-macros-backend/src/pyimpl.rs | 5 +- src/conversion.rs | 25 +++++++--- src/conversions/chrono.rs | 29 +++++++---- src/conversions/chrono_tz.rs | 5 +- src/conversions/either.rs | 7 +-- src/conversions/hashbrown.rs | 37 ++++---------- src/conversions/indexmap.rs | 31 ++---------- src/conversions/num_bigint.rs | 11 ++-- src/conversions/num_complex.rs | 1 + src/conversions/num_rational.rs | 23 +++++---- src/conversions/rust_decimal.rs | 15 +++--- src/conversions/smallvec.rs | 8 +-- src/conversions/std/array.rs | 26 ++++++---- src/conversions/std/cell.rs | 7 +-- src/conversions/std/ipaddr.rs | 11 ++-- src/conversions/std/map.rs | 14 +++--- src/conversions/std/num.rs | 40 ++++++++------- src/conversions/std/option.rs | 7 +-- src/conversions/std/osstr.rs | 36 ++++---------- src/conversions/std/path.rs | 36 ++++---------- src/conversions/std/set.rs | 12 +++-- src/conversions/std/slice.rs | 8 +-- src/conversions/std/string.rs | 33 ++++++++---- src/conversions/std/time.rs | 23 +++++---- src/conversions/std/vec.rs | 5 +- src/coroutine.rs | 23 +++++---- src/err/impls.rs | 17 +++++-- src/err/mod.rs | 10 ++-- src/ffi/tests.rs | 8 +-- src/impl_/coroutine.rs | 15 ++---- src/impl_/pyclass.rs | 9 ++-- src/impl_/wrap.rs | 7 ++- src/instance.rs | 52 ++++++++++++++----- src/lib.rs | 4 +- src/marker.rs | 11 ++-- src/prelude.rs | 4 +- src/pybacked.rs | 11 ++-- src/pycell.rs | 8 ++- src/types/any.rs | 5 +- src/types/boolobject.rs | 8 +-- src/types/datetime.rs | 4 +- src/types/float.rs | 10 ++-- src/types/function.rs | 4 +- src/types/module.rs | 1 + src/types/none.rs | 12 +++-- src/types/num.rs | 11 ++-- src/types/string.rs | 9 +++- src/types/traceback.rs | 5 +- src/types/tuple.rs | 12 +++-- tests/test_arithmetics.rs | 18 +++++-- tests/test_buffer_protocol.rs | 7 +-- tests/test_class_basics.rs | 2 +- tests/test_class_conversion.rs | 8 +-- tests/test_enum.rs | 4 +- tests/test_frompyobject.rs | 80 +++++++++++++++++------------- tests/test_getter_setter.rs | 5 +- tests/test_mapping.rs | 15 ++++-- tests/test_proto_methods.rs | 2 +- tests/test_pyfunction.rs | 10 ++-- tests/test_pyself.rs | 4 +- tests/test_sequence.rs | 7 +-- 67 files changed, 533 insertions(+), 414 deletions(-) create mode 100644 newsfragments/4618.changed.md diff --git a/guide/src/class.md b/guide/src/class.md index c20cacd3cc7..6a80fd7ad9c 100644 --- a/guide/src/class.md +++ b/guide/src/class.md @@ -1258,8 +1258,8 @@ enum Shape { # #[cfg(Py_3_10)] Python::with_gil(|py| { - let circle = Shape::Circle { radius: 10.0 }.into_py(py); - let square = Shape::RegularPolygon(4, 10.0).into_py(py); + let circle = Shape::Circle { radius: 10.0 }.into_pyobject(py)?; + let square = Shape::RegularPolygon(4, 10.0).into_pyobject(py)?; let cls = py.get_type::(); pyo3::py_run!(py, circle square cls, r#" assert isinstance(circle, cls) @@ -1284,11 +1284,13 @@ Python::with_gil(|py| { assert count_vertices(cls, circle) == 0 assert count_vertices(cls, square) == 4 - "#) + "#); +# Ok::<_, PyErr>(()) }) +# .unwrap(); ``` -WARNING: `Py::new` and `.into_py` are currently inconsistent. Note how the constructed value is _not_ an instance of the specific variant. For this reason, constructing values is only recommended using `.into_py`. +WARNING: `Py::new` and `.into_pyobject` are currently inconsistent. Note how the constructed value is _not_ an instance of the specific variant. For this reason, constructing values is only recommended using `.into_pyobject`. ```rust # use pyo3::prelude::*; @@ -1404,6 +1406,7 @@ impl<'a, 'py> pyo3::impl_::extract_argument::PyFunctionArgument<'a, 'py> for &'a } } +#[allow(deprecated)] impl pyo3::IntoPy for MyClass { fn into_py(self, py: pyo3::Python<'_>) -> pyo3::PyObject { pyo3::IntoPy::into_py(pyo3::Py::new(py, self).unwrap(), py) diff --git a/guide/src/class/protocols.md b/guide/src/class/protocols.md index c5ad847834f..4d553c276eb 100644 --- a/guide/src/class/protocols.md +++ b/guide/src/class/protocols.md @@ -97,19 +97,21 @@ given signatures should be interpreted as follows: ```rust use pyo3::class::basic::CompareOp; + use pyo3::types::PyNotImplemented; # use pyo3::prelude::*; + # use pyo3::BoundObject; # # #[pyclass] # struct Number(i32); # #[pymethods] impl Number { - fn __richcmp__(&self, other: &Self, op: CompareOp, py: Python<'_>) -> PyObject { + fn __richcmp__<'py>(&self, other: &Self, op: CompareOp, py: Python<'py>) -> PyResult> { match op { - CompareOp::Eq => (self.0 == other.0).into_py(py), - CompareOp::Ne => (self.0 != other.0).into_py(py), - _ => py.NotImplemented(), + CompareOp::Eq => Ok((self.0 == other.0).into_pyobject(py)?.into_any()), + CompareOp::Ne => Ok((self.0 != other.0).into_pyobject(py)?.into_any()), + _ => Ok(PyNotImplemented::get(py).into_any()), } } } diff --git a/guide/src/conversions/traits.md b/guide/src/conversions/traits.md index def32ecc614..6cc809e0d03 100644 --- a/guide/src/conversions/traits.md +++ b/guide/src/conversions/traits.md @@ -615,6 +615,7 @@ use pyo3::prelude::*; # #[allow(dead_code)] struct MyPyObjectWrapper(PyObject); +#[allow(deprecated)] impl IntoPy for MyPyObjectWrapper { fn into_py(self, py: Python<'_>) -> PyObject { self.0 diff --git a/guide/src/migration.md b/guide/src/migration.md index 6a5b44a6c00..0d76d220dc9 100644 --- a/guide/src/migration.md +++ b/guide/src/migration.md @@ -1217,6 +1217,7 @@ Python::with_gil(|py| { After, some type annotations may be necessary: ```rust +# #![allow(deprecated)] # use pyo3::prelude::*; # # fn main() { @@ -1695,6 +1696,7 @@ After # #[allow(dead_code)] struct MyPyObjectWrapper(PyObject); +# #[allow(deprecated)] impl IntoPy for MyPyObjectWrapper { fn into_py(self, _py: Python<'_>) -> PyObject { self.0 @@ -1714,6 +1716,7 @@ let obj = PyObject::from_py(1.234, py); After: ```rust +# #![allow(deprecated)] # use pyo3::prelude::*; # Python::with_gil(|py| { let obj: PyObject = 1.234.into_py(py); diff --git a/newsfragments/4618.changed.md b/newsfragments/4618.changed.md new file mode 100644 index 00000000000..f3e9dee773d --- /dev/null +++ b/newsfragments/4618.changed.md @@ -0,0 +1 @@ +deprecate `IntoPy` in favor of `IntoPyObject` \ No newline at end of file diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index d7edeb0bf24..e44ac890c9c 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -1044,6 +1044,7 @@ fn impl_complex_enum( .collect(); quote! { + #[allow(deprecated)] impl #pyo3_path::IntoPy<#pyo3_path::PyObject> for #cls { fn into_py(self, py: #pyo3_path::Python) -> #pyo3_path::PyObject { match self { @@ -1349,13 +1350,11 @@ fn impl_complex_enum_tuple_variant_getitem( let match_arms: Vec<_> = (0..num_fields) .map(|i| { let field_access = format_ident!("_{}", i); - quote! { - #i => ::std::result::Result::Ok( - #pyo3_path::IntoPy::into_py( - #variant_cls::#field_access(slf)? - , py) - ) - + quote! { #i => + #pyo3_path::IntoPyObject::into_pyobject(#variant_cls::#field_access(slf)?, py) + .map(#pyo3_path::BoundObject::into_any) + .map(#pyo3_path::BoundObject::unbind) + .map_err(::std::convert::Into::into) } }) .collect(); @@ -1837,10 +1836,16 @@ fn pyclass_richcmp_arms( .map(|span| { quote_spanned! { span => #pyo3_path::pyclass::CompareOp::Eq => { - ::std::result::Result::Ok(#pyo3_path::conversion::IntoPy::into_py(self_val == other, py)) + #pyo3_path::IntoPyObject::into_pyobject(self_val == other, py) + .map(#pyo3_path::BoundObject::into_any) + .map(#pyo3_path::BoundObject::unbind) + .map_err(::std::convert::Into::into) }, #pyo3_path::pyclass::CompareOp::Ne => { - ::std::result::Result::Ok(#pyo3_path::conversion::IntoPy::into_py(self_val != other, py)) + #pyo3_path::IntoPyObject::into_pyobject(self_val != other, py) + .map(#pyo3_path::BoundObject::into_any) + .map(#pyo3_path::BoundObject::unbind) + .map_err(::std::convert::Into::into) }, } }) @@ -1855,16 +1860,28 @@ fn pyclass_richcmp_arms( .map(|ord| { quote_spanned! { ord.span() => #pyo3_path::pyclass::CompareOp::Gt => { - ::std::result::Result::Ok(#pyo3_path::conversion::IntoPy::into_py(self_val > other, py)) + #pyo3_path::IntoPyObject::into_pyobject(self_val > other, py) + .map(#pyo3_path::BoundObject::into_any) + .map(#pyo3_path::BoundObject::unbind) + .map_err(::std::convert::Into::into) }, #pyo3_path::pyclass::CompareOp::Lt => { - ::std::result::Result::Ok(#pyo3_path::conversion::IntoPy::into_py(self_val < other, py)) + #pyo3_path::IntoPyObject::into_pyobject(self_val < other, py) + .map(#pyo3_path::BoundObject::into_any) + .map(#pyo3_path::BoundObject::unbind) + .map_err(::std::convert::Into::into) }, #pyo3_path::pyclass::CompareOp::Le => { - ::std::result::Result::Ok(#pyo3_path::conversion::IntoPy::into_py(self_val <= other, py)) + #pyo3_path::IntoPyObject::into_pyobject(self_val <= other, py) + .map(#pyo3_path::BoundObject::into_any) + .map(#pyo3_path::BoundObject::unbind) + .map_err(::std::convert::Into::into) }, #pyo3_path::pyclass::CompareOp::Ge => { - ::std::result::Result::Ok(#pyo3_path::conversion::IntoPy::into_py(self_val >= other, py)) + #pyo3_path::IntoPyObject::into_pyobject(self_val >= other, py) + .map(#pyo3_path::BoundObject::into_any) + .map(#pyo3_path::BoundObject::unbind) + .map_err(::std::convert::Into::into) }, } }) @@ -2145,6 +2162,7 @@ impl<'a> PyClassImplsBuilder<'a> { // If #cls is not extended type, we allow Self->PyObject conversion if attr.options.extends.is_none() { quote! { + #[allow(deprecated)] impl #pyo3_path::IntoPy<#pyo3_path::PyObject> for #cls { fn into_py(self, py: #pyo3_path::Python<'_>) -> #pyo3_path::PyObject { #pyo3_path::IntoPy::into_py(#pyo3_path::Py::new(py, self).unwrap(), py) diff --git a/pyo3-macros-backend/src/pyimpl.rs b/pyo3-macros-backend/src/pyimpl.rs index 1c3d7f766c2..614db3c5459 100644 --- a/pyo3-macros-backend/src/pyimpl.rs +++ b/pyo3-macros-backend/src/pyimpl.rs @@ -213,7 +213,10 @@ pub fn gen_py_const(cls: &syn::Type, spec: &ConstSpec, ctx: &Ctx) -> MethodAndMe let associated_method = quote! { fn #wrapper_ident(py: #pyo3_path::Python<'_>) -> #pyo3_path::PyResult<#pyo3_path::PyObject> { - ::std::result::Result::Ok(#pyo3_path::IntoPy::into_py(#cls::#member, py)) + #pyo3_path::IntoPyObject::into_pyobject(#cls::#member, py) + .map(#pyo3_path::BoundObject::into_any) + .map(#pyo3_path::BoundObject::unbind) + .map_err(::std::convert::Into::into) } }; diff --git a/src/conversion.rs b/src/conversion.rs index e551e7c1133..a280055cc7c 100644 --- a/src/conversion.rs +++ b/src/conversion.rs @@ -19,16 +19,17 @@ use std::convert::Infallible; /// /// ```rust /// use pyo3::prelude::*; -/// use pyo3::types::PyString; /// use pyo3::ffi; /// /// Python::with_gil(|py| { -/// let s: Py = "foo".into_py(py); +/// let s = "foo".into_pyobject(py)?; /// let ptr = s.as_ptr(); /// /// let is_really_a_pystring = unsafe { ffi::PyUnicode_CheckExact(ptr) }; /// assert_eq!(is_really_a_pystring, 1); -/// }); +/// # Ok::<_, PyErr>(()) +/// }) +/// # .unwrap(); /// ``` /// /// # Safety @@ -41,18 +42,21 @@ use std::convert::Infallible; /// # use pyo3::ffi; /// # /// Python::with_gil(|py| { -/// let ptr: *mut ffi::PyObject = 0xabad1dea_u32.into_py(py).as_ptr(); +/// // ERROR: calling `.as_ptr()` will throw away the temporary object and leave `ptr` dangling. +/// let ptr: *mut ffi::PyObject = 0xabad1dea_u32.into_pyobject(py)?.as_ptr(); /// /// let isnt_a_pystring = unsafe { /// // `ptr` is dangling, this is UB /// ffi::PyUnicode_CheckExact(ptr) /// }; -/// # assert_eq!(isnt_a_pystring, 0); -/// }); +/// # assert_eq!(isnt_a_pystring, 0); +/// # Ok::<_, PyErr>(()) +/// }) +/// # .unwrap(); /// ``` /// /// This happens because the pointer returned by `as_ptr` does not carry any lifetime information -/// and the Python object is dropped immediately after the `0xabad1dea_u32.into_py(py).as_ptr()` +/// and the Python object is dropped immediately after the `0xabad1dea_u32.into_pyobject(py).as_ptr()` /// expression is evaluated. To fix the problem, bind Python object to a local variable like earlier /// to keep the Python object alive until the end of its scope. /// @@ -100,6 +104,7 @@ pub trait ToPyObject { /// However, it may not be desirable to expose the existence of `Number` to Python code. /// `IntoPy` allows us to define a conversion to an appropriate Python object. /// ```rust +/// #![allow(deprecated)] /// use pyo3::prelude::*; /// /// # #[allow(dead_code)] @@ -121,6 +126,7 @@ pub trait ToPyObject { /// This is useful for types like enums that can carry different types. /// /// ```rust +/// #![allow(deprecated)] /// use pyo3::prelude::*; /// /// enum Value { @@ -161,6 +167,10 @@ pub trait ToPyObject { note = "if you do not own `{Self}` you can perform a manual conversion to one of the types in `pyo3::types::*`" ) )] +#[deprecated( + since = "0.23.0", + note = "`IntoPy` is going to be replaced by `IntoPyObject`. See the migration guide (https://pyo3.rs/v0.23/migration) for more information." +)] pub trait IntoPy: Sized { /// Performs the conversion. fn into_py(self, py: Python<'_>) -> T; @@ -503,6 +513,7 @@ where } /// Converts `()` to an empty Python tuple. +#[allow(deprecated)] impl IntoPy> for () { fn into_py(self, py: Python<'_>) -> Py { PyTuple::empty(py).unbind() diff --git a/src/conversions/chrono.rs b/src/conversions/chrono.rs index ce94b98c1a3..90f9c69761d 100644 --- a/src/conversions/chrono.rs +++ b/src/conversions/chrono.rs @@ -54,11 +54,11 @@ use crate::types::{ timezone_utc, PyDate, PyDateAccess, PyDateTime, PyDelta, PyDeltaAccess, PyTime, PyTimeAccess, PyTzInfo, PyTzInfoAccess, }; -#[allow(deprecated)] -use crate::ToPyObject; -use crate::{ffi, Bound, FromPyObject, IntoPy, PyAny, PyErr, PyObject, PyResult, Python}; +use crate::{ffi, Bound, FromPyObject, PyAny, PyErr, PyObject, PyResult, Python}; #[cfg(Py_LIMITED_API)] use crate::{intern, DowncastError}; +#[allow(deprecated)] +use crate::{IntoPy, ToPyObject}; use chrono::offset::{FixedOffset, Utc}; use chrono::{ DateTime, Datelike, Duration, NaiveDate, NaiveDateTime, NaiveTime, Offset, TimeZone, Timelike, @@ -72,6 +72,7 @@ impl ToPyObject for Duration { } } +#[allow(deprecated)] impl IntoPy for Duration { #[inline] fn into_py(self, py: Python<'_>) -> PyObject { @@ -178,6 +179,7 @@ impl ToPyObject for NaiveDate { } } +#[allow(deprecated)] impl IntoPy for NaiveDate { #[inline] fn into_py(self, py: Python<'_>) -> PyObject { @@ -244,6 +246,7 @@ impl ToPyObject for NaiveTime { } } +#[allow(deprecated)] impl IntoPy for NaiveTime { #[inline] fn into_py(self, py: Python<'_>) -> PyObject { @@ -320,6 +323,7 @@ impl ToPyObject for NaiveDateTime { } } +#[allow(deprecated)] impl IntoPy for NaiveDateTime { #[inline] fn into_py(self, py: Python<'_>) -> PyObject { @@ -411,6 +415,7 @@ impl ToPyObject for DateTime { } } +#[allow(deprecated)] impl IntoPy for DateTime { fn into_py(self, py: Python<'_>) -> PyObject { self.into_pyobject(py).unwrap().into_any().unbind() @@ -505,6 +510,7 @@ impl ToPyObject for FixedOffset { } } +#[allow(deprecated)] impl IntoPy for FixedOffset { #[inline] fn into_py(self, py: Python<'_>) -> PyObject { @@ -589,6 +595,7 @@ impl ToPyObject for Utc { } } +#[allow(deprecated)] impl IntoPy for Utc { #[inline] fn into_py(self, py: Python<'_>) -> PyObject { @@ -1408,8 +1415,8 @@ mod tests { // python values of durations (from -999999999 to 999999999 days), Python::with_gil(|py| { let dur = Duration::days(days); - let py_delta = dur.into_py(py); - let roundtripped: Duration = py_delta.extract(py).expect("Round trip"); + let py_delta = dur.into_pyobject(py).unwrap(); + let roundtripped: Duration = py_delta.extract().expect("Round trip"); assert_eq!(dur, roundtripped); }) } @@ -1418,8 +1425,8 @@ mod tests { fn test_fixed_offset_roundtrip(secs in -86399i32..=86399i32) { Python::with_gil(|py| { let offset = FixedOffset::east_opt(secs).unwrap(); - let py_offset = offset.into_py(py); - let roundtripped: FixedOffset = py_offset.extract(py).expect("Round trip"); + let py_offset = offset.into_pyobject(py).unwrap(); + let roundtripped: FixedOffset = py_offset.extract().expect("Round trip"); assert_eq!(offset, roundtripped); }) } @@ -1504,8 +1511,8 @@ mod tests { if let (Some(date), Some(time)) = (date_opt, time_opt) { let dt: DateTime = NaiveDateTime::new(date, time).and_utc(); // Wrap in CatchWarnings to avoid into_py firing warning for truncated leap second - let py_dt = CatchWarnings::enter(py, |_| Ok(dt.into_py(py))).unwrap(); - let roundtripped: DateTime = py_dt.extract(py).expect("Round trip"); + let py_dt = CatchWarnings::enter(py, |_| dt.into_pyobject(py)).unwrap(); + let roundtripped: DateTime = py_dt.extract().expect("Round trip"); // Leap seconds are not roundtripped let expected_roundtrip_time = micro.checked_sub(1_000_000).map(|micro| NaiveTime::from_hms_micro_opt(hour, min, sec, micro).unwrap()).unwrap_or(time); let expected_roundtrip_dt: DateTime = NaiveDateTime::new(date, expected_roundtrip_time).and_utc(); @@ -1532,8 +1539,8 @@ mod tests { if let (Some(date), Some(time)) = (date_opt, time_opt) { let dt: DateTime = NaiveDateTime::new(date, time).and_local_timezone(offset).unwrap(); // Wrap in CatchWarnings to avoid into_py firing warning for truncated leap second - let py_dt = CatchWarnings::enter(py, |_| Ok(dt.into_py(py))).unwrap(); - let roundtripped: DateTime = py_dt.extract(py).expect("Round trip"); + let py_dt = CatchWarnings::enter(py, |_| dt.into_pyobject(py)).unwrap(); + let roundtripped: DateTime = py_dt.extract().expect("Round trip"); // Leap seconds are not roundtripped let expected_roundtrip_time = micro.checked_sub(1_000_000).map(|micro| NaiveTime::from_hms_micro_opt(hour, min, sec, micro).unwrap()).unwrap_or(time); let expected_roundtrip_dt: DateTime = NaiveDateTime::new(date, expected_roundtrip_time).and_local_timezone(offset).unwrap(); diff --git a/src/conversions/chrono_tz.rs b/src/conversions/chrono_tz.rs index 8d324bfcacb..bb1a74c1519 100644 --- a/src/conversions/chrono_tz.rs +++ b/src/conversions/chrono_tz.rs @@ -39,9 +39,9 @@ use crate::exceptions::PyValueError; use crate::pybacked::PyBackedStr; use crate::sync::GILOnceCell; use crate::types::{any::PyAnyMethods, PyType}; +use crate::{intern, Bound, FromPyObject, Py, PyAny, PyErr, PyObject, PyResult, Python}; #[allow(deprecated)] -use crate::ToPyObject; -use crate::{intern, Bound, FromPyObject, IntoPy, Py, PyAny, PyErr, PyObject, PyResult, Python}; +use crate::{IntoPy, ToPyObject}; use chrono_tz::Tz; use std::str::FromStr; @@ -53,6 +53,7 @@ impl ToPyObject for Tz { } } +#[allow(deprecated)] impl IntoPy for Tz { #[inline] fn into_py(self, py: Python<'_>) -> PyObject { diff --git a/src/conversions/either.rs b/src/conversions/either.rs index 2cf1029ffb6..1fdcd22d7e2 100644 --- a/src/conversions/either.rs +++ b/src/conversions/either.rs @@ -46,15 +46,16 @@ #[cfg(feature = "experimental-inspect")] use crate::inspect::types::TypeInfo; -#[allow(deprecated)] -use crate::ToPyObject; use crate::{ conversion::IntoPyObject, exceptions::PyTypeError, types::any::PyAnyMethods, Bound, - BoundObject, FromPyObject, IntoPy, PyAny, PyErr, PyObject, PyResult, Python, + BoundObject, FromPyObject, PyAny, PyErr, PyObject, PyResult, Python, }; +#[allow(deprecated)] +use crate::{IntoPy, ToPyObject}; use either::Either; #[cfg_attr(docsrs, doc(cfg(feature = "either")))] +#[allow(deprecated)] impl IntoPy for Either where L: IntoPy, diff --git a/src/conversions/hashbrown.rs b/src/conversions/hashbrown.rs index 25ec014341e..0efe7f5161f 100644 --- a/src/conversions/hashbrown.rs +++ b/src/conversions/hashbrown.rs @@ -16,8 +16,6 @@ //! //! Note that you must use compatible versions of hashbrown and PyO3. //! The required hashbrown version may vary based on the version of PyO3. -#[allow(deprecated)] -use crate::ToPyObject; use crate::{ conversion::IntoPyObject, types::{ @@ -27,8 +25,10 @@ use crate::{ set::{new_from_iter, try_new_from_iter, PySetMethods}, PyDict, PyFrozenSet, PySet, }, - Bound, FromPyObject, IntoPy, PyAny, PyErr, PyObject, PyResult, Python, + Bound, FromPyObject, PyAny, PyErr, PyObject, PyResult, Python, }; +#[allow(deprecated)] +use crate::{IntoPy, ToPyObject}; use std::{cmp, hash}; #[allow(deprecated)] @@ -47,6 +47,7 @@ where } } +#[allow(deprecated)] impl IntoPy for hashbrown::HashMap where K: hash::Hash + cmp::Eq + IntoPy, @@ -128,6 +129,7 @@ where } } +#[allow(deprecated)] impl IntoPy for hashbrown::HashSet where K: IntoPy + Eq + hash::Hash, @@ -193,7 +195,7 @@ mod tests { use crate::types::IntoPyDict; #[test] - fn test_hashbrown_hashmap_to_python() { + fn test_hashbrown_hashmap_into_pyobject() { Python::with_gil(|py| { let mut map = hashbrown::HashMap::::new(); map.insert(1, 1); @@ -213,27 +215,6 @@ mod tests { assert_eq!(map, py_map.extract().unwrap()); }); } - #[test] - fn test_hashbrown_hashmap_into_python() { - Python::with_gil(|py| { - let mut map = hashbrown::HashMap::::new(); - map.insert(1, 1); - - let m: PyObject = map.into_py(py); - let py_map = m.downcast_bound::(py).unwrap(); - - assert!(py_map.len() == 1); - assert!( - py_map - .get_item(1) - .unwrap() - .unwrap() - .extract::() - .unwrap() - == 1 - ); - }); - } #[test] fn test_hashbrown_hashmap_into_dict() { @@ -270,13 +251,13 @@ mod tests { } #[test] - fn test_hashbrown_hashset_into_py() { + fn test_hashbrown_hashset_into_pyobject() { Python::with_gil(|py| { let hs: hashbrown::HashSet = [1, 2, 3, 4, 5].iter().cloned().collect(); - let hso: PyObject = hs.clone().into_py(py); + let hso = hs.clone().into_pyobject(py).unwrap(); - assert_eq!(hs, hso.extract(py).unwrap()); + assert_eq!(hs, hso.extract().unwrap()); }); } } diff --git a/src/conversions/indexmap.rs b/src/conversions/indexmap.rs index 8d74b126f29..e3787e68091 100644 --- a/src/conversions/indexmap.rs +++ b/src/conversions/indexmap.rs @@ -89,9 +89,9 @@ use crate::conversion::IntoPyObject; use crate::types::*; +use crate::{Bound, FromPyObject, PyErr, PyObject, Python}; #[allow(deprecated)] -use crate::ToPyObject; -use crate::{Bound, FromPyObject, IntoPy, PyErr, PyObject, Python}; +use crate::{IntoPy, ToPyObject}; use std::{cmp, hash}; #[allow(deprecated)] @@ -110,6 +110,7 @@ where } } +#[allow(deprecated)] impl IntoPy for indexmap::IndexMap where K: hash::Hash + cmp::Eq + IntoPy, @@ -183,10 +184,10 @@ where mod test_indexmap { use crate::types::*; - use crate::{IntoPy, IntoPyObject, PyObject, Python}; + use crate::{IntoPyObject, Python}; #[test] - fn test_indexmap_indexmap_to_python() { + fn test_indexmap_indexmap_into_pyobject() { Python::with_gil(|py| { let mut map = indexmap::IndexMap::::new(); map.insert(1, 1); @@ -210,28 +211,6 @@ mod test_indexmap { }); } - #[test] - fn test_indexmap_indexmap_into_python() { - Python::with_gil(|py| { - let mut map = indexmap::IndexMap::::new(); - map.insert(1, 1); - - let m: PyObject = map.into_py(py); - let py_map = m.downcast_bound::(py).unwrap(); - - assert!(py_map.len() == 1); - assert!( - py_map - .get_item(1) - .unwrap() - .unwrap() - .extract::() - .unwrap() - == 1 - ); - }); - } - #[test] fn test_indexmap_indexmap_into_dict() { Python::with_gil(|py| { diff --git a/src/conversions/num_bigint.rs b/src/conversions/num_bigint.rs index df8e108776f..d91973b5572 100644 --- a/src/conversions/num_bigint.rs +++ b/src/conversions/num_bigint.rs @@ -49,15 +49,15 @@ #[cfg(Py_LIMITED_API)] use crate::types::{bytes::PyBytesMethods, PyBytes}; -#[allow(deprecated)] -use crate::ToPyObject; use crate::{ conversion::IntoPyObject, ffi, instance::Bound, types::{any::PyAnyMethods, PyInt}, - FromPyObject, IntoPy, Py, PyAny, PyErr, PyObject, PyResult, Python, + FromPyObject, Py, PyAny, PyErr, PyObject, PyResult, Python, }; +#[allow(deprecated)] +use crate::{IntoPy, ToPyObject}; use num_bigint::{BigInt, BigUint}; @@ -77,6 +77,7 @@ macro_rules! bigint_conversion { } #[cfg_attr(docsrs, doc(cfg(feature = "num-bigint")))] + #[allow(deprecated)] impl IntoPy for $rust_ty { #[inline] fn into_py(self, py: Python<'_>) -> PyObject { @@ -451,8 +452,8 @@ mod tests { ($T:ty, $value:expr, $py:expr) => { let value = $value; println!("{}: {}", stringify!($T), value); - let python_value = value.clone().into_py(py); - let roundtrip_value = python_value.extract::<$T>(py).unwrap(); + let python_value = value.clone().into_pyobject(py).unwrap(); + let roundtrip_value = python_value.extract::<$T>().unwrap(); assert_eq!(value, roundtrip_value); }; } diff --git a/src/conversions/num_complex.rs b/src/conversions/num_complex.rs index 4b3be8e15b0..db981a4179b 100644 --- a/src/conversions/num_complex.rs +++ b/src/conversions/num_complex.rs @@ -130,6 +130,7 @@ macro_rules! complex_conversion { } #[cfg_attr(docsrs, doc(cfg(feature = "num-complex")))] + #[allow(deprecated)] impl crate::IntoPy for Complex<$float> { fn into_py(self, py: Python<'_>) -> PyObject { unsafe { diff --git a/src/conversions/num_rational.rs b/src/conversions/num_rational.rs index cda877502b3..6c48f67fcf2 100644 --- a/src/conversions/num_rational.rs +++ b/src/conversions/num_rational.rs @@ -48,9 +48,9 @@ use crate::ffi; use crate::sync::GILOnceCell; use crate::types::any::PyAnyMethods; use crate::types::PyType; +use crate::{Bound, FromPyObject, Py, PyAny, PyErr, PyObject, PyResult, Python}; #[allow(deprecated)] -use crate::ToPyObject; -use crate::{Bound, FromPyObject, IntoPy, Py, PyAny, PyErr, PyObject, PyResult, Python}; +use crate::{IntoPy, ToPyObject}; #[cfg(feature = "num-bigint")] use num_bigint::BigInt; @@ -91,6 +91,7 @@ macro_rules! rational_conversion { self.into_pyobject(py).unwrap().into_any().unbind() } } + #[allow(deprecated)] impl IntoPy for Ratio<$int> { #[inline] fn into_py(self, py: Python<'_>) -> PyObject { @@ -223,9 +224,9 @@ mod tests { #[test] fn test_int_roundtrip() { Python::with_gil(|py| { - let rs_frac = Ratio::new(1, 2); - let py_frac: PyObject = rs_frac.into_py(py); - let roundtripped: Ratio = py_frac.extract(py).unwrap(); + let rs_frac = Ratio::new(1i32, 2); + let py_frac = rs_frac.into_pyobject(py).unwrap(); + let roundtripped: Ratio = py_frac.extract().unwrap(); assert_eq!(rs_frac, roundtripped); // float conversion }) @@ -236,8 +237,8 @@ mod tests { fn test_big_int_roundtrip() { Python::with_gil(|py| { let rs_frac = Ratio::from_float(5.5).unwrap(); - let py_frac: PyObject = rs_frac.clone().into_py(py); - let roundtripped: Ratio = py_frac.extract(py).unwrap(); + let py_frac = rs_frac.clone().into_pyobject(py).unwrap(); + let roundtripped: Ratio = py_frac.extract().unwrap(); assert_eq!(rs_frac, roundtripped); }) } @@ -248,8 +249,8 @@ mod tests { fn test_int_roundtrip(num in any::(), den in any::()) { Python::with_gil(|py| { let rs_frac = Ratio::new(num, den); - let py_frac = rs_frac.into_py(py); - let roundtripped: Ratio = py_frac.extract(py).unwrap(); + let py_frac = rs_frac.into_pyobject(py).unwrap(); + let roundtripped: Ratio = py_frac.extract().unwrap(); assert_eq!(rs_frac, roundtripped); }) } @@ -259,8 +260,8 @@ mod tests { fn test_big_int_roundtrip(num in any::()) { Python::with_gil(|py| { let rs_frac = Ratio::from_float(num).unwrap(); - let py_frac = rs_frac.clone().into_py(py); - let roundtripped: Ratio = py_frac.extract(py).unwrap(); + let py_frac = rs_frac.clone().into_pyobject(py).unwrap(); + let roundtripped: Ratio = py_frac.extract().unwrap(); assert_eq!(roundtripped, rs_frac); }) } diff --git a/src/conversions/rust_decimal.rs b/src/conversions/rust_decimal.rs index 7926592119a..7c6eda21a66 100644 --- a/src/conversions/rust_decimal.rs +++ b/src/conversions/rust_decimal.rs @@ -55,9 +55,9 @@ use crate::sync::GILOnceCell; use crate::types::any::PyAnyMethods; use crate::types::string::PyStringMethods; use crate::types::PyType; +use crate::{Bound, FromPyObject, Py, PyAny, PyErr, PyObject, PyResult, Python}; #[allow(deprecated)] -use crate::ToPyObject; -use crate::{Bound, FromPyObject, IntoPy, Py, PyAny, PyErr, PyObject, PyResult, Python}; +use crate::{IntoPy, ToPyObject}; use rust_decimal::Decimal; use std::str::FromStr; @@ -90,6 +90,7 @@ impl ToPyObject for Decimal { } } +#[allow(deprecated)] impl IntoPy for Decimal { #[inline] fn into_py(self, py: Python<'_>) -> PyObject { @@ -138,7 +139,7 @@ mod test_rust_decimal { fn $name() { Python::with_gil(|py| { let rs_orig = $rs; - let rs_dec = rs_orig.into_py(py); + let rs_dec = rs_orig.into_pyobject(py).unwrap(); let locals = PyDict::new(py); locals.set_item("rs_dec", &rs_dec).unwrap(); // Checks if Rust Decimal -> Python Decimal conversion is correct @@ -181,7 +182,7 @@ mod test_rust_decimal { ) { let num = Decimal::from_parts(lo, mid, high, negative, scale); Python::with_gil(|py| { - let rs_dec = num.into_py(py); + let rs_dec = num.into_pyobject(py).unwrap(); let locals = PyDict::new(py); locals.set_item("rs_dec", &rs_dec).unwrap(); py.run( @@ -189,7 +190,7 @@ mod test_rust_decimal { "import decimal\npy_dec = decimal.Decimal(\"{}\")\nassert py_dec == rs_dec", num)).unwrap(), None, Some(&locals)).unwrap(); - let roundtripped: Decimal = rs_dec.extract(py).unwrap(); + let roundtripped: Decimal = rs_dec.extract().unwrap(); assert_eq!(num, roundtripped); }) } @@ -197,8 +198,8 @@ mod test_rust_decimal { #[test] fn test_integers(num in any::()) { Python::with_gil(|py| { - let py_num = num.into_py(py); - let roundtripped: Decimal = py_num.extract(py).unwrap(); + let py_num = num.into_pyobject(py).unwrap(); + let roundtripped: Decimal = py_num.extract().unwrap(); let rs_dec = Decimal::new(num, 0); assert_eq!(rs_dec, roundtripped); }) diff --git a/src/conversions/smallvec.rs b/src/conversions/smallvec.rs index a01f204b73d..99244d81417 100644 --- a/src/conversions/smallvec.rs +++ b/src/conversions/smallvec.rs @@ -23,11 +23,9 @@ use crate::types::any::PyAnyMethods; use crate::types::list::new_from_iter; use crate::types::{PySequence, PyString}; use crate::PyErr; +use crate::{err::DowncastError, ffi, Bound, FromPyObject, PyAny, PyObject, PyResult, Python}; #[allow(deprecated)] -use crate::ToPyObject; -use crate::{ - err::DowncastError, ffi, Bound, FromPyObject, IntoPy, PyAny, PyObject, PyResult, Python, -}; +use crate::{IntoPy, ToPyObject}; use smallvec::{Array, SmallVec}; #[allow(deprecated)] @@ -41,6 +39,7 @@ where } } +#[allow(deprecated)] impl IntoPy for SmallVec where A: Array, @@ -144,6 +143,7 @@ mod tests { use crate::types::{PyBytes, PyBytesMethods, PyDict, PyList}; #[test] + #[allow(deprecated)] fn test_smallvec_into_py() { Python::with_gil(|py| { let sv: SmallVec<[u64; 8]> = [1, 2, 3, 4, 5].iter().cloned().collect(); diff --git a/src/conversions/std/array.rs b/src/conversions/std/array.rs index c184aa7f732..1c33a610273 100644 --- a/src/conversions/std/array.rs +++ b/src/conversions/std/array.rs @@ -2,11 +2,12 @@ use crate::conversion::IntoPyObject; use crate::instance::Bound; use crate::types::any::PyAnyMethods; use crate::types::PySequence; -#[allow(deprecated)] -use crate::ToPyObject; -use crate::{err::DowncastError, ffi, FromPyObject, IntoPy, Py, PyAny, PyObject, PyResult, Python}; +use crate::{err::DowncastError, ffi, FromPyObject, Py, PyAny, PyObject, PyResult, Python}; use crate::{exceptions, PyErr}; +#[allow(deprecated)] +use crate::{IntoPy, ToPyObject}; +#[allow(deprecated)] impl IntoPy for [T; N] where T: IntoPy, @@ -168,7 +169,7 @@ mod tests { ffi, types::{any::PyAnyMethods, PyBytes, PyBytesMethods}, }; - use crate::{types::PyList, IntoPy, PyResult, Python}; + use crate::{types::PyList, PyResult, Python}; #[test] fn array_try_from_fn() { @@ -246,11 +247,15 @@ mod tests { } #[test] - fn test_intopy_array_conversion() { + fn test_intopyobject_array_conversion() { Python::with_gil(|py| { let array: [f32; 4] = [0.0, -16.0, 16.0, 42.0]; - let pyobject = array.into_py(py); - let pylist = pyobject.downcast_bound::(py).unwrap(); + let pylist = array + .into_pyobject(py) + .unwrap() + .downcast_into::() + .unwrap(); + assert_eq!(pylist.get_item(0).unwrap().extract::().unwrap(), 0.0); assert_eq!(pylist.get_item(1).unwrap().extract::().unwrap(), -16.0); assert_eq!(pylist.get_item(2).unwrap().extract::().unwrap(), 16.0); @@ -290,8 +295,11 @@ mod tests { Python::with_gil(|py| { let array: [Foo; 8] = [Foo, Foo, Foo, Foo, Foo, Foo, Foo, Foo]; - let pyobject = array.into_py(py); - let list = pyobject.downcast_bound::(py).unwrap(); + let list = array + .into_pyobject(py) + .unwrap() + .downcast_into::() + .unwrap(); let _bound = list.get_item(4).unwrap().downcast::().unwrap(); }); } diff --git a/src/conversions/std/cell.rs b/src/conversions/std/cell.rs index 369d91bf69e..70da688b70c 100644 --- a/src/conversions/std/cell.rs +++ b/src/conversions/std/cell.rs @@ -1,8 +1,8 @@ use std::cell::Cell; use crate::{ - conversion::IntoPyObject, types::any::PyAnyMethods, Bound, FromPyObject, IntoPy, PyAny, - PyObject, PyResult, Python, + conversion::IntoPyObject, types::any::PyAnyMethods, Bound, FromPyObject, PyAny, PyObject, + PyResult, Python, }; #[allow(deprecated)] @@ -12,7 +12,8 @@ impl crate::ToPyObject for Cell { } } -impl> IntoPy for Cell { +#[allow(deprecated)] +impl> crate::IntoPy for Cell { fn into_py(self, py: Python<'_>) -> PyObject { self.get().into_py(py) } diff --git a/src/conversions/std/ipaddr.rs b/src/conversions/std/ipaddr.rs index 6c5e74d4881..7ba8da87749 100755 --- a/src/conversions/std/ipaddr.rs +++ b/src/conversions/std/ipaddr.rs @@ -7,9 +7,9 @@ use crate::sync::GILOnceCell; use crate::types::any::PyAnyMethods; use crate::types::string::PyStringMethods; use crate::types::PyType; +use crate::{intern, FromPyObject, Py, PyAny, PyErr, PyObject, PyResult, Python}; #[allow(deprecated)] -use crate::ToPyObject; -use crate::{intern, FromPyObject, IntoPy, Py, PyAny, PyErr, PyObject, PyResult, Python}; +use crate::{IntoPy, ToPyObject}; impl FromPyObject<'_> for IpAddr { fn extract_bound(obj: &Bound<'_, PyAny>) -> PyResult { @@ -103,6 +103,7 @@ impl ToPyObject for IpAddr { } } +#[allow(deprecated)] impl IntoPy for IpAddr { #[inline] fn into_py(self, py: Python<'_>) -> PyObject { @@ -153,12 +154,12 @@ mod test_ipaddr { "IPv6Address" }; - let pyobj = ip.into_py(py); - let repr = pyobj.bind(py).repr().unwrap(); + let pyobj = ip.into_pyobject(py).unwrap(); + let repr = pyobj.repr().unwrap(); let repr = repr.to_string_lossy(); assert_eq!(repr, format!("{}('{}')", py_cls, ip)); - let ip2: IpAddr = pyobj.extract(py).unwrap(); + let ip2: IpAddr = pyobj.extract().unwrap(); assert_eq!(ip, ip2); } roundtrip(py, "127.0.0.1"); diff --git a/src/conversions/std/map.rs b/src/conversions/std/map.rs index 388c50b2f3e..8a6ba57012e 100644 --- a/src/conversions/std/map.rs +++ b/src/conversions/std/map.rs @@ -2,14 +2,14 @@ use std::{cmp, collections, hash}; #[cfg(feature = "experimental-inspect")] use crate::inspect::types::TypeInfo; -#[allow(deprecated)] -use crate::ToPyObject; use crate::{ conversion::IntoPyObject, instance::Bound, types::{any::PyAnyMethods, dict::PyDictMethods, PyDict}, - FromPyObject, IntoPy, PyAny, PyErr, PyObject, Python, + FromPyObject, PyAny, PyErr, PyObject, Python, }; +#[allow(deprecated)] +use crate::{IntoPy, ToPyObject}; #[allow(deprecated)] impl ToPyObject for collections::HashMap @@ -42,6 +42,7 @@ where } } +#[allow(deprecated)] impl IntoPy for collections::HashMap where K: hash::Hash + cmp::Eq + IntoPy, @@ -107,6 +108,7 @@ where } } +#[allow(deprecated)] impl IntoPy for collections::BTreeMap where K: cmp::Eq + IntoPy, @@ -265,8 +267,7 @@ mod tests { let mut map = HashMap::::new(); map.insert(1, 1); - let m: PyObject = map.into_py(py); - let py_map = m.downcast_bound::(py).unwrap(); + let py_map = map.into_pyobject(py).unwrap(); assert!(py_map.len() == 1); assert!( @@ -287,8 +288,7 @@ mod tests { let mut map = BTreeMap::::new(); map.insert(1, 1); - let m: PyObject = map.into_py(py); - let py_map = m.downcast_bound::(py).unwrap(); + let py_map = map.into_pyobject(py).unwrap(); assert!(py_map.len() == 1); assert!( diff --git a/src/conversions/std/num.rs b/src/conversions/std/num.rs index ded970ef9a1..0450c591ae8 100644 --- a/src/conversions/std/num.rs +++ b/src/conversions/std/num.rs @@ -5,11 +5,9 @@ use crate::ffi_ptr_ext::FfiPtrExt; use crate::inspect::types::TypeInfo; use crate::types::any::PyAnyMethods; use crate::types::{PyBytes, PyInt}; +use crate::{exceptions, ffi, Bound, FromPyObject, PyAny, PyErr, PyObject, PyResult, Python}; #[allow(deprecated)] -use crate::ToPyObject; -use crate::{ - exceptions, ffi, Bound, FromPyObject, IntoPy, PyAny, PyErr, PyObject, PyResult, Python, -}; +use crate::{IntoPy, ToPyObject}; use std::convert::Infallible; use std::num::{ NonZeroI128, NonZeroI16, NonZeroI32, NonZeroI64, NonZeroI8, NonZeroIsize, NonZeroU128, @@ -26,6 +24,7 @@ macro_rules! int_fits_larger_int { self.into_pyobject(py).unwrap().into_any().unbind() } } + #[allow(deprecated)] impl IntoPy for $rust_type { #[inline] fn into_py(self, py: Python<'_>) -> PyObject { @@ -112,6 +111,7 @@ macro_rules! int_convert_u64_or_i64 { self.into_pyobject(py).unwrap().into_any().unbind() } } + #[allow(deprecated)] impl IntoPy for $rust_type { #[inline] fn into_py(self, py: Python<'_>) -> PyObject { @@ -173,6 +173,7 @@ macro_rules! int_fits_c_long { self.into_pyobject(py).unwrap().into_any().unbind() } } + #[allow(deprecated)] impl IntoPy for $rust_type { #[inline] fn into_py(self, py: Python<'_>) -> PyObject { @@ -237,6 +238,7 @@ impl ToPyObject for u8 { self.into_pyobject(py).unwrap().into_any().unbind() } } +#[allow(deprecated)] impl IntoPy for u8 { #[inline] fn into_py(self, py: Python<'_>) -> PyObject { @@ -361,6 +363,8 @@ mod fast_128bit_int_conversion { self.into_pyobject(py).unwrap().into_any().unbind() } } + + #[allow(deprecated)] impl IntoPy for $rust_type { #[inline] fn into_py(self, py: Python<'_>) -> PyObject { @@ -514,6 +518,7 @@ mod slow_128bit_int_conversion { } } + #[allow(deprecated)] impl IntoPy for $rust_type { #[inline] fn into_py(self, py: Python<'_>) -> PyObject { @@ -571,7 +576,7 @@ mod slow_128bit_int_conversion { -1 as _, ffi::PyLong_AsUnsignedLongLongMask(ob.as_ptr()), )? as $rust_type; - let shift = SHIFT.into_py(py); + let shift = SHIFT.into_pyobject(py)?; let shifted = PyObject::from_owned_ptr_or_err( py, ffi::PyNumber_Rshift(ob.as_ptr(), shift.as_ptr()), @@ -617,6 +622,7 @@ macro_rules! nonzero_int_impl { } } + #[allow(deprecated)] impl IntoPy for $nonzero_type { #[inline] fn into_py(self, py: Python<'_>) -> PyObject { @@ -705,11 +711,11 @@ mod test_128bit_integers { #[test] fn test_i128_roundtrip(x: i128) { Python::with_gil(|py| { - let x_py = x.into_py(py); + let x_py = x.into_pyobject(py).unwrap(); let locals = PyDict::new(py); - locals.set_item("x_py", x_py.clone_ref(py)).unwrap(); + locals.set_item("x_py", &x_py).unwrap(); py.run(&CString::new(format!("assert x_py == {}", x)).unwrap(), None, Some(&locals)).unwrap(); - let roundtripped: i128 = x_py.extract(py).unwrap(); + let roundtripped: i128 = x_py.extract().unwrap(); assert_eq!(x, roundtripped); }) } @@ -721,11 +727,11 @@ mod test_128bit_integers { .prop_map(|x| NonZeroI128::new(x).unwrap()) ) { Python::with_gil(|py| { - let x_py = x.into_py(py); + let x_py = x.into_pyobject(py).unwrap(); let locals = PyDict::new(py); - locals.set_item("x_py", x_py.clone_ref(py)).unwrap(); + locals.set_item("x_py", &x_py).unwrap(); py.run(&CString::new(format!("assert x_py == {}", x)).unwrap(), None, Some(&locals)).unwrap(); - let roundtripped: NonZeroI128 = x_py.extract(py).unwrap(); + let roundtripped: NonZeroI128 = x_py.extract().unwrap(); assert_eq!(x, roundtripped); }) } @@ -736,11 +742,11 @@ mod test_128bit_integers { #[test] fn test_u128_roundtrip(x: u128) { Python::with_gil(|py| { - let x_py = x.into_py(py); + let x_py = x.into_pyobject(py).unwrap(); let locals = PyDict::new(py); - locals.set_item("x_py", x_py.clone_ref(py)).unwrap(); + locals.set_item("x_py", &x_py).unwrap(); py.run(&CString::new(format!("assert x_py == {}", x)).unwrap(), None, Some(&locals)).unwrap(); - let roundtripped: u128 = x_py.extract(py).unwrap(); + let roundtripped: u128 = x_py.extract().unwrap(); assert_eq!(x, roundtripped); }) } @@ -752,11 +758,11 @@ mod test_128bit_integers { .prop_map(|x| NonZeroU128::new(x).unwrap()) ) { Python::with_gil(|py| { - let x_py = x.into_py(py); + let x_py = x.into_pyobject(py).unwrap(); let locals = PyDict::new(py); - locals.set_item("x_py", x_py.clone_ref(py)).unwrap(); + locals.set_item("x_py", &x_py).unwrap(); py.run(&CString::new(format!("assert x_py == {}", x)).unwrap(), None, Some(&locals)).unwrap(); - let roundtripped: NonZeroU128 = x_py.extract(py).unwrap(); + let roundtripped: NonZeroU128 = x_py.extract().unwrap(); assert_eq!(x, roundtripped); }) } diff --git a/src/conversions/std/option.rs b/src/conversions/std/option.rs index a9c8908faa7..aac8c7ab210 100644 --- a/src/conversions/std/option.rs +++ b/src/conversions/std/option.rs @@ -1,6 +1,6 @@ use crate::{ conversion::IntoPyObject, ffi, types::any::PyAnyMethods, AsPyPointer, Bound, BoundObject, - FromPyObject, IntoPy, PyAny, PyObject, PyResult, Python, + FromPyObject, PyAny, PyObject, PyResult, Python, }; /// `Option::Some` is converted like `T`. @@ -16,9 +16,10 @@ where } } -impl IntoPy for Option +#[allow(deprecated)] +impl crate::IntoPy for Option where - T: IntoPy, + T: crate::IntoPy, { fn into_py(self, py: Python<'_>) -> PyObject { self.map_or_else(|| py.None(), |val| val.into_py(py)) diff --git a/src/conversions/std/osstr.rs b/src/conversions/std/osstr.rs index 14c5902d8d9..70b5bd152ac 100644 --- a/src/conversions/std/osstr.rs +++ b/src/conversions/std/osstr.rs @@ -3,9 +3,9 @@ use crate::ffi_ptr_ext::FfiPtrExt; use crate::instance::Bound; use crate::types::any::PyAnyMethods; use crate::types::PyString; +use crate::{ffi, FromPyObject, PyAny, PyObject, PyResult, Python}; #[allow(deprecated)] -use crate::ToPyObject; -use crate::{ffi, FromPyObject, IntoPy, PyAny, PyObject, PyResult, Python}; +use crate::{IntoPy, ToPyObject}; use std::borrow::Cow; use std::convert::Infallible; use std::ffi::{OsStr, OsString}; @@ -134,6 +134,7 @@ impl FromPyObject<'_> for OsString { } } +#[allow(deprecated)] impl IntoPy for &'_ OsStr { #[inline] fn into_py(self, py: Python<'_>) -> PyObject { @@ -149,6 +150,7 @@ impl ToPyObject for Cow<'_, OsStr> { } } +#[allow(deprecated)] impl IntoPy for Cow<'_, OsStr> { #[inline] fn into_py(self, py: Python<'_>) -> PyObject { @@ -186,6 +188,7 @@ impl ToPyObject for OsString { } } +#[allow(deprecated)] impl IntoPy for OsString { #[inline] fn into_py(self, py: Python<'_>) -> PyObject { @@ -204,6 +207,7 @@ impl<'py> IntoPyObject<'py> for OsString { } } +#[allow(deprecated)] impl IntoPy for &OsString { #[inline] fn into_py(self, py: Python<'_>) -> PyObject { @@ -225,7 +229,7 @@ impl<'py> IntoPyObject<'py> for &OsString { #[cfg(test)] mod tests { use crate::types::{PyAnyMethods, PyString, PyStringMethods}; - use crate::{BoundObject, IntoPy, IntoPyObject, PyObject, Python}; + use crate::{BoundObject, IntoPyObject, Python}; use std::fmt::Debug; use std::{ borrow::Cow, @@ -246,14 +250,14 @@ mod tests { let os_str = OsStr::from_bytes(payload); // do a roundtrip into Pythonland and back and compare - let py_str: PyObject = os_str.into_py(py); - let os_str_2: OsString = py_str.extract(py).unwrap(); + let py_str = os_str.into_pyobject(py).unwrap(); + let os_str_2: OsString = py_str.extract().unwrap(); assert_eq!(os_str, os_str_2); }); } #[test] - fn test_topyobject_roundtrip() { + fn test_intopyobject_roundtrip() { Python::with_gil(|py| { fn test_roundtrip<'py, T>(py: Python<'py>, obj: T) where @@ -273,24 +277,4 @@ mod tests { test_roundtrip::(py, os_str.to_os_string()); }); } - - #[test] - fn test_intopy_roundtrip() { - Python::with_gil(|py| { - fn test_roundtrip + AsRef + Debug + Clone>( - py: Python<'_>, - obj: T, - ) { - let pyobject = obj.clone().into_py(py); - let pystring = pyobject.downcast_bound::(py).unwrap(); - assert_eq!(pystring.to_string_lossy(), obj.as_ref().to_string_lossy()); - let roundtripped_obj: OsString = pystring.extract().unwrap(); - assert!(obj.as_ref() == roundtripped_obj.as_os_str()); - } - let os_str = OsStr::new("Hello\0\n🐍"); - test_roundtrip::<&OsStr>(py, os_str); - test_roundtrip::(py, os_str.to_os_string()); - test_roundtrip::<&OsString>(py, &os_str.to_os_string()); - }) - } } diff --git a/src/conversions/std/path.rs b/src/conversions/std/path.rs index d1b628b065e..dc528ee3595 100644 --- a/src/conversions/std/path.rs +++ b/src/conversions/std/path.rs @@ -3,9 +3,9 @@ use crate::ffi_ptr_ext::FfiPtrExt; use crate::instance::Bound; use crate::types::any::PyAnyMethods; use crate::types::PyString; +use crate::{ffi, FromPyObject, PyAny, PyObject, PyResult, Python}; #[allow(deprecated)] -use crate::ToPyObject; -use crate::{ffi, FromPyObject, IntoPy, PyAny, PyObject, PyResult, Python}; +use crate::{IntoPy, ToPyObject}; use std::borrow::Cow; use std::convert::Infallible; use std::ffi::OsString; @@ -29,6 +29,7 @@ impl FromPyObject<'_> for PathBuf { } } +#[allow(deprecated)] impl IntoPy for &Path { #[inline] fn into_py(self, py: Python<'_>) -> PyObject { @@ -66,6 +67,7 @@ impl ToPyObject for Cow<'_, Path> { } } +#[allow(deprecated)] impl IntoPy for Cow<'_, Path> { #[inline] fn into_py(self, py: Python<'_>) -> PyObject { @@ -103,6 +105,7 @@ impl ToPyObject for PathBuf { } } +#[allow(deprecated)] impl IntoPy for PathBuf { #[inline] fn into_py(self, py: Python<'_>) -> PyObject { @@ -121,6 +124,7 @@ impl<'py> IntoPyObject<'py> for PathBuf { } } +#[allow(deprecated)] impl IntoPy for &PathBuf { #[inline] fn into_py(self, py: Python<'_>) -> PyObject { @@ -142,7 +146,7 @@ impl<'py> IntoPyObject<'py> for &PathBuf { #[cfg(test)] mod tests { use crate::types::{PyAnyMethods, PyString, PyStringMethods}; - use crate::{BoundObject, IntoPy, IntoPyObject, PyObject, Python}; + use crate::{BoundObject, IntoPyObject, Python}; use std::borrow::Cow; use std::fmt::Debug; use std::path::{Path, PathBuf}; @@ -162,14 +166,14 @@ mod tests { let path = Path::new(OsStr::from_bytes(payload)); // do a roundtrip into Pythonland and back and compare - let py_str: PyObject = path.into_py(py); - let path_2: PathBuf = py_str.extract(py).unwrap(); + let py_str = path.into_pyobject(py).unwrap(); + let path_2: PathBuf = py_str.extract().unwrap(); assert_eq!(path, path_2); }); } #[test] - fn test_topyobject_roundtrip() { + fn test_intopyobject_roundtrip() { Python::with_gil(|py| { fn test_roundtrip<'py, T>(py: Python<'py>, obj: T) where @@ -189,24 +193,4 @@ mod tests { test_roundtrip::(py, path.to_path_buf()); }); } - - #[test] - fn test_intopy_roundtrip() { - Python::with_gil(|py| { - fn test_roundtrip + AsRef + Debug + Clone>( - py: Python<'_>, - obj: T, - ) { - let pyobject = obj.clone().into_py(py); - let pystring = pyobject.downcast_bound::(py).unwrap(); - assert_eq!(pystring.to_string_lossy(), obj.as_ref().to_string_lossy()); - let roundtripped_obj: PathBuf = pystring.extract().unwrap(); - assert_eq!(obj.as_ref(), roundtripped_obj.as_path()); - } - let path = Path::new("Hello\0\n🐍"); - test_roundtrip::<&Path>(py, path); - test_roundtrip::(py, path.to_path_buf()); - test_roundtrip::<&PathBuf>(py, &path.to_path_buf()); - }) - } } diff --git a/src/conversions/std/set.rs b/src/conversions/std/set.rs index 0bd1b9d7ed6..3f1e6733f1a 100644 --- a/src/conversions/std/set.rs +++ b/src/conversions/std/set.rs @@ -2,8 +2,6 @@ use std::{cmp, collections, hash}; #[cfg(feature = "experimental-inspect")] use crate::inspect::types::TypeInfo; -#[allow(deprecated)] -use crate::ToPyObject; use crate::{ conversion::IntoPyObject, instance::Bound, @@ -13,8 +11,10 @@ use crate::{ set::{new_from_iter, try_new_from_iter, PySetMethods}, PyFrozenSet, PySet, }, - FromPyObject, IntoPy, PyAny, PyErr, PyObject, PyResult, Python, + FromPyObject, PyAny, PyErr, PyObject, PyResult, Python, }; +#[allow(deprecated)] +use crate::{IntoPy, ToPyObject}; #[allow(deprecated)] impl ToPyObject for collections::HashSet @@ -41,6 +41,7 @@ where } } +#[allow(deprecated)] impl IntoPy for collections::HashSet where K: IntoPy + Eq + hash::Hash, @@ -116,6 +117,7 @@ where } } +#[allow(deprecated)] impl IntoPy for collections::BTreeSet where K: IntoPy + cmp::Ord, @@ -190,7 +192,7 @@ where #[cfg(test)] mod tests { use crate::types::{any::PyAnyMethods, PyFrozenSet, PySet}; - use crate::{IntoPy, IntoPyObject, PyObject, Python}; + use crate::{IntoPyObject, PyObject, Python}; use std::collections::{BTreeSet, HashSet}; #[test] @@ -220,7 +222,9 @@ mod tests { } #[test] + #[allow(deprecated)] fn test_set_into_py() { + use crate::IntoPy; Python::with_gil(|py| { let bt: BTreeSet = [1, 2, 3, 4, 5].iter().cloned().collect(); let hs: HashSet = [1, 2, 3, 4, 5].iter().cloned().collect(); diff --git a/src/conversions/std/slice.rs b/src/conversions/std/slice.rs index ea33cc8461b..798e5b54c45 100644 --- a/src/conversions/std/slice.rs +++ b/src/conversions/std/slice.rs @@ -2,14 +2,15 @@ use std::borrow::Cow; #[cfg(feature = "experimental-inspect")] use crate::inspect::types::TypeInfo; -#[allow(deprecated)] -use crate::ToPyObject; use crate::{ conversion::IntoPyObject, types::{PyByteArray, PyByteArrayMethods, PyBytes}, - Bound, IntoPy, Py, PyAny, PyErr, PyObject, PyResult, Python, + Bound, Py, PyAny, PyErr, PyObject, PyResult, Python, }; +#[allow(deprecated)] +use crate::{IntoPy, ToPyObject}; +#[allow(deprecated)] impl IntoPy for &[u8] { fn into_py(self, py: Python<'_>) -> PyObject { PyBytes::new(py, self).unbind().into() @@ -82,6 +83,7 @@ impl ToPyObject for Cow<'_, [u8]> { } } +#[allow(deprecated)] impl IntoPy> for Cow<'_, [u8]> { fn into_py(self, py: Python<'_>) -> Py { self.into_pyobject(py).unwrap().into_any().unbind() diff --git a/src/conversions/std/string.rs b/src/conversions/std/string.rs index b1eff507b7a..074b5c2ba73 100644 --- a/src/conversions/std/string.rs +++ b/src/conversions/std/string.rs @@ -2,14 +2,14 @@ use std::{borrow::Cow, convert::Infallible}; #[cfg(feature = "experimental-inspect")] use crate::inspect::types::TypeInfo; -#[allow(deprecated)] -use crate::ToPyObject; use crate::{ conversion::IntoPyObject, instance::Bound, types::{any::PyAnyMethods, string::PyStringMethods, PyString}, - FromPyObject, IntoPy, Py, PyAny, PyObject, PyResult, Python, + FromPyObject, Py, PyAny, PyObject, PyResult, Python, }; +#[allow(deprecated)] +use crate::{IntoPy, ToPyObject}; /// Converts a Rust `str` to a Python object. /// See `PyString::new` for details on the conversion. @@ -21,6 +21,7 @@ impl ToPyObject for str { } } +#[allow(deprecated)] impl IntoPy for &str { #[inline] fn into_py(self, py: Python<'_>) -> PyObject { @@ -28,6 +29,7 @@ impl IntoPy for &str { } } +#[allow(deprecated)] impl IntoPy> for &str { #[inline] fn into_py(self, py: Python<'_>) -> Py { @@ -77,6 +79,7 @@ impl ToPyObject for Cow<'_, str> { } } +#[allow(deprecated)] impl IntoPy for Cow<'_, str> { #[inline] fn into_py(self, py: Python<'_>) -> PyObject { @@ -134,6 +137,7 @@ impl ToPyObject for char { } } +#[allow(deprecated)] impl IntoPy for char { #[inline] fn into_py(self, py: Python<'_>) -> PyObject { @@ -173,6 +177,7 @@ impl<'py> IntoPyObject<'py> for &char { } } +#[allow(deprecated)] impl IntoPy for String { #[inline] fn into_py(self, py: Python<'_>) -> PyObject { @@ -195,6 +200,7 @@ impl<'py> IntoPyObject<'py> for String { } } +#[allow(deprecated)] impl IntoPy for &String { #[inline] fn into_py(self, py: Python<'_>) -> PyObject { @@ -276,11 +282,13 @@ impl FromPyObject<'_> for char { #[cfg(test)] mod tests { use crate::types::any::PyAnyMethods; - use crate::{IntoPy, IntoPyObject, PyObject, Python}; + use crate::{IntoPyObject, PyObject, Python}; use std::borrow::Cow; #[test] + #[allow(deprecated)] fn test_cow_into_py() { + use crate::IntoPy; Python::with_gil(|py| { let s = "Hello Python"; let py_string: PyObject = Cow::Borrowed(s).into_py(py); @@ -345,27 +353,30 @@ mod tests { } #[test] - fn test_string_into_py() { + fn test_string_into_pyobject() { Python::with_gil(|py| { let s = "Hello Python"; let s2 = s.to_owned(); let s3 = &s2; assert_eq!( s, - IntoPy::::into_py(s3, py) - .extract::>(py) + s3.into_pyobject(py) + .unwrap() + .extract::>() .unwrap() ); assert_eq!( s, - IntoPy::::into_py(s2, py) - .extract::>(py) + s2.into_pyobject(py) + .unwrap() + .extract::>() .unwrap() ); assert_eq!( s, - IntoPy::::into_py(s, py) - .extract::>(py) + s.into_pyobject(py) + .unwrap() + .extract::>() .unwrap() ); }) diff --git a/src/conversions/std/time.rs b/src/conversions/std/time.rs index c55a22666de..741c28ee905 100755 --- a/src/conversions/std/time.rs +++ b/src/conversions/std/time.rs @@ -8,9 +8,9 @@ use crate::types::PyType; use crate::types::{timezone_utc, PyDateTime, PyDelta, PyDeltaAccess}; #[cfg(Py_LIMITED_API)] use crate::Py; +use crate::{intern, Bound, FromPyObject, PyAny, PyErr, PyObject, PyResult, Python}; #[allow(deprecated)] -use crate::ToPyObject; -use crate::{intern, Bound, FromPyObject, IntoPy, PyAny, PyErr, PyObject, PyResult, Python}; +use crate::{IntoPy, ToPyObject}; use std::time::{Duration, SystemTime, UNIX_EPOCH}; const SECONDS_PER_DAY: u64 = 24 * 60 * 60; @@ -60,6 +60,7 @@ impl ToPyObject for Duration { } } +#[allow(deprecated)] impl IntoPy for Duration { #[inline] fn into_py(self, py: Python<'_>) -> PyObject { @@ -141,6 +142,7 @@ impl ToPyObject for SystemTime { } } +#[allow(deprecated)] impl IntoPy for SystemTime { #[inline] fn into_py(self, py: Python<'_>) -> PyObject { @@ -201,7 +203,6 @@ fn unix_epoch_py(py: Python<'_>) -> PyResult<&PyObject> { mod tests { use super::*; use crate::types::PyDict; - use std::panic; #[test] fn test_duration_frompyobject() { @@ -347,24 +348,26 @@ mod tests { } #[test] - fn test_time_topyobject() { + fn test_time_intopyobject() { Python::with_gil(|py| { - let assert_eq = |l: PyObject, r: Bound<'_, PyAny>| { - assert!(l.bind(py).eq(r).unwrap()); + let assert_eq = |l: Bound<'_, PyAny>, r: Bound<'_, PyAny>| { + assert!(l.eq(r).unwrap()); }; assert_eq( UNIX_EPOCH .checked_add(Duration::new(1580702706, 7123)) .unwrap() - .into_py(py), + .into_pyobject(py) + .unwrap(), new_datetime(py, 2020, 2, 3, 4, 5, 6, 7), ); assert_eq( UNIX_EPOCH .checked_add(Duration::new(253402300799, 999999000)) .unwrap() - .into_py(py), + .into_pyobject(py) + .unwrap(), max_datetime(py), ); }); @@ -403,12 +406,12 @@ mod tests { } #[test] - fn test_time_topyobject_overflow() { + fn test_time_intopyobject_overflow() { let big_system_time = UNIX_EPOCH .checked_add(Duration::new(300000000000, 0)) .unwrap(); Python::with_gil(|py| { - assert!(panic::catch_unwind(|| big_system_time.into_py(py)).is_err()); + assert!(big_system_time.into_pyobject(py).is_err()); }) } diff --git a/src/conversions/std/vec.rs b/src/conversions/std/vec.rs index a667460fd82..b630ca4019a 100644 --- a/src/conversions/std/vec.rs +++ b/src/conversions/std/vec.rs @@ -2,9 +2,9 @@ use crate::conversion::IntoPyObject; #[cfg(feature = "experimental-inspect")] use crate::inspect::types::TypeInfo; use crate::types::list::new_from_iter; +use crate::{Bound, PyAny, PyErr, PyObject, Python}; #[allow(deprecated)] -use crate::ToPyObject; -use crate::{Bound, IntoPy, PyAny, PyErr, PyObject, Python}; +use crate::{IntoPy, ToPyObject}; #[allow(deprecated)] impl ToPyObject for [T] @@ -28,6 +28,7 @@ where } } +#[allow(deprecated)] impl IntoPy for Vec where T: IntoPy, diff --git a/src/coroutine.rs b/src/coroutine.rs index 82f5460f03e..56ed58f7460 100644 --- a/src/coroutine.rs +++ b/src/coroutine.rs @@ -15,7 +15,7 @@ use crate::{ exceptions::{PyAttributeError, PyRuntimeError, PyStopIteration}, panic::PanicException, types::{string::PyStringMethods, PyIterator, PyString}, - Bound, IntoPy, Py, PyAny, PyErr, PyObject, PyResult, Python, + Bound, BoundObject, IntoPyObject, Py, PyAny, PyErr, PyObject, PyResult, Python, }; pub(crate) mod cancel; @@ -42,24 +42,27 @@ impl Coroutine { /// (should always be `None` anyway). /// /// `Coroutine `throw` drop the wrapped future and reraise the exception passed - pub(crate) fn new( - name: Option>, + pub(crate) fn new<'py, F, T, E>( + name: Option>, qualname_prefix: Option<&'static str>, throw_callback: Option, future: F, ) -> Self where F: Future> + Send + 'static, - T: IntoPy, + T: IntoPyObject<'py>, E: Into, { let wrap = async move { let obj = future.await.map_err(Into::into)?; // SAFETY: GIL is acquired when future is polled (see `Coroutine::poll`) - Ok(obj.into_py(unsafe { Python::assume_gil_acquired() })) + obj.into_pyobject(unsafe { Python::assume_gil_acquired() }) + .map(BoundObject::into_any) + .map(BoundObject::unbind) + .map_err(Into::into) }; Self { - name, + name: name.map(Bound::unbind), qualname_prefix, throw_callback, future: Some(Box::pin(wrap)), @@ -115,7 +118,7 @@ impl Coroutine { } // if waker has been waken during future polling, this is roughly equivalent to // `await asyncio.sleep(0)`, so just yield `None`. - Ok(py.None().into_py(py)) + Ok(py.None()) } } @@ -132,9 +135,11 @@ impl Coroutine { #[getter] fn __qualname__(&self, py: Python<'_>) -> PyResult> { match (&self.name, &self.qualname_prefix) { - (Some(name), Some(prefix)) => Ok(format!("{}.{}", prefix, name.bind(py).to_cow()?) + (Some(name), Some(prefix)) => format!("{}.{}", prefix, name.bind(py).to_cow()?) .as_str() - .into_py(py)), + .into_pyobject(py) + .map(BoundObject::unbind) + .map_err(Into::into), (Some(name), None) => Ok(name.clone_ref(py)), (None, _) => Err(PyAttributeError::new_err("__qualname__")), } diff --git a/src/err/impls.rs b/src/err/impls.rs index 81d0b2ae81b..b84f46d4306 100644 --- a/src/err/impls.rs +++ b/src/err/impls.rs @@ -1,4 +1,5 @@ -use crate::{err::PyErrArguments, exceptions, IntoPy, PyErr, PyObject, Python}; +use crate::IntoPyObject; +use crate::{err::PyErrArguments, exceptions, PyErr, PyObject, Python}; use std::io; /// Convert `PyErr` to `io::Error` @@ -60,7 +61,12 @@ impl From for PyErr { impl PyErrArguments for io::Error { fn arguments(self, py: Python<'_>) -> PyObject { - self.to_string().into_py(py) + //FIXME(icxolu) remove unwrap + self.to_string() + .into_pyobject(py) + .unwrap() + .into_any() + .unbind() } } @@ -86,7 +92,12 @@ macro_rules! impl_to_pyerr { ($err: ty, $pyexc: ty) => { impl PyErrArguments for $err { fn arguments(self, py: Python<'_>) -> PyObject { - self.to_string().into_py(py) + // FIXME(icxolu) remove unwrap + self.to_string() + .into_pyobject(py) + .unwrap() + .into_any() + .unbind() } } diff --git a/src/err/mod.rs b/src/err/mod.rs index 6d8259a2919..20108f6e8dc 100644 --- a/src/err/mod.rs +++ b/src/err/mod.rs @@ -3,13 +3,13 @@ use crate::panic::PanicException; use crate::type_object::PyTypeInfo; use crate::types::any::PyAnyMethods; use crate::types::{string::PyStringMethods, typeobject::PyTypeMethods, PyTraceback, PyType}; -#[allow(deprecated)] -use crate::ToPyObject; use crate::{ exceptions::{self, PyBaseException}, ffi, }; -use crate::{Borrowed, BoundObject, IntoPy, Py, PyAny, PyObject, Python}; +use crate::{Borrowed, BoundObject, Py, PyAny, PyObject, Python}; +#[allow(deprecated)] +use crate::{IntoPy, ToPyObject}; use std::borrow::Cow; use std::ffi::{CStr, CString}; @@ -246,7 +246,7 @@ impl PyErr { // is not the case let obj = err.into_inner(); let py = obj.py(); - PyErrState::lazy_arguments(obj.into_py(py), py.None()) + PyErrState::lazy_arguments(obj.unbind(), py.None()) } }; @@ -793,6 +793,7 @@ impl std::fmt::Display for PyErr { impl std::error::Error for PyErr {} +#[allow(deprecated)] impl IntoPy for PyErr { #[inline] fn into_py(self, py: Python<'_>) -> PyObject { @@ -808,6 +809,7 @@ impl ToPyObject for PyErr { } } +#[allow(deprecated)] impl IntoPy for &PyErr { #[inline] fn into_py(self, py: Python<'_>) -> PyObject { diff --git a/src/ffi/tests.rs b/src/ffi/tests.rs index cd01eed4b1e..3396e409368 100644 --- a/src/ffi/tests.rs +++ b/src/ffi/tests.rs @@ -6,7 +6,7 @@ use crate::Python; use crate::types::PyString; #[cfg(not(Py_LIMITED_API))] -use crate::{types::PyDict, Bound, IntoPy, Py, PyAny}; +use crate::{types::PyDict, Bound, PyAny}; #[cfg(not(any(Py_3_12, Py_LIMITED_API)))] use libc::wchar_t; @@ -14,8 +14,9 @@ use libc::wchar_t; #[cfg_attr(target_arch = "wasm32", ignore)] // DateTime import fails on wasm for mysterious reasons #[test] fn test_datetime_fromtimestamp() { + use crate::IntoPyObject; Python::with_gil(|py| { - let args: Py = (100,).into_py(py); + let args = (100,).into_pyobject(py).unwrap(); let dt = unsafe { PyDateTime_IMPORT(); Bound::from_owned_ptr(py, PyDateTime_FromTimestamp(args.as_ptr())) @@ -35,8 +36,9 @@ fn test_datetime_fromtimestamp() { #[cfg_attr(target_arch = "wasm32", ignore)] // DateTime import fails on wasm for mysterious reasons #[test] fn test_date_fromtimestamp() { + use crate::IntoPyObject; Python::with_gil(|py| { - let args: Py = (100,).into_py(py); + let args = (100,).into_pyobject(py).unwrap(); let dt = unsafe { PyDateTime_IMPORT(); Bound::from_owned_ptr(py, PyDate_FromTimestamp(args.as_ptr())) diff --git a/src/impl_/coroutine.rs b/src/impl_/coroutine.rs index 1d3119400a0..f893a2c2fe9 100644 --- a/src/impl_/coroutine.rs +++ b/src/impl_/coroutine.rs @@ -9,26 +9,21 @@ use crate::{ pycell::impl_::PyClassBorrowChecker, pyclass::boolean_struct::False, types::{PyAnyMethods, PyString}, - IntoPy, Py, PyAny, PyClass, PyErr, PyObject, PyResult, Python, + IntoPyObject, Py, PyAny, PyClass, PyErr, PyResult, Python, }; -pub fn new_coroutine( - name: &Bound<'_, PyString>, +pub fn new_coroutine<'py, F, T, E>( + name: &Bound<'py, PyString>, qualname_prefix: Option<&'static str>, throw_callback: Option, future: F, ) -> Coroutine where F: Future> + Send + 'static, - T: IntoPy, + T: IntoPyObject<'py>, E: Into, { - Coroutine::new( - Some(name.clone().into()), - qualname_prefix, - throw_callback, - future, - ) + Coroutine::new(Some(name.clone()), qualname_prefix, throw_callback, future) } fn get_ptr(obj: &Py) -> *mut T { diff --git a/src/impl_/pyclass.rs b/src/impl_/pyclass.rs index 5a1fb963271..85cf95a9e11 100644 --- a/src/impl_/pyclass.rs +++ b/src/impl_/pyclass.rs @@ -1,5 +1,3 @@ -#[allow(deprecated)] -use crate::ToPyObject; use crate::{ conversion::IntoPyObject, exceptions::{PyAttributeError, PyNotImplementedError, PyRuntimeError, PyValueError}, @@ -12,8 +10,10 @@ use crate::{ }, pycell::PyBorrowError, types::{any::PyAnyMethods, PyBool}, - Borrowed, BoundObject, IntoPy, Py, PyAny, PyClass, PyErr, PyRef, PyResult, PyTypeInfo, Python, + Borrowed, BoundObject, Py, PyAny, PyClass, PyErr, PyRef, PyResult, PyTypeInfo, Python, }; +#[allow(deprecated)] +use crate::{IntoPy, ToPyObject}; use std::{ borrow::Cow, ffi::{CStr, CString}, @@ -1372,6 +1372,7 @@ where } /// IntoPy + Clone fallback case, which was the only behaviour before PyO3 0.22. +#[allow(deprecated)] impl PyClassGetterGenerator where @@ -1452,6 +1453,7 @@ impl IsToPyObject { probe!(IsIntoPy); +#[allow(deprecated)] impl> IsIntoPy { pub const VALUE: bool = true; } @@ -1556,6 +1558,7 @@ where .into_ptr()) } +#[allow(deprecated)] fn pyo3_get_value< ClassT: PyClass, FieldT: IntoPy> + Clone, diff --git a/src/impl_/wrap.rs b/src/impl_/wrap.rs index c999cf40249..9381828245a 100644 --- a/src/impl_/wrap.rs +++ b/src/impl_/wrap.rs @@ -1,8 +1,9 @@ use std::{convert::Infallible, marker::PhantomData, ops::Deref}; +#[allow(deprecated)] +use crate::IntoPy; use crate::{ - conversion::IntoPyObject, ffi, types::PyNone, Bound, BoundObject, IntoPy, PyObject, PyResult, - Python, + conversion::IntoPyObject, ffi, types::PyNone, Bound, BoundObject, PyObject, PyResult, Python, }; /// Used to wrap values in `Option` for default arguments. @@ -112,6 +113,7 @@ impl<'py, T: IntoPyObject<'py>, E> IntoPyObjectConverter> { } } +#[allow(deprecated)] impl> IntoPyConverter { #[inline] pub fn wrap(&self, obj: T) -> Result { @@ -119,6 +121,7 @@ impl> IntoPyConverter { } } +#[allow(deprecated)] impl, E> IntoPyConverter> { #[inline] pub fn wrap(&self, obj: Result) -> Result { diff --git a/src/instance.rs b/src/instance.rs index 6b191abd5a2..99643e12eb1 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -6,13 +6,13 @@ use crate::pycell::{PyBorrowError, PyBorrowMutError}; use crate::pyclass::boolean_struct::{False, True}; use crate::types::{any::PyAnyMethods, string::PyStringMethods, typeobject::PyTypeMethods}; use crate::types::{DerefToPyAny, PyDict, PyString, PyTuple}; -#[allow(deprecated)] -use crate::ToPyObject; use crate::{ - ffi, AsPyPointer, DowncastError, FromPyObject, IntoPy, PyAny, PyClass, PyClassInitializer, - PyRef, PyRefMut, PyTypeInfo, Python, + ffi, AsPyPointer, DowncastError, FromPyObject, PyAny, PyClass, PyClassInitializer, PyRef, + PyRefMut, PyTypeInfo, Python, }; use crate::{gil, PyTypeCheck}; +#[allow(deprecated)] +use crate::{IntoPy, ToPyObject}; use std::marker::PhantomData; use std::mem::ManuallyDrop; use std::ops::Deref; @@ -830,6 +830,7 @@ impl ToPyObject for Borrowed<'_, '_, T> { } } +#[allow(deprecated)] impl IntoPy for Borrowed<'_, '_, T> { /// Converts `Py` instance -> PyObject. #[inline] @@ -1465,7 +1466,7 @@ impl Py { /// # Example: `intern!`ing the attribute name /// /// ``` - /// # use pyo3::{intern, pyfunction, types::PyModule, IntoPy, PyObject, Python, PyResult}; + /// # use pyo3::{intern, pyfunction, types::PyModule, IntoPyObject, PyObject, Python, PyResult}; /// # /// #[pyfunction] /// fn set_answer(ob: PyObject, py: Python<'_>) -> PyResult<()> { @@ -1473,7 +1474,7 @@ impl Py { /// } /// # /// # Python::with_gil(|py| { - /// # let ob = PyModule::new(py, "empty").unwrap().into_py(py); + /// # let ob = PyModule::new(py, "empty").unwrap().into_pyobject(py).unwrap().into_any().unbind(); /// # set_answer(ob, py).unwrap(); /// # }); /// ``` @@ -1497,11 +1498,19 @@ impl Py { where A: IntoPyObject<'py, Target = PyTuple>, { - self.bind(py).as_any().call(args, kwargs).map(Bound::unbind) + self.bind(py) + .as_any() + .call( + // FIXME(icxolu): remove explicit args conversion + args.into_pyobject(py).map_err(Into::into)?.into_bound(), + kwargs, + ) + .map(Bound::unbind) } /// Deprecated name for [`Py::call`]. #[deprecated(since = "0.23.0", note = "renamed to `Py::call`")] + #[allow(deprecated)] #[inline] pub fn call_bound( &self, @@ -1519,7 +1528,11 @@ impl Py { where N: IntoPyObject<'py, Target = PyTuple>, { - self.bind(py).as_any().call1(args).map(Bound::unbind) + self.bind(py) + .as_any() + // FIXME(icxolu): remove explicit args conversion + .call1(args.into_pyobject(py).map_err(Into::into)?.into_bound()) + .map(Bound::unbind) } /// Calls the object without arguments. @@ -1548,12 +1561,18 @@ impl Py { { self.bind(py) .as_any() - .call_method(name, args, kwargs) + .call_method( + name, + // FIXME(icxolu): remove explicit args conversion + args.into_pyobject(py).map_err(Into::into)?.into_bound(), + kwargs, + ) .map(Bound::unbind) } /// Deprecated name for [`Py::call_method`]. #[deprecated(since = "0.23.0", note = "renamed to `Py::call_method`")] + #[allow(deprecated)] #[inline] pub fn call_method_bound( &self, @@ -1582,7 +1601,11 @@ impl Py { { self.bind(py) .as_any() - .call_method1(name, args) + .call_method1( + name, + // FIXME(icxolu): remove explicit args conversion + args.into_pyobject(py).map_err(Into::into)?.into_bound(), + ) .map(Bound::unbind) } @@ -1720,6 +1743,7 @@ impl ToPyObject for Py { } } +#[allow(deprecated)] impl IntoPy for Py { /// Converts a `Py` instance to `PyObject`. /// Consumes `self` without calling `Py_DECREF()`. @@ -1729,6 +1753,7 @@ impl IntoPy for Py { } } +#[allow(deprecated)] impl IntoPy for &'_ Py { #[inline] fn into_py(self, py: Python<'_>) -> PyObject { @@ -1745,6 +1770,7 @@ impl ToPyObject for Bound<'_, T> { } } +#[allow(deprecated)] impl IntoPy for Bound<'_, T> { /// Converts a `Bound` instance to `PyObject`. #[inline] @@ -1753,6 +1779,7 @@ impl IntoPy for Bound<'_, T> { } } +#[allow(deprecated)] impl IntoPy for &Bound<'_, T> { /// Converts `&Bound` instance -> PyObject, increasing the reference count. #[inline] @@ -1785,8 +1812,7 @@ where { #[inline] fn from(other: Bound<'_, T>) -> Self { - let py = other.py(); - other.into_py(py) + other.into_any().unbind() } } @@ -1928,7 +1954,7 @@ impl PyObject { /// } /// /// Python::with_gil(|py| { - /// let class: PyObject = Py::new(py, Class { i: 0 }).unwrap().into_py(py); + /// let class: PyObject = Py::new(py, Class { i: 0 })?.into_any(); /// /// let class_bound = class.downcast_bound::(py)?; /// diff --git a/src/lib.rs b/src/lib.rs index d6bf1b374c3..b4fcf918fae 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -323,9 +323,9 @@ #![doc = concat!("[Features chapter of the guide]: https://pyo3.rs/v", env!("CARGO_PKG_VERSION"), "/features.html#features-reference \"Features Reference - PyO3 user guide\"")] //! [`Ungil`]: crate::marker::Ungil pub use crate::class::*; +pub use crate::conversion::{AsPyPointer, FromPyObject, IntoPyObject}; #[allow(deprecated)] -pub use crate::conversion::ToPyObject; -pub use crate::conversion::{AsPyPointer, FromPyObject, IntoPy, IntoPyObject}; +pub use crate::conversion::{IntoPy, ToPyObject}; pub use crate::err::{DowncastError, DowncastIntoError, PyErr, PyErrArguments, PyResult, ToPyErr}; #[cfg(not(any(PyPy, GraalPy)))] pub use crate::gil::{prepare_freethreaded_python, with_embedded_python_interpreter}; diff --git a/src/marker.rs b/src/marker.rs index 0fedf3f8c81..1a4c0482569 100644 --- a/src/marker.rs +++ b/src/marker.rs @@ -129,7 +129,9 @@ use crate::types::{ PyAny, PyDict, PyEllipsis, PyModule, PyNone, PyNotImplemented, PyString, PyType, }; use crate::version::PythonVersionInfo; -use crate::{ffi, Bound, IntoPy, Py, PyObject, PyTypeInfo}; +#[allow(deprecated)] +use crate::IntoPy; +use crate::{ffi, Bound, Py, PyObject, PyTypeInfo}; use std::ffi::{CStr, CString}; use std::marker::PhantomData; use std::os::raw::c_int; @@ -715,6 +717,7 @@ impl<'py> Python<'py> { /// Deprecated name for [`Python::import`]. #[deprecated(since = "0.23.0", note = "renamed to `Python::import`")] + #[allow(deprecated)] #[track_caller] #[inline] pub fn import_bound(self, name: N) -> PyResult> @@ -728,21 +731,21 @@ impl<'py> Python<'py> { #[allow(non_snake_case)] // the Python keyword starts with uppercase #[inline] pub fn None(self) -> PyObject { - PyNone::get(self).into_py(self) + PyNone::get(self).to_owned().into_any().unbind() } /// Gets the Python builtin value `Ellipsis`, or `...`. #[allow(non_snake_case)] // the Python keyword starts with uppercase #[inline] pub fn Ellipsis(self) -> PyObject { - PyEllipsis::get(self).into_py(self) + PyEllipsis::get(self).to_owned().into_any().unbind() } /// Gets the Python builtin value `NotImplemented`. #[allow(non_snake_case)] // the Python keyword starts with uppercase #[inline] pub fn NotImplemented(self) -> PyObject { - PyNotImplemented::get(self).into_py(self) + PyNotImplemented::get(self).to_owned().into_any().unbind() } /// Gets the running Python interpreter version as a string. diff --git a/src/prelude.rs b/src/prelude.rs index 7624d37a1e9..54f5a9f6beb 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -8,9 +8,9 @@ //! use pyo3::prelude::*; //! ``` +pub use crate::conversion::{FromPyObject, IntoPyObject}; #[allow(deprecated)] -pub use crate::conversion::ToPyObject; -pub use crate::conversion::{FromPyObject, IntoPy, IntoPyObject}; +pub use crate::conversion::{IntoPy, ToPyObject}; pub use crate::err::{PyErr, PyResult}; pub use crate::instance::{Borrowed, Bound, Py, PyObject}; pub use crate::marker::Python; diff --git a/src/pybacked.rs b/src/pybacked.rs index 5a45c8f1036..173d8e0e9e4 100644 --- a/src/pybacked.rs +++ b/src/pybacked.rs @@ -2,15 +2,15 @@ use std::{convert::Infallible, ops::Deref, ptr::NonNull, sync::Arc}; -#[allow(deprecated)] -use crate::ToPyObject; use crate::{ types::{ any::PyAnyMethods, bytearray::PyByteArrayMethods, bytes::PyBytesMethods, string::PyStringMethods, PyByteArray, PyBytes, PyString, }, - Bound, DowncastError, FromPyObject, IntoPy, IntoPyObject, Py, PyAny, PyErr, PyResult, Python, + Bound, DowncastError, FromPyObject, IntoPyObject, Py, PyAny, PyErr, PyResult, Python, }; +#[allow(deprecated)] +use crate::{IntoPy, ToPyObject}; /// A wrapper around `str` where the storage is owned by a Python `bytes` or `str` object. /// @@ -99,6 +99,7 @@ impl ToPyObject for PyBackedStr { } } +#[allow(deprecated)] impl IntoPy> for PyBackedStr { #[cfg(any(Py_3_10, not(Py_LIMITED_API)))] fn into_py(self, _py: Python<'_>) -> Py { @@ -248,6 +249,7 @@ impl ToPyObject for PyBackedBytes { } } +#[allow(deprecated)] impl IntoPy> for PyBackedBytes { fn into_py(self, py: Python<'_>) -> Py { match self.storage { @@ -403,6 +405,7 @@ mod test { } #[test] + #[allow(deprecated)] fn py_backed_str_into_py() { Python::with_gil(|py| { let orig_str = PyString::new(py, "hello"); @@ -451,6 +454,7 @@ mod test { } #[test] + #[allow(deprecated)] fn py_backed_bytes_into_pyobject() { Python::with_gil(|py| { let orig_bytes = PyBytes::new(py, b"abcde"); @@ -464,6 +468,7 @@ mod test { } #[test] + #[allow(deprecated)] fn rust_backed_bytes_into_pyobject() { Python::with_gil(|py| { let orig_bytes = PyByteArray::new(py, b"abcde"); diff --git a/src/pycell.rs b/src/pycell.rs index 1451e5499ce..51c9f201068 100644 --- a/src/pycell.rs +++ b/src/pycell.rs @@ -199,7 +199,9 @@ use crate::ffi_ptr_ext::FfiPtrExt; use crate::internal_tricks::{ptr_from_mut, ptr_from_ref}; use crate::pyclass::{boolean_struct::False, PyClass}; use crate::types::any::PyAnyMethods; -use crate::{ffi, Borrowed, Bound, IntoPy, PyErr, PyObject, Python}; +#[allow(deprecated)] +use crate::IntoPy; +use crate::{ffi, Borrowed, Bound, PyErr, PyObject, Python}; use std::convert::Infallible; use std::fmt; use std::mem::ManuallyDrop; @@ -447,12 +449,14 @@ impl Drop for PyRef<'_, T> { } } +#[allow(deprecated)] impl IntoPy for PyRef<'_, T> { fn into_py(self, py: Python<'_>) -> PyObject { unsafe { PyObject::from_borrowed_ptr(py, self.inner.as_ptr()) } } } +#[allow(deprecated)] impl IntoPy for &'_ PyRef<'_, T> { fn into_py(self, py: Python<'_>) -> PyObject { unsafe { PyObject::from_borrowed_ptr(py, self.inner.as_ptr()) } @@ -636,12 +640,14 @@ impl> Drop for PyRefMut<'_, T> { } } +#[allow(deprecated)] impl> IntoPy for PyRefMut<'_, T> { fn into_py(self, py: Python<'_>) -> PyObject { unsafe { PyObject::from_borrowed_ptr(py, self.inner.as_ptr()) } } } +#[allow(deprecated)] impl> IntoPy for &'_ PyRefMut<'_, T> { fn into_py(self, py: Python<'_>) -> PyObject { self.inner.clone().into_py(py) diff --git a/src/types/any.rs b/src/types/any.rs index 62f7cdfc27e..e620cf6d137 100644 --- a/src/types/any.rs +++ b/src/types/any.rs @@ -227,12 +227,11 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed { /// ```rust /// use pyo3::class::basic::CompareOp; /// use pyo3::prelude::*; - /// use pyo3::types::PyInt; /// /// # fn main() -> PyResult<()> { /// Python::with_gil(|py| -> PyResult<()> { - /// let a: Bound<'_, PyInt> = 0_u8.into_py(py).into_bound(py).downcast_into()?; - /// let b: Bound<'_, PyInt> = 42_u8.into_py(py).into_bound(py).downcast_into()?; + /// let a = 0_u8.into_pyobject(py)?; + /// let b = 42_u8.into_pyobject(py)?; /// assert!(a.rich_compare(b, CompareOp::Le)?.is_truthy()?); /// Ok(()) /// })?; diff --git a/src/types/boolobject.rs b/src/types/boolobject.rs index d54bddc8848..53043fa798c 100644 --- a/src/types/boolobject.rs +++ b/src/types/boolobject.rs @@ -1,12 +1,11 @@ #[cfg(feature = "experimental-inspect")] use crate::inspect::types::TypeInfo; -#[allow(deprecated)] -use crate::ToPyObject; use crate::{ exceptions::PyTypeError, ffi, ffi_ptr_ext::FfiPtrExt, instance::Bound, - types::typeobject::PyTypeMethods, Borrowed, FromPyObject, IntoPy, PyAny, PyObject, PyResult, - Python, + types::typeobject::PyTypeMethods, Borrowed, FromPyObject, PyAny, PyObject, PyResult, Python, }; +#[allow(deprecated)] +use crate::{IntoPy, ToPyObject}; use super::any::PyAnyMethods; use crate::conversion::IntoPyObject; @@ -155,6 +154,7 @@ impl ToPyObject for bool { } } +#[allow(deprecated)] impl IntoPy for bool { #[inline] fn into_py(self, py: Python<'_>) -> PyObject { diff --git a/src/types/datetime.rs b/src/types/datetime.rs index ac956c250d3..8ab512ac466 100644 --- a/src/types/datetime.rs +++ b/src/types/datetime.rs @@ -25,7 +25,7 @@ use crate::ffi_ptr_ext::FfiPtrExt; use crate::py_result_ext::PyResultExt; use crate::types::any::PyAnyMethods; use crate::types::PyTuple; -use crate::{Bound, IntoPy, Py, PyAny, PyErr, Python}; +use crate::{Bound, IntoPyObject, PyAny, PyErr, Python}; use std::os::raw::c_int; #[cfg(feature = "chrono")] use std::ptr; @@ -409,7 +409,7 @@ impl PyDateTime { timestamp: f64, tzinfo: Option<&Bound<'py, PyTzInfo>>, ) -> PyResult> { - let args = IntoPy::>::into_py((timestamp, tzinfo), py).into_bound(py); + let args = (timestamp, tzinfo).into_pyobject(py)?; // safety ensure API is loaded let _api = ensure_datetime_api(py)?; diff --git a/src/types/float.rs b/src/types/float.rs index 8438629e577..3c2d6643d18 100644 --- a/src/types/float.rs +++ b/src/types/float.rs @@ -2,12 +2,12 @@ use super::any::PyAnyMethods; use crate::conversion::IntoPyObject; #[cfg(feature = "experimental-inspect")] use crate::inspect::types::TypeInfo; -#[allow(deprecated)] -use crate::ToPyObject; use crate::{ - ffi, ffi_ptr_ext::FfiPtrExt, instance::Bound, Borrowed, FromPyObject, IntoPy, PyAny, PyErr, - PyObject, PyResult, Python, + ffi, ffi_ptr_ext::FfiPtrExt, instance::Bound, Borrowed, FromPyObject, PyAny, PyErr, PyObject, + PyResult, Python, }; +#[allow(deprecated)] +use crate::{IntoPy, ToPyObject}; use std::convert::Infallible; use std::os::raw::c_double; @@ -86,6 +86,7 @@ impl ToPyObject for f64 { } } +#[allow(deprecated)] impl IntoPy for f64 { #[inline] fn into_py(self, py: Python<'_>) -> PyObject { @@ -163,6 +164,7 @@ impl ToPyObject for f32 { } } +#[allow(deprecated)] impl IntoPy for f32 { #[inline] fn into_py(self, py: Python<'_>) -> PyObject { diff --git a/src/types/function.rs b/src/types/function.rs index f443403aa67..039e2774546 100644 --- a/src/types/function.rs +++ b/src/types/function.rs @@ -7,7 +7,7 @@ use crate::{ impl_::pymethods::{self, PyMethodDef}, types::{PyCapsule, PyDict, PyModule, PyString, PyTuple}, }; -use crate::{Bound, IntoPy, Py, PyAny, PyResult, Python}; +use crate::{Bound, Py, PyAny, PyResult, Python}; use std::cell::UnsafeCell; use std::ffi::CStr; @@ -155,7 +155,7 @@ impl PyCFunction { ) -> PyResult> { let (mod_ptr, module_name): (_, Option>) = if let Some(m) = module { let mod_ptr = m.as_ptr(); - (mod_ptr, Some(m.name()?.into_py(py))) + (mod_ptr, Some(m.name()?.unbind())) } else { (std::ptr::null_mut(), None) }; diff --git a/src/types/module.rs b/src/types/module.rs index 3822ed86714..f3490385721 100644 --- a/src/types/module.rs +++ b/src/types/module.rs @@ -97,6 +97,7 @@ impl PyModule { /// Deprecated name for [`PyModule::import`]. #[deprecated(since = "0.23.0", note = "renamed to `PyModule::import`")] + #[allow(deprecated)] #[inline] pub fn import_bound(py: Python<'_>, name: N) -> PyResult> where diff --git a/src/types/none.rs b/src/types/none.rs index 9a0c9b11f45..1ec12d3f5b0 100644 --- a/src/types/none.rs +++ b/src/types/none.rs @@ -1,9 +1,7 @@ use crate::ffi_ptr_ext::FfiPtrExt; +use crate::{ffi, types::any::PyAnyMethods, Borrowed, Bound, PyAny, PyObject, PyTypeInfo, Python}; #[allow(deprecated)] -use crate::ToPyObject; -use crate::{ - ffi, types::any::PyAnyMethods, Borrowed, Bound, IntoPy, PyAny, PyObject, PyTypeInfo, Python, -}; +use crate::{IntoPy, ToPyObject}; /// Represents the Python `None` object. /// @@ -58,6 +56,7 @@ impl ToPyObject for () { } } +#[allow(deprecated)] impl IntoPy for () { #[inline] fn into_py(self, py: Python<'_>) -> PyObject { @@ -69,7 +68,8 @@ impl IntoPy for () { mod tests { use crate::types::any::PyAnyMethods; use crate::types::{PyDict, PyNone}; - use crate::{IntoPy, PyObject, PyTypeInfo, Python}; + use crate::{PyObject, PyTypeInfo, Python}; + #[test] fn test_none_is_itself() { Python::with_gil(|py| { @@ -102,7 +102,9 @@ mod tests { } #[test] + #[allow(deprecated)] fn test_unit_into_py_is_none() { + use crate::IntoPy; Python::with_gil(|py| { let obj: PyObject = ().into_py(py); assert!(obj.downcast_bound::(py).is_ok()); diff --git a/src/types/num.rs b/src/types/num.rs index f33d85f4a1c..0e377f66d48 100644 --- a/src/types/num.rs +++ b/src/types/num.rs @@ -60,8 +60,7 @@ int_compare!(usize); #[cfg(test)] mod tests { - use super::PyInt; - use crate::{types::PyAnyMethods, IntoPy, Python}; + use crate::{IntoPyObject, Python}; #[test] fn test_partial_eq() { @@ -78,7 +77,7 @@ mod tests { let v_u128 = 123u128; let v_isize = 123isize; let v_usize = 123usize; - let obj = 123_i64.into_py(py).downcast_bound(py).unwrap().clone(); + let obj = 123_i64.into_pyobject(py).unwrap(); assert_eq!(v_i8, obj); assert_eq!(obj, v_i8); @@ -116,11 +115,7 @@ mod tests { assert_eq!(obj, v_usize); let big_num = (u8::MAX as u16) + 1; - let big_obj = big_num - .into_py(py) - .into_bound(py) - .downcast_into::() - .unwrap(); + let big_obj = big_num.into_pyobject(py).unwrap(); for x in 0u8..=u8::MAX { assert_ne!(x, big_obj); diff --git a/src/types/string.rs b/src/types/string.rs index 1bcd025d1ce..65a9e85fa3e 100644 --- a/src/types/string.rs +++ b/src/types/string.rs @@ -6,7 +6,9 @@ use crate::py_result_ext::PyResultExt; use crate::types::any::PyAnyMethods; use crate::types::bytes::PyBytesMethods; use crate::types::PyBytes; -use crate::{ffi, Bound, IntoPy, Py, PyAny, PyResult, Python}; +#[allow(deprecated)] +use crate::IntoPy; +use crate::{ffi, Bound, Py, PyAny, PyResult, Python}; use std::borrow::Cow; use std::str; @@ -444,18 +446,21 @@ impl Py { } } +#[allow(deprecated)] impl IntoPy> for Bound<'_, PyString> { fn into_py(self, _py: Python<'_>) -> Py { self.unbind() } } +#[allow(deprecated)] impl IntoPy> for &Bound<'_, PyString> { fn into_py(self, _py: Python<'_>) -> Py { self.clone().unbind() } } +#[allow(deprecated)] impl IntoPy> for &'_ Py { fn into_py(self, py: Python<'_>) -> Py { self.clone_ref(py) @@ -805,7 +810,7 @@ mod tests { fn test_py_to_str_utf8() { Python::with_gil(|py| { let s = "ascii 🐈"; - let py_string: Py = PyString::new(py, s).into_py(py); + let py_string = PyString::new(py, s).unbind(); #[cfg(any(Py_3_10, not(Py_LIMITED_API)))] assert_eq!(s, py_string.to_str(py).unwrap()); diff --git a/src/types/traceback.rs b/src/types/traceback.rs index 6c9909e7113..885c0f67031 100644 --- a/src/types/traceback.rs +++ b/src/types/traceback.rs @@ -80,10 +80,11 @@ impl<'py> PyTracebackMethods<'py> for Bound<'py, PyTraceback> { #[cfg(test)] mod tests { + use crate::IntoPyObject; use crate::{ ffi, types::{any::PyAnyMethods, dict::PyDictMethods, traceback::PyTracebackMethods, PyDict}, - IntoPy, PyErr, Python, + PyErr, Python, }; #[test] @@ -143,7 +144,7 @@ def f(): let f = locals.get_item("f").unwrap().unwrap(); let err = f.call0().unwrap_err(); let traceback = err.traceback(py).unwrap(); - let err_object = err.clone_ref(py).into_py(py).into_bound(py); + let err_object = err.clone_ref(py).into_pyobject(py).unwrap(); assert!(err_object.getattr("__traceback__").unwrap().is(&traceback)); }) diff --git a/src/types/tuple.rs b/src/types/tuple.rs index 1b6a95abb14..3a1f92815c2 100644 --- a/src/types/tuple.rs +++ b/src/types/tuple.rs @@ -8,12 +8,11 @@ use crate::inspect::types::TypeInfo; use crate::instance::Borrowed; use crate::internal_tricks::get_ssize_index; use crate::types::{any::PyAnyMethods, sequence::PySequenceMethods, PyList, PySequence}; -#[allow(deprecated)] -use crate::ToPyObject; use crate::{ - exceptions, Bound, BoundObject, FromPyObject, IntoPy, Py, PyAny, PyErr, PyObject, PyResult, - Python, + exceptions, Bound, BoundObject, FromPyObject, Py, PyAny, PyErr, PyObject, PyResult, Python, }; +#[allow(deprecated)] +use crate::{IntoPy, ToPyObject}; #[inline] #[track_caller] @@ -496,12 +495,14 @@ impl ExactSizeIterator for BorrowedTupleIterator<'_, '_> { impl FusedIterator for BorrowedTupleIterator<'_, '_> {} +#[allow(deprecated)] impl IntoPy> for Bound<'_, PyTuple> { fn into_py(self, _: Python<'_>) -> Py { self.unbind() } } +#[allow(deprecated)] impl IntoPy> for &'_ Bound<'_, PyTuple> { fn into_py(self, _: Python<'_>) -> Py { self.clone().unbind() @@ -525,6 +526,8 @@ macro_rules! tuple_conversion ({$length:expr,$(($refN:ident, $n:tt, $T:ident)),+ array_into_tuple(py, [$(self.$n.to_object(py)),+]).into() } } + + #[allow(deprecated)] impl <$($T: IntoPy),+> IntoPy for ($($T,)+) { fn into_py(self, py: Python<'_>) -> PyObject { array_into_tuple(py, [$(self.$n.into_py(py)),+]).into() @@ -568,6 +571,7 @@ macro_rules! tuple_conversion ({$length:expr,$(($refN:ident, $n:tt, $T:ident)),+ } } + #[allow(deprecated)] impl <$($T: IntoPy),+> IntoPy> for ($($T,)+) { fn into_py(self, py: Python<'_>) -> Py { array_into_tuple(py, [$(self.$n.into_py(py)),+]) diff --git a/tests/test_arithmetics.rs b/tests/test_arithmetics.rs index e7914a0bb95..21e32d4c13c 100644 --- a/tests/test_arithmetics.rs +++ b/tests/test_arithmetics.rs @@ -1,8 +1,8 @@ #![cfg(feature = "macros")] use pyo3::class::basic::CompareOp; -use pyo3::prelude::*; use pyo3::py_run; +use pyo3::{prelude::*, BoundObject}; #[path = "../src/tests/common.rs"] mod common; @@ -527,11 +527,19 @@ impl RichComparisons2 { "RC2" } - fn __richcmp__(&self, other: &Bound<'_, PyAny>, op: CompareOp) -> PyObject { + fn __richcmp__(&self, other: &Bound<'_, PyAny>, op: CompareOp) -> PyResult { match op { - CompareOp::Eq => true.into_py(other.py()), - CompareOp::Ne => false.into_py(other.py()), - _ => other.py().NotImplemented(), + CompareOp::Eq => true + .into_pyobject(other.py()) + .map_err(Into::into) + .map(BoundObject::into_any) + .map(BoundObject::unbind), + CompareOp::Ne => false + .into_pyobject(other.py()) + .map_err(Into::into) + .map(BoundObject::into_any) + .map(BoundObject::unbind), + _ => Ok(other.py().NotImplemented()), } } } diff --git a/tests/test_buffer_protocol.rs b/tests/test_buffer_protocol.rs index 4d396f9be68..1f15e34b384 100644 --- a/tests/test_buffer_protocol.rs +++ b/tests/test_buffer_protocol.rs @@ -71,13 +71,14 @@ fn test_buffer_referenced() { let buf = { let input = vec![b' ', b'2', b'3']; Python::with_gil(|py| { - let instance: PyObject = TestBufferClass { + let instance = TestBufferClass { vec: input.clone(), drop_called: drop_called.clone(), } - .into_py(py); + .into_pyobject(py) + .unwrap(); - let buf = PyBuffer::::get(instance.bind(py)).unwrap(); + let buf = PyBuffer::::get(&instance).unwrap(); assert_eq!(buf.to_vec(py).unwrap(), input); drop(instance); buf diff --git a/tests/test_class_basics.rs b/tests/test_class_basics.rs index a48354c47c8..4a687a89eea 100644 --- a/tests/test_class_basics.rs +++ b/tests/test_class_basics.rs @@ -247,7 +247,7 @@ fn class_with_hash() { let env = [ ("obj", Py::new(py, class).unwrap().into_any()), - ("hsh", hash.into_py(py)), + ("hsh", hash.into_pyobject(py).unwrap().into_any().unbind()), ] .into_py_dict(py) .unwrap(); diff --git a/tests/test_class_conversion.rs b/tests/test_class_conversion.rs index 671dcd126b2..a1e2188e83a 100644 --- a/tests/test_class_conversion.rs +++ b/tests/test_class_conversion.rs @@ -112,11 +112,11 @@ fn test_polymorphic_container_does_not_accept_other_types() { ) .unwrap(); - let setattr = |value: PyObject| p.bind(py).setattr("inner", value); + let setattr = |value: Bound<'_, PyAny>| p.bind(py).setattr("inner", value); - assert!(setattr(1i32.into_py(py)).is_err()); - assert!(setattr(py.None()).is_err()); - assert!(setattr((1i32, 2i32).into_py(py)).is_err()); + assert!(setattr(1i32.into_pyobject(py).unwrap().into_any()).is_err()); + assert!(setattr(py.None().into_bound(py)).is_err()); + assert!(setattr((1i32, 2i32).into_pyobject(py).unwrap().into_any()).is_err()); }); } diff --git a/tests/test_enum.rs b/tests/test_enum.rs index 5e994548edd..9cb9d5fae65 100644 --- a/tests/test_enum.rs +++ b/tests/test_enum.rs @@ -257,7 +257,7 @@ fn test_simple_enum_with_hash() { let env = [ ("obj", Py::new(py, class).unwrap().into_any()), - ("hsh", hash.into_py(py)), + ("hsh", hash.into_pyobject(py).unwrap().into_any().unbind()), ] .into_py_dict(py) .unwrap(); @@ -289,7 +289,7 @@ fn test_complex_enum_with_hash() { let env = [ ("obj", Py::new(py, class).unwrap().into_any()), - ("hsh", hash.into_py(py)), + ("hsh", hash.into_pyobject(py).unwrap().into_any().unbind()), ] .into_py_dict(py) .unwrap(); diff --git a/tests/test_frompyobject.rs b/tests/test_frompyobject.rs index 6093b774733..344a47acf72 100644 --- a/tests/test_frompyobject.rs +++ b/tests/test_frompyobject.rs @@ -76,13 +76,13 @@ pub struct B { #[test] fn test_transparent_named_field_struct() { Python::with_gil(|py| { - let test: PyObject = "test".into_py(py); + let test = "test".into_pyobject(py).unwrap(); let b = test - .extract::(py) + .extract::() .expect("Failed to extract B from String"); assert_eq!(b.test, "test"); - let test: PyObject = 1.into_py(py); - let b = test.extract::(py); + let test = 1i32.into_pyobject(py).unwrap(); + let b = test.extract::(); assert!(b.is_err()); }); } @@ -96,14 +96,14 @@ pub struct D { #[test] fn test_generic_transparent_named_field_struct() { Python::with_gil(|py| { - let test: PyObject = "test".into_py(py); + let test = "test".into_pyobject(py).unwrap(); let d = test - .extract::>(py) + .extract::>() .expect("Failed to extract D from String"); assert_eq!(d.test, "test"); - let test = 1usize.into_py(py); + let test = 1usize.into_pyobject(py).unwrap(); let d = test - .extract::>(py) + .extract::>() .expect("Failed to extract D from String"); assert_eq!(d.test, 1); }); @@ -146,14 +146,15 @@ fn test_generic_named_fields_struct() { test: "test".into(), test2: 2, } - .into_py(py); + .into_pyobject(py) + .unwrap(); let e = pye - .extract::>(py) + .extract::>() .expect("Failed to extract E from PyE"); assert_eq!(e.test, "test"); assert_eq!(e.test2, 2); - let e = pye.extract::>(py); + let e = pye.extract::>(); assert!(e.is_err()); }); } @@ -171,8 +172,9 @@ fn test_named_field_with_ext_fn() { test: "foo".into(), test2: 0, } - .into_py(py); - let c = pyc.extract::(py).expect("Failed to extract C from PyE"); + .into_pyobject(py) + .unwrap(); + let c = pyc.extract::().expect("Failed to extract C from PyE"); assert_eq!(c.test, "foo"); }); } @@ -215,12 +217,12 @@ pub struct TransparentTuple(String); #[test] fn test_transparent_tuple_struct() { Python::with_gil(|py| { - let tup: PyObject = 1.into_py(py); - let tup = tup.extract::(py); + let tup = 1i32.into_pyobject(py).unwrap(); + let tup = tup.extract::(); assert!(tup.is_err()); - let test: PyObject = "test".into_py(py); + let test = "test".into_pyobject(py).unwrap(); let tup = test - .extract::(py) + .extract::() .expect("Failed to extract TransparentTuple from PyTuple"); assert_eq!(tup.0, "test"); }); @@ -251,9 +253,10 @@ fn test_struct_nested_type_errors() { test2: 0, }, } - .into_py(py); + .into_pyobject(py) + .unwrap(); - let test = pybaz.extract::>(py); + let test = pybaz.extract::>(); assert!(test.is_err()); assert_eq!( extract_traceback(py,test.unwrap_err()), @@ -273,9 +276,10 @@ fn test_struct_nested_type_errors_with_generics() { test2: 0, }, } - .into_py(py); + .into_pyobject(py) + .unwrap(); - let test = pybaz.extract::>(py); + let test = pybaz.extract::>(); assert!(test.is_err()); assert_eq!( extract_traceback(py, test.unwrap_err()), @@ -288,8 +292,8 @@ fn test_struct_nested_type_errors_with_generics() { #[test] fn test_transparent_struct_error_message() { Python::with_gil(|py| { - let tup: PyObject = 1.into_py(py); - let tup = tup.extract::(py); + let tup = 1i32.into_pyobject(py).unwrap(); + let tup = tup.extract::(); assert!(tup.is_err()); assert_eq!( extract_traceback(py,tup.unwrap_err()), @@ -302,8 +306,8 @@ fn test_transparent_struct_error_message() { #[test] fn test_tuple_struct_error_message() { Python::with_gil(|py| { - let tup: PyObject = (1, "test").into_py(py); - let tup = tup.extract::(py); + let tup = (1, "test").into_pyobject(py).unwrap(); + let tup = tup.extract::(); assert!(tup.is_err()); assert_eq!( extract_traceback(py, tup.unwrap_err()), @@ -316,8 +320,8 @@ fn test_tuple_struct_error_message() { #[test] fn test_transparent_tuple_error_message() { Python::with_gil(|py| { - let tup: PyObject = 1.into_py(py); - let tup = tup.extract::(py); + let tup = 1i32.into_pyobject(py).unwrap(); + let tup = tup.extract::(); assert!(tup.is_err()); assert_eq!( extract_traceback(py, tup.unwrap_err()), @@ -362,7 +366,14 @@ pub struct PyBool { #[test] fn test_enum() { Python::with_gil(|py| { - let tup = PyTuple::new(py, &[1i32.into_py(py), "test".into_py(py)]).unwrap(); + let tup = PyTuple::new( + py, + &[ + 1i32.into_pyobject(py).unwrap().into_any(), + "test".into_pyobject(py).unwrap().into_any(), + ], + ) + .unwrap(); let f = tup .extract::>() .expect("Failed to extract Foo from tuple"); @@ -378,18 +389,19 @@ fn test_enum() { test: "foo".into(), test2: 0, } - .into_py(py); + .into_pyobject(py) + .unwrap(); let f = pye - .extract::>(py) + .extract::>() .expect("Failed to extract Foo from PyE"); match f { Foo::StructVar { test } => assert_eq!(test.to_string_lossy(), "foo"), _ => panic!("Expected extracting Foo::StructVar, got {:?}", f), } - let int: PyObject = 1.into_py(py); + let int = 1i32.into_pyobject(py).unwrap(); let f = int - .extract::>(py) + .extract::>() .expect("Failed to extract Foo from int"); match f { Foo::TransparentTuple(test) => assert_eq!(test, 1), @@ -404,9 +416,9 @@ fn test_enum() { _ => panic!("Expected extracting Foo::TransparentStructVar, got {:?}", f), } - let pybool = PyBool { bla: true }.into_py(py); + let pybool = PyBool { bla: true }.into_pyobject(py).unwrap(); let f = pybool - .extract::>(py) + .extract::>() .expect("Failed to extract Foo from PyBool"); match f { Foo::StructVarGetAttrArg { a } => assert!(a), diff --git a/tests/test_getter_setter.rs b/tests/test_getter_setter.rs index 144e3f2b7eb..064511772da 100644 --- a/tests/test_getter_setter.rs +++ b/tests/test_getter_setter.rs @@ -4,6 +4,7 @@ use std::cell::Cell; use pyo3::prelude::*; use pyo3::py_run; +use pyo3::types::PyString; use pyo3::types::{IntoPyDict, PyList}; #[path = "../src/tests/common.rs"] @@ -266,14 +267,14 @@ fn frozen_py_field_get() { #[pyclass(frozen)] struct FrozenPyField { #[pyo3(get)] - value: Py, + value: Py, } Python::with_gil(|py| { let inst = Py::new( py, FrozenPyField { - value: "value".into_py(py), + value: "value".into_pyobject(py).unwrap().unbind(), }, ) .unwrap(); diff --git a/tests/test_mapping.rs b/tests/test_mapping.rs index ecb944e983e..ba65c647222 100644 --- a/tests/test_mapping.rs +++ b/tests/test_mapping.rs @@ -61,11 +61,16 @@ impl Mapping { } #[pyo3(signature=(key, default=None))] - fn get(&self, py: Python<'_>, key: &str, default: Option) -> Option { - self.index - .get(key) - .map(|value| value.into_py(py)) - .or(default) + fn get( + &self, + py: Python<'_>, + key: &str, + default: Option, + ) -> PyResult> { + match self.index.get(key) { + Some(value) => Ok(Some(value.into_pyobject(py)?.into_any().unbind())), + None => Ok(default), + } } } diff --git a/tests/test_proto_methods.rs b/tests/test_proto_methods.rs index 11214ec0dc6..bd4847c46bf 100644 --- a/tests/test_proto_methods.rs +++ b/tests/test_proto_methods.rs @@ -22,7 +22,7 @@ struct ExampleClass { impl ExampleClass { fn __getattr__(&self, py: Python<'_>, attr: &str) -> PyResult { if attr == "special_custom_attr" { - Ok(self.custom_attr.into_py(py)) + Ok(self.custom_attr.into_pyobject(py)?.into_any().unbind()) } else { Err(PyAttributeError::new_err(attr.to_string())) } diff --git a/tests/test_pyfunction.rs b/tests/test_pyfunction.rs index 4896207b0ab..13ba5405ed3 100644 --- a/tests/test_pyfunction.rs +++ b/tests/test_pyfunction.rs @@ -435,22 +435,22 @@ fn test_closure() { _kwargs: Option<&Bound<'_, types::PyDict>>| -> PyResult<_> { Python::with_gil(|py| { - let res: Vec<_> = args + let res: PyResult> = args .iter() .map(|elem| { if let Ok(i) = elem.extract::() { - (i + 1).into_py(py) + Ok((i + 1).into_pyobject(py)?.into_any().unbind()) } else if let Ok(f) = elem.extract::() { - (2. * f).into_py(py) + Ok((2. * f).into_pyobject(py)?.into_any().unbind()) } else if let Ok(mut s) = elem.extract::() { s.push_str("-py"); - s.into_py(py) + Ok(s.into_pyobject(py)?.into_any().unbind()) } else { panic!("unexpected argument type for {:?}", elem) } }) .collect(); - Ok(res) + res }) }; let closure_py = diff --git a/tests/test_pyself.rs b/tests/test_pyself.rs index a4899131c41..c4f3e6d6fa6 100644 --- a/tests/test_pyself.rs +++ b/tests/test_pyself.rs @@ -93,7 +93,7 @@ fn reader() -> Reader { #[test] fn test_nested_iter() { Python::with_gil(|py| { - let reader: PyObject = reader().into_py(py); + let reader = reader().into_pyobject(py).unwrap(); py_assert!( py, reader, @@ -105,7 +105,7 @@ fn test_nested_iter() { #[test] fn test_clone_ref() { Python::with_gil(|py| { - let reader: PyObject = reader().into_py(py); + let reader = reader().into_pyobject(py).unwrap(); py_assert!(py, reader, "reader == reader.clone_ref()"); py_assert!(py, reader, "reader == reader.clone_ref_with_py()"); }); diff --git a/tests/test_sequence.rs b/tests/test_sequence.rs index 484e519fd21..e8f61e80e42 100644 --- a/tests/test_sequence.rs +++ b/tests/test_sequence.rs @@ -263,13 +263,14 @@ struct GenericList { #[test] fn test_generic_list_get() { Python::with_gil(|py| { - let list: PyObject = GenericList { + let list = GenericList { items: [1i32, 2, 3] .iter() .map(|i| i.into_pyobject(py).unwrap().into_any().unbind()) .collect(), } - .into_py(py); + .into_pyobject(py) + .unwrap(); py_assert!(py, list, "list.items == [1, 2, 3]"); }); @@ -286,7 +287,7 @@ fn test_generic_list_set() { .items .iter() .zip(&[1u32, 2, 3]) - .all(|(a, b)| a.bind(py).eq(b.into_py(py)).unwrap())); + .all(|(a, b)| a.bind(py).eq(b.into_pyobject(py).unwrap()).unwrap())); }); } From f74d37470c3c5b93852bff17da38dfd6e86608fd Mon Sep 17 00:00:00 2001 From: Kevin Matlock Date: Tue, 29 Oct 2024 14:20:28 -0700 Subject: [PATCH 350/495] Mapping returns `PyList` for `keys`, `values`, and `items` (#4661) * Mappingproxy (#1) Adds in the MappingProxy type. * Move over from `iter` to `try_iter`. * Added lifetime to `try_iter`, preventing need to clone when iterating. * Remove unneccessary borrow. * Add newsfragment * Newline to newsfragment. * Remove explicit lifetime, * Review comments (#2) * Addressing more comments * Remove extract_bound. * Remove extract methods. * Update mapping to return PyList instead of Sequence. * Update comments for list return type. * Add newfragment. * Reimpliment copy with PyMapping type. * Trigger Build --------- Co-authored-by: Kevin Matlock --- newsfragments/4661.changed.md | 1 + src/types/mapping.rs | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 10 deletions(-) create mode 100644 newsfragments/4661.changed.md diff --git a/newsfragments/4661.changed.md b/newsfragments/4661.changed.md new file mode 100644 index 00000000000..10b521fc605 --- /dev/null +++ b/newsfragments/4661.changed.md @@ -0,0 +1 @@ +`PyMapping`'s `keys`, `values` and `items` methods return `PyList` instead of `PySequence`. \ No newline at end of file diff --git a/src/types/mapping.rs b/src/types/mapping.rs index 6249b0eb97b..baf3a023ce9 100644 --- a/src/types/mapping.rs +++ b/src/types/mapping.rs @@ -6,7 +6,7 @@ use crate::py_result_ext::PyResultExt; use crate::sync::GILOnceCell; use crate::type_object::PyTypeInfo; use crate::types::any::PyAnyMethods; -use crate::types::{PyAny, PyDict, PySequence, PyType}; +use crate::types::{PyAny, PyDict, PyList, PyType}; use crate::{ffi, Py, PyTypeCheck, Python}; /// Represents a reference to a Python object supporting the mapping protocol. @@ -77,14 +77,14 @@ pub trait PyMappingMethods<'py>: crate::sealed::Sealed { where K: IntoPyObject<'py>; - /// Returns a sequence containing all keys in the mapping. - fn keys(&self) -> PyResult>; + /// Returns a list containing all keys in the mapping. + fn keys(&self) -> PyResult>; - /// Returns a sequence containing all values in the mapping. - fn values(&self) -> PyResult>; + /// Returns a list containing all values in the mapping. + fn values(&self) -> PyResult>; - /// Returns a sequence of tuples of all (key, value) pairs in the mapping. - fn items(&self) -> PyResult>; + /// Returns a list of all (key, value) pairs in the mapping. + fn items(&self) -> PyResult>; } impl<'py> PyMappingMethods<'py> for Bound<'py, PyMapping> { @@ -133,7 +133,7 @@ impl<'py> PyMappingMethods<'py> for Bound<'py, PyMapping> { } #[inline] - fn keys(&self) -> PyResult> { + fn keys(&self) -> PyResult> { unsafe { ffi::PyMapping_Keys(self.as_ptr()) .assume_owned_or_err(self.py()) @@ -142,7 +142,7 @@ impl<'py> PyMappingMethods<'py> for Bound<'py, PyMapping> { } #[inline] - fn values(&self) -> PyResult> { + fn values(&self) -> PyResult> { unsafe { ffi::PyMapping_Values(self.as_ptr()) .assume_owned_or_err(self.py()) @@ -151,7 +151,7 @@ impl<'py> PyMappingMethods<'py> for Bound<'py, PyMapping> { } #[inline] - fn items(&self) -> PyResult> { + fn items(&self) -> PyResult> { unsafe { ffi::PyMapping_Items(self.as_ptr()) .assume_owned_or_err(self.py()) From 4f537049993442736ddebe0acdef17537bbcca41 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Wed, 30 Oct 2024 13:08:03 -0600 Subject: [PATCH 351/495] Update docs for raw FFI use (#4665) * update top-level docs for pyo3-ffi * update listing of build config variables * don't depend on a file that doesn't exist * Move pyo3-ffi examples under pyo3-ffi repo * apply review comments * add changelog entry for the moved examples * fix URL anchor in pyo3-ffi docs * remove problematic links --- examples/README.md | 6 +- newsfragments/4665.changed.md | 2 + noxfile.py | 2 + pyo3-ffi/examples/README.md | 21 +++ .../examples}/sequential/.template/Cargo.toml | 0 .../sequential/.template/pre-script.rhai | 0 .../sequential/.template/pyproject.toml | 0 .../examples}/sequential/Cargo.toml | 2 +- .../examples}/sequential/MANIFEST.in | 0 .../examples}/sequential/README.md | 0 .../examples}/sequential/cargo-generate.toml | 0 .../examples}/sequential/noxfile.py | 0 .../examples}/sequential/pyproject.toml | 0 .../examples}/sequential/src/id.rs | 0 .../examples}/sequential/src/lib.rs | 0 .../examples}/sequential/src/module.rs | 0 .../examples}/sequential/tests/test.rs | 0 .../examples}/sequential/tests/test_.py | 0 .../examples}/string-sum/.template/Cargo.toml | 0 .../string-sum/.template/pre-script.rhai | 0 .../string-sum/.template/pyproject.toml | 0 .../examples}/string-sum/Cargo.toml | 2 +- .../examples}/string-sum/MANIFEST.in | 0 .../examples}/string-sum/README.md | 0 .../examples}/string-sum/cargo-generate.toml | 0 .../examples}/string-sum/noxfile.py | 0 .../examples}/string-sum/pyproject.toml | 0 .../examples}/string-sum/src/lib.rs | 0 .../examples}/string-sum/tests/test_.py | 0 pyo3-ffi/src/lib.rs | 123 ++++-------------- src/lib.rs | 23 +++- 31 files changed, 76 insertions(+), 105 deletions(-) create mode 100644 newsfragments/4665.changed.md create mode 100644 pyo3-ffi/examples/README.md rename {examples => pyo3-ffi/examples}/sequential/.template/Cargo.toml (100%) rename {examples => pyo3-ffi/examples}/sequential/.template/pre-script.rhai (100%) rename {examples => pyo3-ffi/examples}/sequential/.template/pyproject.toml (100%) rename {examples => pyo3-ffi/examples}/sequential/Cargo.toml (67%) rename {examples => pyo3-ffi/examples}/sequential/MANIFEST.in (100%) rename {examples => pyo3-ffi/examples}/sequential/README.md (100%) rename {examples => pyo3-ffi/examples}/sequential/cargo-generate.toml (100%) rename {examples => pyo3-ffi/examples}/sequential/noxfile.py (100%) rename {examples => pyo3-ffi/examples}/sequential/pyproject.toml (100%) rename {examples => pyo3-ffi/examples}/sequential/src/id.rs (100%) rename {examples => pyo3-ffi/examples}/sequential/src/lib.rs (100%) rename {examples => pyo3-ffi/examples}/sequential/src/module.rs (100%) rename {examples => pyo3-ffi/examples}/sequential/tests/test.rs (100%) rename {examples => pyo3-ffi/examples}/sequential/tests/test_.py (100%) rename {examples => pyo3-ffi/examples}/string-sum/.template/Cargo.toml (100%) rename {examples => pyo3-ffi/examples}/string-sum/.template/pre-script.rhai (100%) rename {examples => pyo3-ffi/examples}/string-sum/.template/pyproject.toml (100%) rename {examples => pyo3-ffi/examples}/string-sum/Cargo.toml (66%) rename {examples => pyo3-ffi/examples}/string-sum/MANIFEST.in (100%) rename {examples => pyo3-ffi/examples}/string-sum/README.md (100%) rename {examples => pyo3-ffi/examples}/string-sum/cargo-generate.toml (100%) rename {examples => pyo3-ffi/examples}/string-sum/noxfile.py (100%) rename {examples => pyo3-ffi/examples}/string-sum/pyproject.toml (100%) rename {examples => pyo3-ffi/examples}/string-sum/src/lib.rs (100%) rename {examples => pyo3-ffi/examples}/string-sum/tests/test_.py (100%) diff --git a/examples/README.md b/examples/README.md index baaa57b650d..3c7cc301399 100644 --- a/examples/README.md +++ b/examples/README.md @@ -9,9 +9,11 @@ Below is a brief description of each of these: | `decorator` | A project showcasing the example from the [Emulating callable objects](https://pyo3.rs/latest/class/call.html) chapter of the guide. | | `maturin-starter` | A template project which is configured to use [`maturin`](https://github.com/PyO3/maturin) for development. | | `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 | + +Note that there are also other examples in the `pyo3-ffi/examples` +directory that illustrate how to create rust extensions using raw FFI calls into +the CPython C API instead of using PyO3's abstractions. ## Creating new projects from these examples diff --git a/newsfragments/4665.changed.md b/newsfragments/4665.changed.md new file mode 100644 index 00000000000..2ebbf0c86b4 --- /dev/null +++ b/newsfragments/4665.changed.md @@ -0,0 +1,2 @@ +* The `sequential` and `string-sum` examples have moved into a new `examples` + directory in the `pyo3-ffi` crate. diff --git a/noxfile.py b/noxfile.py index ce59162f120..32176240f59 100644 --- a/noxfile.py +++ b/noxfile.py @@ -65,6 +65,8 @@ def test_py(session: nox.Session) -> None: _run(session, "nox", "-f", "pytests/noxfile.py", external=True) for example in glob("examples/*/noxfile.py"): _run(session, "nox", "-f", example, external=True) + for example in glob("pyo3-ffi/examples/*/noxfile.py"): + _run(session, "nox", "-f", example, external=True) @nox.session(venv_backend="none") diff --git a/pyo3-ffi/examples/README.md b/pyo3-ffi/examples/README.md new file mode 100644 index 00000000000..f02ae4ba6b4 --- /dev/null +++ b/pyo3-ffi/examples/README.md @@ -0,0 +1,21 @@ +# `pyo3-ffi` Examples + +These example crates are a collection of toy extension modules built with +`pyo3-ffi`. They are all tested using `nox` in PyO3's CI. + +Below is a brief description of each of these: + +| Example | Description | +| `word-count` | Illustrates how to use pyo3-ffi to write a static rust extension | +| `sequential` | Illustrates how to use pyo3-ffi to write subinterpreter-safe modules using multi-phase module initialization | + +## Creating new projects from these examples + +To copy an example, use [`cargo-generate`](https://crates.io/crates/cargo-generate). Follow the commands below, replacing `` with the example to start from: + +```bash +$ cargo install cargo-generate +$ cargo generate --git https://github.com/PyO3/pyo3 examples/ +``` + +(`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/.template/Cargo.toml b/pyo3-ffi/examples/sequential/.template/Cargo.toml similarity index 100% rename from examples/sequential/.template/Cargo.toml rename to pyo3-ffi/examples/sequential/.template/Cargo.toml diff --git a/examples/sequential/.template/pre-script.rhai b/pyo3-ffi/examples/sequential/.template/pre-script.rhai similarity index 100% rename from examples/sequential/.template/pre-script.rhai rename to pyo3-ffi/examples/sequential/.template/pre-script.rhai diff --git a/examples/sequential/.template/pyproject.toml b/pyo3-ffi/examples/sequential/.template/pyproject.toml similarity index 100% rename from examples/sequential/.template/pyproject.toml rename to pyo3-ffi/examples/sequential/.template/pyproject.toml diff --git a/examples/sequential/Cargo.toml b/pyo3-ffi/examples/sequential/Cargo.toml similarity index 67% rename from examples/sequential/Cargo.toml rename to pyo3-ffi/examples/sequential/Cargo.toml index 4500c69b597..3348595b4e9 100644 --- a/examples/sequential/Cargo.toml +++ b/pyo3-ffi/examples/sequential/Cargo.toml @@ -8,6 +8,6 @@ name = "sequential" crate-type = ["cdylib", "lib"] [dependencies] -pyo3-ffi = { path = "../../pyo3-ffi", features = ["extension-module"] } +pyo3-ffi = { path = "../../", features = ["extension-module"] } [workspace] diff --git a/examples/sequential/MANIFEST.in b/pyo3-ffi/examples/sequential/MANIFEST.in similarity index 100% rename from examples/sequential/MANIFEST.in rename to pyo3-ffi/examples/sequential/MANIFEST.in diff --git a/examples/sequential/README.md b/pyo3-ffi/examples/sequential/README.md similarity index 100% rename from examples/sequential/README.md rename to pyo3-ffi/examples/sequential/README.md diff --git a/examples/sequential/cargo-generate.toml b/pyo3-ffi/examples/sequential/cargo-generate.toml similarity index 100% rename from examples/sequential/cargo-generate.toml rename to pyo3-ffi/examples/sequential/cargo-generate.toml diff --git a/examples/sequential/noxfile.py b/pyo3-ffi/examples/sequential/noxfile.py similarity index 100% rename from examples/sequential/noxfile.py rename to pyo3-ffi/examples/sequential/noxfile.py diff --git a/examples/sequential/pyproject.toml b/pyo3-ffi/examples/sequential/pyproject.toml similarity index 100% rename from examples/sequential/pyproject.toml rename to pyo3-ffi/examples/sequential/pyproject.toml diff --git a/examples/sequential/src/id.rs b/pyo3-ffi/examples/sequential/src/id.rs similarity index 100% rename from examples/sequential/src/id.rs rename to pyo3-ffi/examples/sequential/src/id.rs diff --git a/examples/sequential/src/lib.rs b/pyo3-ffi/examples/sequential/src/lib.rs similarity index 100% rename from examples/sequential/src/lib.rs rename to pyo3-ffi/examples/sequential/src/lib.rs diff --git a/examples/sequential/src/module.rs b/pyo3-ffi/examples/sequential/src/module.rs similarity index 100% rename from examples/sequential/src/module.rs rename to pyo3-ffi/examples/sequential/src/module.rs diff --git a/examples/sequential/tests/test.rs b/pyo3-ffi/examples/sequential/tests/test.rs similarity index 100% rename from examples/sequential/tests/test.rs rename to pyo3-ffi/examples/sequential/tests/test.rs diff --git a/examples/sequential/tests/test_.py b/pyo3-ffi/examples/sequential/tests/test_.py similarity index 100% rename from examples/sequential/tests/test_.py rename to pyo3-ffi/examples/sequential/tests/test_.py diff --git a/examples/string-sum/.template/Cargo.toml b/pyo3-ffi/examples/string-sum/.template/Cargo.toml similarity index 100% rename from examples/string-sum/.template/Cargo.toml rename to pyo3-ffi/examples/string-sum/.template/Cargo.toml diff --git a/examples/string-sum/.template/pre-script.rhai b/pyo3-ffi/examples/string-sum/.template/pre-script.rhai similarity index 100% rename from examples/string-sum/.template/pre-script.rhai rename to pyo3-ffi/examples/string-sum/.template/pre-script.rhai diff --git a/examples/string-sum/.template/pyproject.toml b/pyo3-ffi/examples/string-sum/.template/pyproject.toml similarity index 100% rename from examples/string-sum/.template/pyproject.toml rename to pyo3-ffi/examples/string-sum/.template/pyproject.toml diff --git a/examples/string-sum/Cargo.toml b/pyo3-ffi/examples/string-sum/Cargo.toml similarity index 66% rename from examples/string-sum/Cargo.toml rename to pyo3-ffi/examples/string-sum/Cargo.toml index 4a48b221c60..6fb72141cdc 100644 --- a/examples/string-sum/Cargo.toml +++ b/pyo3-ffi/examples/string-sum/Cargo.toml @@ -8,6 +8,6 @@ name = "string_sum" crate-type = ["cdylib"] [dependencies] -pyo3-ffi = { path = "../../pyo3-ffi", features = ["extension-module"] } +pyo3-ffi = { path = "../../", features = ["extension-module"] } [workspace] diff --git a/examples/string-sum/MANIFEST.in b/pyo3-ffi/examples/string-sum/MANIFEST.in similarity index 100% rename from examples/string-sum/MANIFEST.in rename to pyo3-ffi/examples/string-sum/MANIFEST.in diff --git a/examples/string-sum/README.md b/pyo3-ffi/examples/string-sum/README.md similarity index 100% rename from examples/string-sum/README.md rename to pyo3-ffi/examples/string-sum/README.md diff --git a/examples/string-sum/cargo-generate.toml b/pyo3-ffi/examples/string-sum/cargo-generate.toml similarity index 100% rename from examples/string-sum/cargo-generate.toml rename to pyo3-ffi/examples/string-sum/cargo-generate.toml diff --git a/examples/string-sum/noxfile.py b/pyo3-ffi/examples/string-sum/noxfile.py similarity index 100% rename from examples/string-sum/noxfile.py rename to pyo3-ffi/examples/string-sum/noxfile.py diff --git a/examples/string-sum/pyproject.toml b/pyo3-ffi/examples/string-sum/pyproject.toml similarity index 100% rename from examples/string-sum/pyproject.toml rename to pyo3-ffi/examples/string-sum/pyproject.toml diff --git a/examples/string-sum/src/lib.rs b/pyo3-ffi/examples/string-sum/src/lib.rs similarity index 100% rename from examples/string-sum/src/lib.rs rename to pyo3-ffi/examples/string-sum/src/lib.rs diff --git a/examples/string-sum/tests/test_.py b/pyo3-ffi/examples/string-sum/tests/test_.py similarity index 100% rename from examples/string-sum/tests/test_.py rename to pyo3-ffi/examples/string-sum/tests/test_.py diff --git a/pyo3-ffi/src/lib.rs b/pyo3-ffi/src/lib.rs index 293c5171eb5..23a5e0000b1 100644 --- a/pyo3-ffi/src/lib.rs +++ b/pyo3-ffi/src/lib.rs @@ -64,8 +64,8 @@ //! your `Cargo.toml`: //! //! ```toml -//! [build-dependency] -//! pyo3-build-config = "VER" +//! [build-dependencies] +#![doc = concat!("pyo3-build-config =\"", env!("CARGO_PKG_VERSION"), "\"")] //! ``` //! //! And then either create a new `build.rs` file in the project root or modify @@ -108,104 +108,31 @@ //! [dependencies.pyo3-ffi] #![doc = concat!("version = \"", env!("CARGO_PKG_VERSION"), "\"")] //! features = ["extension-module"] +//! +//! [build-dependencies] +//! # This is only necessary if you need to configure your build based on +//! # the Python version or the compile-time configuration for the interpreter. +#![doc = concat!("pyo3_build_config = \"", env!("CARGO_PKG_VERSION"), "\"")] //! ``` //! -//! **`src/lib.rs`** -//! ```rust -//! use std::os::raw::c_char; -//! use std::ptr; -//! -//! use pyo3_ffi::*; -//! -//! static mut MODULE_DEF: PyModuleDef = PyModuleDef { -//! m_base: PyModuleDef_HEAD_INIT, -//! m_name: c_str!("string_sum").as_ptr(), -//! m_doc: c_str!("A Python module written in Rust.").as_ptr(), -//! m_size: 0, -//! m_methods: unsafe { METHODS.as_mut_ptr().cast() }, -//! m_slots: std::ptr::null_mut(), -//! m_traverse: None, -//! m_clear: None, -//! m_free: None, -//! }; -//! -//! static mut METHODS: [PyMethodDef; 2] = [ -//! PyMethodDef { -//! ml_name: c_str!("sum_as_string").as_ptr(), -//! ml_meth: PyMethodDefPointer { -//! PyCFunctionFast: sum_as_string, -//! }, -//! ml_flags: METH_FASTCALL, -//! ml_doc: c_str!("returns the sum of two integers as a string").as_ptr(), -//! }, -//! // A zeroed PyMethodDef to mark the end of the array. -//! PyMethodDef::zeroed() -//! ]; -//! -//! // The module initialization function, which must be named `PyInit_`. -//! #[allow(non_snake_case)] -//! #[no_mangle] -//! pub unsafe extern "C" fn PyInit_string_sum() -> *mut PyObject { -//! PyModule_Create(ptr::addr_of_mut!(MODULE_DEF)) -//! } +//! If you need to use conditional compilation based on Python version or how +//! Python was compiled, you need to add `pyo3-build-config` as a +//! `build-dependency` in your `Cargo.toml` as in the example above and either +//! create a new `build.rs` file or modify an existing one so that +//! `pyo3_build_config::use_pyo3_cfgs()` gets called at build time: //! -//! pub unsafe extern "C" fn sum_as_string( -//! _self: *mut PyObject, -//! args: *mut *mut PyObject, -//! nargs: Py_ssize_t, -//! ) -> *mut PyObject { -//! if nargs != 2 { -//! PyErr_SetString( -//! PyExc_TypeError, -//! c_str!("sum_as_string() expected 2 positional arguments").as_ptr(), -//! ); -//! return std::ptr::null_mut(); -//! } -//! -//! let arg1 = *args; -//! if PyLong_Check(arg1) == 0 { -//! PyErr_SetString( -//! PyExc_TypeError, -//! c_str!("sum_as_string() expected an int for positional argument 1").as_ptr(), -//! ); -//! return std::ptr::null_mut(); -//! } -//! -//! let arg1 = PyLong_AsLong(arg1); -//! if !PyErr_Occurred().is_null() { -//! return ptr::null_mut(); -//! } -//! -//! let arg2 = *args.add(1); -//! if PyLong_Check(arg2) == 0 { -//! PyErr_SetString( -//! PyExc_TypeError, -//! c_str!("sum_as_string() expected an int for positional argument 2").as_ptr(), -//! ); -//! return std::ptr::null_mut(); -//! } -//! -//! let arg2 = PyLong_AsLong(arg2); -//! if !PyErr_Occurred().is_null() { -//! return ptr::null_mut(); -//! } -//! -//! match arg1.checked_add(arg2) { -//! Some(sum) => { -//! let string = sum.to_string(); -//! PyUnicode_FromStringAndSize(string.as_ptr().cast::(), string.len() as isize) -//! } -//! None => { -//! PyErr_SetString( -//! PyExc_OverflowError, -//! c_str!("arguments too large to add").as_ptr(), -//! ); -//! std::ptr::null_mut() -//! } -//! } +//! **`build.rs`** +//! ```rust,ignore +//! fn main() { +//! pyo3_build_config::use_pyo3_cfgs() //! } //! ``` //! +//! **`src/lib.rs`** +//! ```rust +#![doc = include_str!("../examples/string-sum/src/lib.rs")] +//! ``` +//! //! With those two files in place, now `maturin` needs to be installed. This can be done using //! Python's package manager `pip`. First, load up a new Python `virtualenv`, and install `maturin` //! into it: @@ -230,6 +157,12 @@ //! [manually][manual_builds]. Both offer more flexibility than `maturin` but require further //! configuration. //! +//! This example stores the module definition statically and uses the `PyModule_Create` function +//! in the CPython C API to register the module. This is the "old" style for registering modules +//! and has the limitation that it cannot support subinterpreters. You can also create a module +//! using the new multi-phase initialization API that does support subinterpreters. See the +//! `sequential` project located in the `examples` directory at the root of the `pyo3-ffi` crate +//! for a worked example of how to this using `pyo3-ffi`. //! //! # Using Python from Rust //! @@ -255,7 +188,7 @@ #![doc = concat!("[manual_builds]: https://pyo3.rs/v", env!("CARGO_PKG_VERSION"), "/building-and-distribution.html#manual-builds \"Manual builds - Building and Distribution - PyO3 user guide\"")] //! [setuptools-rust]: https://github.com/PyO3/setuptools-rust "Setuptools plugin for Rust extensions" //! [PEP 384]: https://www.python.org/dev/peps/pep-0384 "PEP 384 -- Defining a Stable ABI" -#![doc = concat!("[Features chapter of the guide]: https://pyo3.rs/v", env!("CARGO_PKG_VERSION"), "/features.html#features-reference \"Features Reference - PyO3 user guide\"")] +#![doc = concat!("[Features chapter of the guide]: https://pyo3.rs/v", env!("CARGO_PKG_VERSION"), "/features.html#features-reference \"Features eference - PyO3 user guide\"")] #![allow( missing_docs, non_camel_case_types, diff --git a/src/lib.rs b/src/lib.rs index b4fcf918fae..25c88143609 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -123,14 +123,25 @@ //! - `nightly`: Uses `#![feature(auto_traits, negative_impls)]` to define [`Ungil`] as an auto trait. // //! ## `rustc` environment flags -//! -//! PyO3 uses `rustc`'s `--cfg` flags to enable or disable code used for different Python versions. -//! If you want to do this for your own crate, you can do so with the [`pyo3-build-config`] crate. -//! -//! - `Py_3_7`, `Py_3_8`, `Py_3_9`, `Py_3_10`: Marks code that is only enabled when -//! compiling for a given minimum Python version. +//! - `Py_3_7`, `Py_3_8`, `Py_3_9`, `Py_3_10`, `Py_3_11`, `Py_3_12`, `Py_3_13`: Marks code that is +//! only enabled when compiling for a given minimum Python version. //! - `Py_LIMITED_API`: Marks code enabled when the `abi3` feature flag is enabled. +//! - `Py_GIL_DISABLED`: Marks code that runs only in the free-threaded build of CPython. //! - `PyPy` - Marks code enabled when compiling for PyPy. +//! - `GraalPy` - Marks code enabled when compiling for GraalPy. +//! +//! Additionally, you can query for the values `Py_DEBUG`, `Py_REF_DEBUG`, +//! `Py_TRACE_REFS`, and `COUNT_ALLOCS` from `py_sys_config` to query for the +//! corresponding C build-time defines. For example, to conditionally define +//! debug code using `Py_DEBUG`, you could do: +//! +//! ```rust,ignore +//! #[cfg(py_sys_config = "Py_DEBUG")] +//! println!("only runs if python was compiled with Py_DEBUG") +//! ``` +//! To use these attributes, add [`pyo3-build-config`] as a build dependency in +//! your `Cargo.toml` and call `pyo3_build_config::use_pyo3_cfgs()` in a +//! `build.rs` file. //! //! # Minimum supported Rust and Python versions //! From 5464f1656430d4b8677f1305e0da0177d7841273 Mon Sep 17 00:00:00 2001 From: Dmitry Dygalo Date: Wed, 30 Oct 2024 20:13:25 +0100 Subject: [PATCH 352/495] docs: Update `jsonschema` link & description (#4670) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1094a489999..a131a823f0e 100644 --- a/README.md +++ b/README.md @@ -207,7 +207,7 @@ about this topic. - [hyperjson](https://github.com/mre/hyperjson) _A hyper-fast Python module for reading/writing JSON data using Rust's serde-json._ - [inline-python](https://github.com/fusion-engineering/inline-python) _Inline Python code directly in your Rust code._ - [johnnycanencrypt](https://github.com/kushaldas/johnnycanencrypt) OpenPGP library with Yubikey support. -- [jsonschema-rs](https://github.com/Stranger6667/jsonschema-rs/tree/master/bindings/python) _Fast JSON Schema validation library._ +- [jsonschema](https://github.com/Stranger6667/jsonschema/tree/master/crates/jsonschema-py) _A high-performance JSON Schema validator for Python._ - [mocpy](https://github.com/cds-astro/mocpy) _Astronomical Python library offering data structures for describing any arbitrary coverage regions on the unit sphere._ - [opendal](https://github.com/apache/opendal/tree/main/bindings/python) _A data access layer that allows users to easily and efficiently retrieve data from various storage services in a unified way._ - [orjson](https://github.com/ijl/orjson) _Fast Python JSON library._ From c0e6232f88c24078e3b4483de17b3973a3d72fe6 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers <7943856+bschoenmaeckers@users.noreply.github.com> Date: Fri, 1 Nov 2024 14:21:39 +0100 Subject: [PATCH 353/495] Add `PyList_Extend` & `PyList_Clear` to pyo3-ffi (#4667) --- newsfragments/4667.added.md | 1 + pyo3-ffi/src/compat/py_3_13.rs | 21 +++++++++++++++++++++ pyo3-ffi/src/listobject.rs | 4 ++++ 3 files changed, 26 insertions(+) create mode 100644 newsfragments/4667.added.md diff --git a/newsfragments/4667.added.md b/newsfragments/4667.added.md new file mode 100644 index 00000000000..fc2a914607e --- /dev/null +++ b/newsfragments/4667.added.md @@ -0,0 +1 @@ +Add `PyList_Extend` & `PyList_Clear` to pyo3-ffi diff --git a/pyo3-ffi/src/compat/py_3_13.rs b/pyo3-ffi/src/compat/py_3_13.rs index 9f44ced6f3f..59289cb76ae 100644 --- a/pyo3-ffi/src/compat/py_3_13.rs +++ b/pyo3-ffi/src/compat/py_3_13.rs @@ -83,3 +83,24 @@ compat_function!( 1 } ); + +compat_function!( + originally_defined_for(Py_3_13); + + #[inline] + pub unsafe fn PyList_Extend( + list: *mut crate::PyObject, + iterable: *mut crate::PyObject, + ) -> std::os::raw::c_int { + crate::PyList_SetSlice(list, crate::PY_SSIZE_T_MAX, crate::PY_SSIZE_T_MAX, iterable) + } +); + +compat_function!( + originally_defined_for(Py_3_13); + + #[inline] + pub unsafe fn PyList_Clear(list: *mut crate::PyObject) -> std::os::raw::c_int { + crate::PyList_SetSlice(list, 0, crate::PY_SSIZE_T_MAX, std::ptr::null_mut()) + } +); diff --git a/pyo3-ffi/src/listobject.rs b/pyo3-ffi/src/listobject.rs index 9d8b7ed6a58..881a8a8707b 100644 --- a/pyo3-ffi/src/listobject.rs +++ b/pyo3-ffi/src/listobject.rs @@ -50,6 +50,10 @@ extern "C" { arg3: Py_ssize_t, arg4: *mut PyObject, ) -> c_int; + #[cfg(Py_3_13)] + pub fn PyList_Extend(list: *mut PyObject, iterable: *mut PyObject) -> c_int; + #[cfg(Py_3_13)] + pub fn PyList_Clear(list: *mut PyObject) -> c_int; #[cfg_attr(PyPy, link_name = "PyPyList_Sort")] pub fn PyList_Sort(arg1: *mut PyObject) -> c_int; #[cfg_attr(PyPy, link_name = "PyPyList_Reverse")] From fdb29cc31fc8dc0327738e28c8363253c8b6811f Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Sat, 2 Nov 2024 11:06:54 +0100 Subject: [PATCH 354/495] fix unintentional `unsafe_op_in_unsafe_fn` trigger (#4674) * fix unintentional `unsafe_op_in_unsafe_fn` trigger * add newsfragment --- newsfragments/4674.fixed.md | 1 + pyo3-macros-backend/src/method.rs | 30 ++++++++++++---------- pyo3-macros-backend/src/module.rs | 2 +- pyo3-macros-backend/src/params.rs | 5 ++-- pyo3-macros-backend/src/pymethod.rs | 39 +++++++++++++++-------------- tests/ui/forbid_unsafe.rs | 1 + 6 files changed, 43 insertions(+), 35 deletions(-) create mode 100644 newsfragments/4674.fixed.md diff --git a/newsfragments/4674.fixed.md b/newsfragments/4674.fixed.md new file mode 100644 index 00000000000..6245a6f734a --- /dev/null +++ b/newsfragments/4674.fixed.md @@ -0,0 +1 @@ +Fixes unintentional `unsafe_op_in_unsafe_fn` trigger by adjusting macro hygiene. \ No newline at end of file diff --git a/pyo3-macros-backend/src/method.rs b/pyo3-macros-backend/src/method.rs index 019fb5e644b..f99e64562b7 100644 --- a/pyo3-macros-backend/src/method.rs +++ b/pyo3-macros-backend/src/method.rs @@ -270,9 +270,9 @@ impl FnType { ::std::convert::Into::into( #pyo3_path::impl_::pymethods::BoundRef::ref_from_ptr(#py, &*(&#slf as *const _ as *const *mut _)) .downcast_unchecked::<#pyo3_path::types::PyType>() - ), + ) }; - Some(ret) + Some(quote! { unsafe { #ret }, }) } FnType::FnModule(span) => { let py = syn::Ident::new("py", Span::call_site()); @@ -283,9 +283,9 @@ impl FnType { ::std::convert::Into::into( #pyo3_path::impl_::pymethods::BoundRef::ref_from_ptr(#py, &*(&#slf as *const _ as *const *mut _)) .downcast_unchecked::<#pyo3_path::types::PyModule>() - ), + ) }; - Some(ret) + Some(quote! { unsafe { #ret }, }) } FnType::FnNew | FnType::FnStatic | FnType::ClassAttribute => None, } @@ -332,6 +332,8 @@ impl SelfType { let py = syn::Ident::new("py", Span::call_site()); let slf = syn::Ident::new("_slf", Span::call_site()); let Ctx { pyo3_path, .. } = ctx; + let bound_ref = + quote! { unsafe { #pyo3_path::impl_::pymethods::BoundRef::ref_from_ptr(#py, &#slf) } }; match self { SelfType::Receiver { span, mutable } => { let method = if *mutable { @@ -344,7 +346,7 @@ impl SelfType { error_mode.handle_error( quote_spanned! { *span => #pyo3_path::impl_::extract_argument::#method::<#cls>( - #pyo3_path::impl_::pymethods::BoundRef::ref_from_ptr(#py, &#slf).0, + #bound_ref.0, &mut #holder, ) }, @@ -355,7 +357,7 @@ impl SelfType { let pyo3_path = pyo3_path.to_tokens_spanned(*span); error_mode.handle_error( quote_spanned! { *span => - #pyo3_path::impl_::pymethods::BoundRef::ref_from_ptr(#py, &#slf).downcast::<#cls>() + #bound_ref.downcast::<#cls>() .map_err(::std::convert::Into::<#pyo3_path::PyErr>::into) .and_then( #[allow(unknown_lints, clippy::unnecessary_fallible_conversions)] // In case slf is Py (unknown_lints can be removed when MSRV is 1.75+) @@ -665,14 +667,14 @@ impl<'a> FnSpec<'a> { FnType::Fn(SelfType::Receiver { mutable: false, .. }) => { quote! {{ #(let #arg_names = #args;)* - let __guard = #pyo3_path::impl_::coroutine::RefGuard::<#cls>::new(&#pyo3_path::impl_::pymethods::BoundRef::ref_from_ptr(py, &_slf))?; + let __guard = unsafe { #pyo3_path::impl_::coroutine::RefGuard::<#cls>::new(&#pyo3_path::impl_::pymethods::BoundRef::ref_from_ptr(py, &_slf))? }; async move { function(&__guard, #(#arg_names),*).await } }} } FnType::Fn(SelfType::Receiver { mutable: true, .. }) => { quote! {{ #(let #arg_names = #args;)* - let mut __guard = #pyo3_path::impl_::coroutine::RefMutGuard::<#cls>::new(&#pyo3_path::impl_::pymethods::BoundRef::ref_from_ptr(py, &_slf))?; + let mut __guard = unsafe { #pyo3_path::impl_::coroutine::RefMutGuard::<#cls>::new(&#pyo3_path::impl_::pymethods::BoundRef::ref_from_ptr(py, &_slf))? }; async move { function(&mut __guard, #(#arg_names),*).await } }} } @@ -862,11 +864,13 @@ impl<'a> FnSpec<'a> { _args: *mut #pyo3_path::ffi::PyObject, ) -> *mut #pyo3_path::ffi::PyObject { - #pyo3_path::impl_::trampoline::noargs( - _slf, - _args, - #wrapper - ) + unsafe { + #pyo3_path::impl_::trampoline::noargs( + _slf, + _args, + #wrapper + ) + } } trampoline }, diff --git a/pyo3-macros-backend/src/module.rs b/pyo3-macros-backend/src/module.rs index 7d2c72dbdfb..5aaf7740461 100644 --- a/pyo3-macros-backend/src/module.rs +++ b/pyo3-macros-backend/src/module.rs @@ -449,7 +449,7 @@ fn module_initialization( #[doc(hidden)] #[export_name = #pyinit_symbol] pub unsafe extern "C" fn __pyo3_init() -> *mut #pyo3_path::ffi::PyObject { - #pyo3_path::impl_::trampoline::module_init(|py| _PYO3_DEF.make_module(py)) + unsafe { #pyo3_path::impl_::trampoline::module_init(|py| _PYO3_DEF.make_module(py)) } } }); } diff --git a/pyo3-macros-backend/src/params.rs b/pyo3-macros-backend/src/params.rs index ccf725d3760..67054458c98 100644 --- a/pyo3-macros-backend/src/params.rs +++ b/pyo3-macros-backend/src/params.rs @@ -79,7 +79,7 @@ pub fn impl_arg_params( .collect(); return ( quote! { - let _args = #pyo3_path::impl_::pymethods::BoundRef::ref_from_ptr(py, &_args); + let _args = unsafe { #pyo3_path::impl_::pymethods::BoundRef::ref_from_ptr(py, &_args) }; let _kwargs = #pyo3_path::impl_::pymethods::BoundRef::ref_from_ptr_or_opt(py, &_kwargs); #from_py_with }, @@ -301,9 +301,10 @@ pub(crate) fn impl_regular_arg_param( } } else { let holder = holders.push_holder(arg.name.span()); + let unwrap = quote! {unsafe { #pyo3_path::impl_::extract_argument::unwrap_required_argument(#arg_value) }}; quote_arg_span! { #pyo3_path::impl_::extract_argument::extract_argument( - #pyo3_path::impl_::extract_argument::unwrap_required_argument(#arg_value), + #unwrap, &mut #holder, #name_str )? diff --git a/pyo3-macros-backend/src/pymethod.rs b/pyo3-macros-backend/src/pymethod.rs index d825609cd77..1254a8d510b 100644 --- a/pyo3-macros-backend/src/pymethod.rs +++ b/pyo3-macros-backend/src/pymethod.rs @@ -1182,25 +1182,26 @@ fn extract_object( let Ctx { pyo3_path, .. } = ctx; let name = arg.name().unraw().to_string(); - let extract = - if let Some(from_py_with) = arg.from_py_with().map(|from_py_with| &from_py_with.value) { - quote! { - #pyo3_path::impl_::extract_argument::from_py_with( - #pyo3_path::impl_::pymethods::BoundRef::ref_from_ptr(py, &#source_ptr).0, - #name, - #from_py_with as fn(_) -> _, - ) - } - } else { - let holder = holders.push_holder(Span::call_site()); - quote! { - #pyo3_path::impl_::extract_argument::extract_argument( - #pyo3_path::impl_::pymethods::BoundRef::ref_from_ptr(py, &#source_ptr).0, - &mut #holder, - #name - ) - } - }; + let extract = if let Some(from_py_with) = + arg.from_py_with().map(|from_py_with| &from_py_with.value) + { + quote! { + #pyo3_path::impl_::extract_argument::from_py_with( + unsafe { #pyo3_path::impl_::pymethods::BoundRef::ref_from_ptr(py, &#source_ptr).0 }, + #name, + #from_py_with as fn(_) -> _, + ) + } + } else { + let holder = holders.push_holder(Span::call_site()); + quote! { + #pyo3_path::impl_::extract_argument::extract_argument( + unsafe { #pyo3_path::impl_::pymethods::BoundRef::ref_from_ptr(py, &#source_ptr).0 }, + &mut #holder, + #name + ) + } + }; let extracted = extract_error_mode.handle_error(extract, ctx); quote!(#extracted) diff --git a/tests/ui/forbid_unsafe.rs b/tests/ui/forbid_unsafe.rs index 9b62886b650..660f5fa36c0 100644 --- a/tests/ui/forbid_unsafe.rs +++ b/tests/ui/forbid_unsafe.rs @@ -1,4 +1,5 @@ #![forbid(unsafe_code)] +#![forbid(unsafe_op_in_unsafe_fn)] use pyo3::*; From 55c95438e5d3d70222f43e6ae99c352fdc5b98cd Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Sat, 2 Nov 2024 14:44:50 +0000 Subject: [PATCH 355/495] add `sync::OnceExt` and `sync::OnceLockExt` traits (#4676) * add `sync::OnceExt` trait * newsfragment * refactor to use RAII guard * Add docs for single-initialization * mark init logic with #[cold] * attempt to include OnceLockExt as well * Add OnceLockExt * ignore clippy MSRV lint * simplify * fix wasm tests --------- Co-authored-by: Nathan Goldbaum --- guide/src/faq.md | 4 +- guide/src/free-threading.md | 54 +++++++++ guide/src/migration.md | 7 +- newsfragments/4676.added.md | 1 + pyo3-build-config/src/lib.rs | 5 + src/sealed.rs | 2 + src/sync.rs | 194 ++++++++++++++++++++++++++++++- tests/test_declarative_module.rs | 18 ++- 8 files changed, 278 insertions(+), 7 deletions(-) create mode 100644 newsfragments/4676.added.md diff --git a/guide/src/faq.md b/guide/src/faq.md index 5752e14adbd..83089cf395e 100644 --- a/guide/src/faq.md +++ b/guide/src/faq.md @@ -13,9 +13,11 @@ Sorry that you're having trouble using PyO3. If you can't find the answer to you 5. Thread A is blocked, because it waits to re-acquire the GIL which thread B still holds. 6. Deadlock. -PyO3 provides a struct [`GILOnceCell`] which works similarly to these types but avoids risk of deadlocking with the Python GIL. This means it can be used in place of other choices when you are experiencing the deadlock described above. See the documentation for [`GILOnceCell`] for further details and an example how to use it. +PyO3 provides a struct [`GILOnceCell`] which implements a single-initialization API based on these types that relies on the GIL for locking. If the GIL is released or there is no GIL, then this type allows the initialization function to race but ensures that the data is only ever initialized once. If you need to ensure that the initialization function is called once and only once, you can make use of the [`OnceExt`] and [`OnceLockExt`] extension traits that enable using the standard library types for this purpose but provide new methods for these types that avoid the risk of deadlocking with the Python GIL. This means they can be used in place of other choices when you are experiencing the deadlock described above. See the documentation for [`GILOnceCell`] and [`OnceExt`] for further details and an example how to use them. [`GILOnceCell`]: {{#PYO3_DOCS_URL}}/pyo3/sync/struct.GILOnceCell.html +[`OnceExt`]: {{#PYO3_DOCS_URL}}/pyo3/sync/trait.OnceExt.html +[`OnceLockExt`]: {{#PYO3_DOCS_URL}}/pyo3/sync/trait.OnceLockExt.html ## I can't run `cargo test`; or I can't build in a Cargo workspace: I'm having linker issues like "Symbol not found" or "Undefined reference to _PyExc_SystemError"! diff --git a/guide/src/free-threading.md b/guide/src/free-threading.md index 77b2ff327a2..8100a3d45ef 100644 --- a/guide/src/free-threading.md +++ b/guide/src/free-threading.md @@ -152,6 +152,60 @@ We plan to allow user-selectable semantics for mutable pyclass definitions in PyO3 0.24, allowing some form of opt-in locking to emulate the GIL if that is needed. +## Thread-safe single initialization + +Until version 0.23, PyO3 provided only `GILOnceCell` to enable deadlock-free +single initialization of data in contexts that might execute arbitrary Python +code. While we have updated `GILOnceCell` to avoid thread safety issues +triggered only under the free-threaded build, the design of `GILOnceCell` is +inherently thread-unsafe, in a manner that can be problematic even in the +GIL-enabled build. + +If, for example, the function executed by `GILOnceCell` releases the GIL or +calls code that releases the GIL, then it is possible for multiple threads to +try to race to initialize the cell. While the cell will only ever be intialized +once, it can be problematic in some contexts that `GILOnceCell` does not block +like the standard library `OnceLock`. + +In cases where the initialization function must run exactly once, you can bring +the `OnceExt` or `OnceLockExt` traits into scope. The `OnceExt` trait adds +`OnceExt::call_once_py_attached` and `OnceExt::call_once_force_py_attached` +functions to the api of `std::sync::Once`, enabling use of `Once` in contexts +where the GIL is held. Similarly, `OnceLockExt` adds +`OnceLockExt::get_or_init_py_attached`. These functions are analogous to +`Once::call_once`, `Once::call_once_force`, and `OnceLock::get_or_init` except +they accept a `Python<'py>` token in addition to an `FnOnce`. All of these +functions release the GIL and re-acquire it before executing the function, +avoiding deadlocks with the GIL that are possible without using the PyO3 +extension traits. Here is an example of how to use `OnceExt` to +enable single-initialization of a runtime cache holding a `Py`. + +```rust +# fn main() { +# use pyo3::prelude::*; +use std::sync::Once; +use pyo3::sync::OnceExt; +use pyo3::types::PyDict; + +struct RuntimeCache { + once: Once, + cache: Option> +} + +let mut cache = RuntimeCache { + once: Once::new(), + cache: None +}; + +Python::with_gil(|py| { + // guaranteed to be called once and only once + cache.once.call_once_py_attached(py, || { + cache.cache = Some(PyDict::new(py).unbind()); + }); +}); +# } +``` + ## `GILProtected` is not exposed `GILProtected` is a PyO3 type that allows mutable access to static data by diff --git a/guide/src/migration.md b/guide/src/migration.md index 0d76d220dc9..0f56498043b 100644 --- a/guide/src/migration.md +++ b/guide/src/migration.md @@ -230,7 +230,12 @@ PyO3 0.23 introduces preliminary support for the new free-threaded build of CPython 3.13. PyO3 features that implicitly assumed the existence of the GIL are not exposed in the free-threaded build, since they are no longer safe. Other features, such as `GILOnceCell`, have been internally rewritten to be threadsafe -without the GIL. +without the GIL, although note that `GILOnceCell` is inherently racey. You can +also use `OnceExt::call_once_py_attached` or +`OnceExt::call_once_force_py_attached` to enable use of `std::sync::Once` in +code that has the GIL acquired without risking a dealock with the GIL. We plan +We plan to expose more extension traits in the future that make it easier to +write code for the GIL-enabled and free-threaded builds of Python. If you make use of these features then you will need to account for the unavailability of this API in the free-threaded build. One way to handle it is diff --git a/newsfragments/4676.added.md b/newsfragments/4676.added.md new file mode 100644 index 00000000000..730b2297d91 --- /dev/null +++ b/newsfragments/4676.added.md @@ -0,0 +1 @@ +Add `pyo3::sync::OnceExt` and `pyo3::sync::OnceLockExt` traits. diff --git a/pyo3-build-config/src/lib.rs b/pyo3-build-config/src/lib.rs index 033e7b46540..642fdf1659f 100644 --- a/pyo3-build-config/src/lib.rs +++ b/pyo3-build-config/src/lib.rs @@ -138,6 +138,10 @@ fn resolve_cross_compile_config_path() -> Option { pub fn print_feature_cfgs() { let rustc_minor_version = rustc_minor_version().unwrap_or(0); + if rustc_minor_version >= 70 { + println!("cargo:rustc-cfg=rustc_has_once_lock"); + } + // invalid_from_utf8 lint was added in Rust 1.74 if rustc_minor_version >= 74 { println!("cargo:rustc-cfg=invalid_from_utf8_lint"); @@ -175,6 +179,7 @@ pub fn print_expected_cfgs() { println!("cargo:rustc-check-cfg=cfg(pyo3_leak_on_drop_without_reference_pool)"); println!("cargo:rustc-check-cfg=cfg(diagnostic_namespace)"); println!("cargo:rustc-check-cfg=cfg(c_str_lit)"); + println!("cargo:rustc-check-cfg=cfg(rustc_has_once_lock)"); // allow `Py_3_*` cfgs from the minimum supported version up to the // maximum minor version (+1 for development for the next) diff --git a/src/sealed.rs b/src/sealed.rs index cc835bee3b8..0a2846b134a 100644 --- a/src/sealed.rs +++ b/src/sealed.rs @@ -53,3 +53,5 @@ impl Sealed for ModuleDef {} impl Sealed for PyNativeTypeInitializer {} impl Sealed for PyClassInitializer {} + +impl Sealed for std::sync::Once {} diff --git a/src/sync.rs b/src/sync.rs index 65a81d06bd5..0845eaf8cec 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -5,10 +5,17 @@ //! //! [PEP 703]: https://peps.python.org/pep-703/ use crate::{ + ffi, + sealed::Sealed, types::{any::PyAnyMethods, PyAny, PyString}, Bound, Py, PyResult, PyTypeCheck, Python, }; -use std::{cell::UnsafeCell, marker::PhantomData, mem::MaybeUninit, sync::Once}; +use std::{ + cell::UnsafeCell, + marker::PhantomData, + mem::MaybeUninit, + sync::{Once, OnceState}, +}; #[cfg(not(Py_GIL_DISABLED))] use crate::PyVisit; @@ -473,6 +480,139 @@ where } } +#[cfg(rustc_has_once_lock)] +mod once_lock_ext_sealed { + pub trait Sealed {} + impl Sealed for std::sync::OnceLock {} +} + +/// Helper trait for `Once` to help avoid deadlocking when using a `Once` when attached to a +/// Python thread. +pub trait OnceExt: Sealed { + /// Similar to [`call_once`][Once::call_once], but releases the Python GIL temporarily + /// if blocking on another thread currently calling this `Once`. + fn call_once_py_attached(&self, py: Python<'_>, f: impl FnOnce()); + + /// Similar to [`call_once_force`][Once::call_once_force], but releases the Python GIL + /// temporarily if blocking on another thread currently calling this `Once`. + fn call_once_force_py_attached(&self, py: Python<'_>, f: impl FnOnce(&OnceState)); +} + +// Extension trait for [`std::sync::OnceLock`] which helps avoid deadlocks between the Python +/// interpreter and initialization with the `OnceLock`. +#[cfg(rustc_has_once_lock)] +pub trait OnceLockExt: once_lock_ext_sealed::Sealed { + /// Initializes this `OnceLock` with the given closure if it has not been initialized yet. + /// + /// If this function would block, this function detaches from the Python interpreter and + /// reattaches before calling `f`. This avoids deadlocks between the Python interpreter and + /// the `OnceLock` in cases where `f` can call arbitrary Python code, as calling arbitrary + /// Python code can lead to `f` itself blocking on the Python interpreter. + /// + /// By detaching from the Python interpreter before blocking, this ensures that if `f` blocks + /// then the Python interpreter cannot be blocked by `f` itself. + fn get_or_init_py_attached(&self, py: Python<'_>, f: F) -> &T + where + F: FnOnce() -> T; +} + +struct Guard(*mut crate::ffi::PyThreadState); + +impl Drop for Guard { + fn drop(&mut self) { + unsafe { ffi::PyEval_RestoreThread(self.0) }; + } +} + +impl OnceExt for Once { + fn call_once_py_attached(&self, py: Python<'_>, f: impl FnOnce()) { + if self.is_completed() { + return; + } + + init_once_py_attached(self, py, f) + } + + fn call_once_force_py_attached(&self, py: Python<'_>, f: impl FnOnce(&OnceState)) { + if self.is_completed() { + return; + } + + init_once_force_py_attached(self, py, f); + } +} + +#[cfg(rustc_has_once_lock)] +impl OnceLockExt for std::sync::OnceLock { + fn get_or_init_py_attached(&self, py: Python<'_>, f: F) -> &T + where + F: FnOnce() -> T, + { + // this trait is guarded by a rustc version config + // so clippy's MSRV check is wrong + #[allow(clippy::incompatible_msrv)] + // Use self.get() first to create a fast path when initialized + self.get() + .unwrap_or_else(|| init_once_lock_py_attached(self, py, f)) + } +} + +#[cold] +fn init_once_py_attached(once: &Once, _py: Python<'_>, f: F) +where + F: FnOnce() -> T, +{ + // Safety: we are currently attached to the GIL, and we expect to block. We will save + // the current thread state and restore it as soon as we are done blocking. + let ts_guard = Guard(unsafe { ffi::PyEval_SaveThread() }); + + once.call_once(move || { + drop(ts_guard); + f(); + }); +} + +#[cold] +fn init_once_force_py_attached(once: &Once, _py: Python<'_>, f: F) +where + F: FnOnce(&OnceState) -> T, +{ + // Safety: we are currently attached to the GIL, and we expect to block. We will save + // the current thread state and restore it as soon as we are done blocking. + let ts_guard = Guard(unsafe { ffi::PyEval_SaveThread() }); + + once.call_once_force(move |state| { + drop(ts_guard); + f(state); + }); +} + +#[cfg(rustc_has_once_lock)] +#[cold] +fn init_once_lock_py_attached<'a, F, T>( + lock: &'a std::sync::OnceLock, + _py: Python<'_>, + f: F, +) -> &'a T +where + F: FnOnce() -> T, +{ + // SAFETY: we are currently attached to a Python thread + let ts_guard = Guard(unsafe { ffi::PyEval_SaveThread() }); + + // this trait is guarded by a rustc version config + // so clippy's MSRV check is wrong + #[allow(clippy::incompatible_msrv)] + // By having detached here, we guarantee that `.get_or_init` cannot deadlock with + // the Python interpreter + let value = lock.get_or_init(move || { + drop(ts_guard); + f() + }); + + value +} + #[cfg(test)] mod tests { use super::*; @@ -589,4 +729,56 @@ mod tests { }); }); } + + #[test] + #[cfg(not(target_arch = "wasm32"))] // We are building wasm Python with pthreads disabled + fn test_once_ext() { + // adapted from the example in the docs for Once::try_once_force + let init = Once::new(); + std::thread::scope(|s| { + // poison the once + let handle = s.spawn(|| { + Python::with_gil(|py| { + init.call_once_py_attached(py, || panic!()); + }) + }); + assert!(handle.join().is_err()); + + // poisoning propagates + let handle = s.spawn(|| { + Python::with_gil(|py| { + init.call_once_py_attached(py, || {}); + }); + }); + + assert!(handle.join().is_err()); + + // call_once_force will still run and reset the poisoned state + Python::with_gil(|py| { + init.call_once_force_py_attached(py, |state| { + assert!(state.is_poisoned()); + }); + + // once any success happens, we stop propagating the poison + init.call_once_py_attached(py, || {}); + }); + }); + } + + #[cfg(rustc_has_once_lock)] + #[cfg(not(target_arch = "wasm32"))] // We are building wasm Python with pthreads disabled + #[test] + fn test_once_lock_ext() { + let cell = std::sync::OnceLock::new(); + std::thread::scope(|s| { + assert!(cell.get().is_none()); + + s.spawn(|| { + Python::with_gil(|py| { + assert_eq!(*cell.get_or_init_py_attached(py, || 12345), 12345); + }); + }); + }); + assert_eq!(cell.get(), Some(&12345)); + } } diff --git a/tests/test_declarative_module.rs b/tests/test_declarative_module.rs index a911702ce20..93e0e1366f0 100644 --- a/tests/test_declarative_module.rs +++ b/tests/test_declarative_module.rs @@ -1,9 +1,11 @@ #![cfg(feature = "macros")] +use std::sync::Once; + use pyo3::create_exception; use pyo3::exceptions::PyException; use pyo3::prelude::*; -use pyo3::sync::GILOnceCell; +use pyo3::sync::{GILOnceCell, OnceExt}; #[path = "../src/tests/common.rs"] mod common; @@ -149,9 +151,17 @@ mod declarative_module2 { fn declarative_module(py: Python<'_>) -> &Bound<'_, PyModule> { static MODULE: GILOnceCell> = GILOnceCell::new(); - MODULE - .get_or_init(py, || pyo3::wrap_pymodule!(declarative_module)(py)) - .bind(py) + static ONCE: Once = Once::new(); + + // Guarantee that the module is only ever initialized once; GILOnceCell can race. + // TODO: use OnceLock when MSRV >= 1.70 + ONCE.call_once_py_attached(py, || { + MODULE + .set(py, pyo3::wrap_pymodule!(declarative_module)(py)) + .expect("only ever set once"); + }); + + MODULE.get(py).expect("once is completed").bind(py) } #[test] From 00d84d888bd01831f3419dfa3f086758b6d825ca Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Mon, 4 Nov 2024 08:10:44 +0100 Subject: [PATCH 356/495] add `IntoPyObjectRef` derive macro (#4672) --- guide/src/conversions/traits.md | 3 + pyo3-macros-backend/src/intopyobject.rs | 94 +++++++++++++++------- pyo3-macros/src/lib.rs | 13 ++- src/lib.rs | 4 +- src/prelude.rs | 4 +- src/tests/hygiene/misc.rs | 8 +- tests/test_frompy_intopy_roundtrip.rs | 101 +++++++++++++++++++++--- tests/ui/invalid_intopy_derive.rs | 42 +++++----- 8 files changed, 200 insertions(+), 69 deletions(-) diff --git a/guide/src/conversions/traits.md b/guide/src/conversions/traits.md index 6cc809e0d03..a0e6ec6db0e 100644 --- a/guide/src/conversions/traits.md +++ b/guide/src/conversions/traits.md @@ -559,6 +559,9 @@ enum Enum<'a, 'py, K: Hash + Eq, V> { // enums are supported and convert using t } ``` +Additionally `IntoPyObject` can be derived for a reference to a struct or enum using the +`IntoPyObjectRef` derive macro. All the same rules from above apply as well. + #### manual implementation If the derive macro is not suitable for your use case, `IntoPyObject` can be implemented manually as diff --git a/pyo3-macros-backend/src/intopyobject.rs b/pyo3-macros-backend/src/intopyobject.rs index 3b4b2d376bb..4a46c07418f 100644 --- a/pyo3-macros-backend/src/intopyobject.rs +++ b/pyo3-macros-backend/src/intopyobject.rs @@ -164,10 +164,17 @@ impl FieldAttributes { } } +enum IntoPyObjectTypes { + Transparent(syn::Type), + Opaque { + target: TokenStream, + output: TokenStream, + error: TokenStream, + }, +} + struct IntoPyObjectImpl { - target: TokenStream, - output: TokenStream, - error: TokenStream, + types: IntoPyObjectTypes, body: TokenStream, } @@ -351,12 +358,10 @@ impl<'a> Container<'a> { .unwrap_or_default(); IntoPyObjectImpl { - target: quote! {<#ty as #pyo3_path::conversion::IntoPyObject<'py>>::Target}, - output: quote! {<#ty as #pyo3_path::conversion::IntoPyObject<'py>>::Output}, - error: quote! {<#ty as #pyo3_path::conversion::IntoPyObject<'py>>::Error}, + types: IntoPyObjectTypes::Transparent(ty.clone()), body: quote_spanned! { ty.span() => #unpack - <#ty as #pyo3_path::conversion::IntoPyObject<'py>>::into_pyobject(arg0, py) + #pyo3_path::conversion::IntoPyObject::into_pyobject(arg0, py) }, } } @@ -391,9 +396,11 @@ impl<'a> Container<'a> { .collect::(); IntoPyObjectImpl { - target: quote!(#pyo3_path::types::PyDict), - output: quote!(#pyo3_path::Bound<'py, Self::Target>), - error: quote!(#pyo3_path::PyErr), + types: IntoPyObjectTypes::Opaque { + target: quote!(#pyo3_path::types::PyDict), + output: quote!(#pyo3_path::Bound<'py, Self::Target>), + error: quote!(#pyo3_path::PyErr), + }, body: quote! { #unpack let dict = #pyo3_path::types::PyDict::new(py); @@ -419,10 +426,9 @@ impl<'a> Container<'a> { .iter() .enumerate() .map(|(i, f)| { - let ty = &f.field.ty; let value = Ident::new(&format!("arg{i}"), f.field.ty.span()); quote_spanned! { f.field.ty.span() => - <#ty as #pyo3_path::conversion::IntoPyObject>::into_pyobject(#value, py) + #pyo3_path::conversion::IntoPyObject::into_pyobject(#value, py) .map(#pyo3_path::BoundObject::into_any) .map(#pyo3_path::BoundObject::into_bound)?, } @@ -430,9 +436,11 @@ impl<'a> Container<'a> { .collect::(); IntoPyObjectImpl { - target: quote!(#pyo3_path::types::PyTuple), - output: quote!(#pyo3_path::Bound<'py, Self::Target>), - error: quote!(#pyo3_path::PyErr), + types: IntoPyObjectTypes::Opaque { + target: quote!(#pyo3_path::types::PyTuple), + output: quote!(#pyo3_path::Bound<'py, Self::Target>), + error: quote!(#pyo3_path::PyErr), + }, body: quote! { #unpack #pyo3_path::types::PyTuple::new(py, [#setter]) @@ -502,9 +510,11 @@ impl<'a> Enum<'a> { .collect::(); IntoPyObjectImpl { - target: quote!(#pyo3_path::types::PyAny), - output: quote!(#pyo3_path::Bound<'py, Self::Target>), - error: quote!(#pyo3_path::PyErr), + types: IntoPyObjectTypes::Opaque { + target: quote!(#pyo3_path::types::PyAny), + output: quote!(#pyo3_path::Bound<'py, Self::Target>), + error: quote!(#pyo3_path::PyErr), + }, body: quote! { match self { #variants @@ -520,13 +530,16 @@ fn verify_and_get_lifetime(generics: &syn::Generics) -> Option<&syn::LifetimePar lifetimes.find(|l| l.lifetime.ident == "py") } -pub fn build_derive_into_pyobject(tokens: &DeriveInput) -> Result { +pub fn build_derive_into_pyobject(tokens: &DeriveInput) -> Result { let options = ContainerOptions::from_attrs(&tokens.attrs)?; let ctx = &Ctx::new(&options.krate, None); let Ctx { pyo3_path, .. } = &ctx; let (_, ty_generics, _) = tokens.generics.split_for_impl(); let mut trait_generics = tokens.generics.clone(); + if REF { + trait_generics.params.push(parse_quote!('_a)); + } let lt_param = if let Some(lt) = verify_and_get_lifetime(&trait_generics) { lt.clone() } else { @@ -538,17 +551,14 @@ pub fn build_derive_into_pyobject(tokens: &DeriveInput) -> Result { let mut where_clause = where_clause.cloned().unwrap_or_else(|| parse_quote!(where)); for param in trait_generics.type_params() { let gen_ident = ¶m.ident; - where_clause - .predicates - .push(parse_quote!(#gen_ident: #pyo3_path::conversion::IntoPyObject<'py>)) + where_clause.predicates.push(if REF { + parse_quote!(&'_a #gen_ident: #pyo3_path::conversion::IntoPyObject<'py>) + } else { + parse_quote!(#gen_ident: #pyo3_path::conversion::IntoPyObject<'py>) + }) } - let IntoPyObjectImpl { - target, - output, - error, - body, - } = match &tokens.data { + let IntoPyObjectImpl { types, body } = match &tokens.data { syn::Data::Enum(en) => { if options.transparent.is_some() { bail_spanned!(tokens.span() => "`transparent` is not supported at top level for enums"); @@ -571,7 +581,35 @@ pub fn build_derive_into_pyobject(tokens: &DeriveInput) -> Result { ), }; + let (target, output, error) = match types { + IntoPyObjectTypes::Transparent(ty) => { + if REF { + ( + quote! { <&'_a #ty as #pyo3_path::IntoPyObject<'py>>::Target }, + quote! { <&'_a #ty as #pyo3_path::IntoPyObject<'py>>::Output }, + quote! { <&'_a #ty as #pyo3_path::IntoPyObject<'py>>::Error }, + ) + } else { + ( + quote! { <#ty as #pyo3_path::IntoPyObject<'py>>::Target }, + quote! { <#ty as #pyo3_path::IntoPyObject<'py>>::Output }, + quote! { <#ty as #pyo3_path::IntoPyObject<'py>>::Error }, + ) + } + } + IntoPyObjectTypes::Opaque { + target, + output, + error, + } => (target, output, error), + }; + let ident = &tokens.ident; + let ident = if REF { + quote! { &'_a #ident} + } else { + quote! { #ident } + }; Ok(quote!( #[automatically_derived] impl #impl_generics #pyo3_path::conversion::IntoPyObject<#lt_param> for #ident #ty_generics #where_clause { diff --git a/pyo3-macros/src/lib.rs b/pyo3-macros/src/lib.rs index 7c43c55dcd7..2621bea4c6e 100644 --- a/pyo3-macros/src/lib.rs +++ b/pyo3-macros/src/lib.rs @@ -156,7 +156,18 @@ pub fn pyfunction(attr: TokenStream, input: TokenStream) -> TokenStream { #[proc_macro_derive(IntoPyObject, attributes(pyo3))] pub fn derive_into_py_object(item: TokenStream) -> TokenStream { let ast = parse_macro_input!(item as syn::DeriveInput); - let expanded = build_derive_into_pyobject(&ast).unwrap_or_compile_error(); + let expanded = build_derive_into_pyobject::(&ast).unwrap_or_compile_error(); + quote!( + #expanded + ) + .into() +} + +#[proc_macro_derive(IntoPyObjectRef, attributes(pyo3))] +pub fn derive_into_py_object_ref(item: TokenStream) -> TokenStream { + let ast = parse_macro_input!(item as syn::DeriveInput); + let expanded = + pyo3_macros_backend::build_derive_into_pyobject::(&ast).unwrap_or_compile_error(); quote!( #expanded ) diff --git a/src/lib.rs b/src/lib.rs index 25c88143609..c71e12b8649 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -457,7 +457,9 @@ mod version; pub use crate::conversions::*; #[cfg(feature = "macros")] -pub use pyo3_macros::{pyfunction, pymethods, pymodule, FromPyObject, IntoPyObject}; +pub use pyo3_macros::{ + pyfunction, pymethods, pymodule, FromPyObject, IntoPyObject, IntoPyObjectRef, +}; /// A proc macro used to expose Rust structs and fieldless enums as Python objects. /// diff --git a/src/prelude.rs b/src/prelude.rs index 54f5a9f6beb..d4f649f552a 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -19,7 +19,9 @@ pub use crate::pyclass_init::PyClassInitializer; pub use crate::types::{PyAny, PyModule}; #[cfg(feature = "macros")] -pub use pyo3_macros::{pyclass, pyfunction, pymethods, pymodule, FromPyObject, IntoPyObject}; +pub use pyo3_macros::{ + pyclass, pyfunction, pymethods, pymodule, FromPyObject, IntoPyObject, IntoPyObjectRef, +}; #[cfg(feature = "macros")] pub use crate::wrap_pyfunction; diff --git a/src/tests/hygiene/misc.rs b/src/tests/hygiene/misc.rs index 1790c65961d..cecc8991f4a 100644 --- a/src/tests/hygiene/misc.rs +++ b/src/tests/hygiene/misc.rs @@ -57,17 +57,17 @@ macro_rules! macro_rules_hygiene { macro_rules_hygiene!(MyClass1, MyClass2); -#[derive(crate::IntoPyObject)] +#[derive(crate::IntoPyObject, crate::IntoPyObjectRef)] #[pyo3(crate = "crate")] struct IntoPyObject1(i32); // transparent newtype case -#[derive(crate::IntoPyObject)] +#[derive(crate::IntoPyObject, crate::IntoPyObjectRef)] #[pyo3(crate = "crate", transparent)] struct IntoPyObject2<'a> { inner: &'a str, // transparent newtype case } -#[derive(crate::IntoPyObject)] +#[derive(crate::IntoPyObject, crate::IntoPyObjectRef)] #[pyo3(crate = "crate")] struct IntoPyObject3<'py>(i32, crate::Bound<'py, crate::PyAny>); // tuple case @@ -78,7 +78,7 @@ struct IntoPyObject4<'a, 'py> { num: usize, } -#[derive(crate::IntoPyObject)] +#[derive(crate::IntoPyObject, crate::IntoPyObjectRef)] #[pyo3(crate = "crate")] enum IntoPyObject5<'a, 'py> { TransparentTuple(i32), diff --git a/tests/test_frompy_intopy_roundtrip.rs b/tests/test_frompy_intopy_roundtrip.rs index 6b3718693d7..b17320fa43b 100644 --- a/tests/test_frompy_intopy_roundtrip.rs +++ b/tests/test_frompy_intopy_roundtrip.rs @@ -1,7 +1,7 @@ #![cfg(feature = "macros")] use pyo3::types::{PyDict, PyString}; -use pyo3::{prelude::*, IntoPyObject}; +use pyo3::{prelude::*, IntoPyObject, IntoPyObjectRef}; use std::collections::HashMap; use std::hash::Hash; @@ -9,7 +9,7 @@ use std::hash::Hash; #[path = "../src/tests/common.rs"] mod common; -#[derive(Debug, Clone, IntoPyObject, FromPyObject)] +#[derive(Debug, Clone, IntoPyObject, IntoPyObjectRef, FromPyObject)] pub struct A<'py> { #[pyo3(item)] s: String, @@ -27,6 +27,16 @@ fn test_named_fields_struct() { t: PyString::new(py, "World"), p: 42i32.into_pyobject(py).unwrap().into_any(), }; + let pya = (&a).into_pyobject(py).unwrap(); + let new_a = pya.extract::>().unwrap(); + + assert_eq!(a.s, new_a.s); + assert_eq!(a.t.to_cow().unwrap(), new_a.t.to_cow().unwrap()); + assert_eq!( + a.p.extract::().unwrap(), + new_a.p.extract::().unwrap() + ); + let pya = a.clone().into_pyobject(py).unwrap(); let new_a = pya.extract::>().unwrap(); @@ -39,7 +49,7 @@ fn test_named_fields_struct() { }); } -#[derive(Debug, Clone, PartialEq, IntoPyObject, FromPyObject)] +#[derive(Debug, Clone, PartialEq, IntoPyObject, IntoPyObjectRef, FromPyObject)] #[pyo3(transparent)] pub struct B { test: String, @@ -51,13 +61,17 @@ fn test_transparent_named_field_struct() { let b = B { test: "test".into(), }; + let pyb = (&b).into_pyobject(py).unwrap(); + let new_b = pyb.extract::().unwrap(); + assert_eq!(b, new_b); + let pyb = b.clone().into_pyobject(py).unwrap(); let new_b = pyb.extract::().unwrap(); assert_eq!(b, new_b); }); } -#[derive(Debug, Clone, PartialEq, IntoPyObject, FromPyObject)] +#[derive(Debug, Clone, PartialEq, IntoPyObject, IntoPyObjectRef, FromPyObject)] #[pyo3(transparent)] pub struct D { test: T, @@ -66,6 +80,18 @@ pub struct D { #[test] fn test_generic_transparent_named_field_struct() { Python::with_gil(|py| { + let d = D { + test: String::from("test"), + }; + let pyd = (&d).into_pyobject(py).unwrap(); + let new_d = pyd.extract::>().unwrap(); + assert_eq!(d, new_d); + + let d = D { test: 1usize }; + let pyd = (&d).into_pyobject(py).unwrap(); + let new_d = pyd.extract::>().unwrap(); + assert_eq!(d, new_d); + let d = D { test: String::from("test"), }; @@ -80,7 +106,7 @@ fn test_generic_transparent_named_field_struct() { }); } -#[derive(Debug, IntoPyObject, FromPyObject)] +#[derive(Debug, IntoPyObject, IntoPyObjectRef, FromPyObject)] pub struct GenericWithBound(HashMap); #[test] @@ -89,10 +115,12 @@ fn test_generic_with_bound() { let mut hash_map = HashMap::::new(); hash_map.insert("1".into(), 1); hash_map.insert("2".into(), 2); - let map = GenericWithBound(hash_map).into_pyobject(py).unwrap(); - assert_eq!(map.len(), 2); + let map = GenericWithBound(hash_map); + let py_map = (&map).into_pyobject(py).unwrap(); + assert_eq!(py_map.len(), 2); assert_eq!( - map.get_item("1") + py_map + .get_item("1") .unwrap() .unwrap() .extract::() @@ -100,44 +128,75 @@ fn test_generic_with_bound() { 1 ); assert_eq!( - map.get_item("2") + py_map + .get_item("2") .unwrap() .unwrap() .extract::() .unwrap(), 2 ); - assert!(map.get_item("3").unwrap().is_none()); + assert!(py_map.get_item("3").unwrap().is_none()); + + let py_map = map.into_pyobject(py).unwrap(); + assert_eq!(py_map.len(), 2); + assert_eq!( + py_map + .get_item("1") + .unwrap() + .unwrap() + .extract::() + .unwrap(), + 1 + ); + assert_eq!( + py_map + .get_item("2") + .unwrap() + .unwrap() + .extract::() + .unwrap(), + 2 + ); + assert!(py_map.get_item("3").unwrap().is_none()); }); } -#[derive(Debug, Clone, PartialEq, IntoPyObject, FromPyObject)] +#[derive(Debug, Clone, PartialEq, IntoPyObject, IntoPyObjectRef, FromPyObject)] pub struct Tuple(String, usize); #[test] fn test_tuple_struct() { Python::with_gil(|py| { let tup = Tuple(String::from("test"), 1); + let tuple = (&tup).into_pyobject(py).unwrap(); + let new_tup = tuple.extract::().unwrap(); + assert_eq!(tup, new_tup); + let tuple = tup.clone().into_pyobject(py).unwrap(); let new_tup = tuple.extract::().unwrap(); assert_eq!(tup, new_tup); }); } -#[derive(Debug, Clone, PartialEq, IntoPyObject, FromPyObject)] +#[derive(Debug, Clone, PartialEq, IntoPyObject, IntoPyObjectRef, FromPyObject)] pub struct TransparentTuple(String); #[test] fn test_transparent_tuple_struct() { Python::with_gil(|py| { let tup = TransparentTuple(String::from("test")); + let tuple = (&tup).into_pyobject(py).unwrap(); + let new_tup = tuple.extract::().unwrap(); + assert_eq!(tup, new_tup); + let tuple = tup.clone().into_pyobject(py).unwrap(); let new_tup = tuple.extract::().unwrap(); assert_eq!(tup, new_tup); }); } -#[derive(Debug, Clone, PartialEq, IntoPyObject, FromPyObject)] +#[derive(Debug, Clone, PartialEq, IntoPyObject, IntoPyObjectRef, FromPyObject)] pub enum Foo { TupleVar(usize, String), StructVar { @@ -156,10 +215,20 @@ pub enum Foo { fn test_enum() { Python::with_gil(|py| { let tuple_var = Foo::TupleVar(1, "test".into()); + let foo = (&tuple_var).into_pyobject(py).unwrap(); + assert_eq!(tuple_var, foo.extract::().unwrap()); + let foo = tuple_var.clone().into_pyobject(py).unwrap(); assert_eq!(tuple_var, foo.extract::().unwrap()); let struct_var = Foo::StructVar { test: 'b' }; + let foo = (&struct_var) + .into_pyobject(py) + .unwrap() + .downcast_into::() + .unwrap(); + assert_eq!(struct_var, foo.extract::().unwrap()); + let foo = struct_var .clone() .into_pyobject(py) @@ -170,10 +239,16 @@ fn test_enum() { assert_eq!(struct_var, foo.extract::().unwrap()); let transparent_tuple = Foo::TransparentTuple(1); + let foo = (&transparent_tuple).into_pyobject(py).unwrap(); + assert_eq!(transparent_tuple, foo.extract::().unwrap()); + let foo = transparent_tuple.clone().into_pyobject(py).unwrap(); assert_eq!(transparent_tuple, foo.extract::().unwrap()); let transparent_struct_var = Foo::TransparentStructVar { a: None }; + let foo = (&transparent_struct_var).into_pyobject(py).unwrap(); + assert_eq!(transparent_struct_var, foo.extract::().unwrap()); + let foo = transparent_struct_var.clone().into_pyobject(py).unwrap(); assert_eq!(transparent_struct_var, foo.extract::().unwrap()); }); diff --git a/tests/ui/invalid_intopy_derive.rs b/tests/ui/invalid_intopy_derive.rs index 310309992d4..c65d44ff1bc 100644 --- a/tests/ui/invalid_intopy_derive.rs +++ b/tests/ui/invalid_intopy_derive.rs @@ -1,67 +1,67 @@ -use pyo3::IntoPyObject; +use pyo3::{IntoPyObject, IntoPyObjectRef}; -#[derive(IntoPyObject)] +#[derive(IntoPyObject, IntoPyObjectRef)] struct Foo(); -#[derive(IntoPyObject)] +#[derive(IntoPyObject, IntoPyObjectRef)] struct Foo2 {} -#[derive(IntoPyObject)] +#[derive(IntoPyObject, IntoPyObjectRef)] enum EmptyEnum {} -#[derive(IntoPyObject)] +#[derive(IntoPyObject, IntoPyObjectRef)] enum EnumWithEmptyTupleVar { EmptyTuple(), Valid(String), } -#[derive(IntoPyObject)] +#[derive(IntoPyObject, IntoPyObjectRef)] enum EnumWithEmptyStructVar { EmptyStruct {}, Valid(String), } -#[derive(IntoPyObject)] +#[derive(IntoPyObject, IntoPyObjectRef)] #[pyo3(transparent)] struct EmptyTransparentTup(); -#[derive(IntoPyObject)] +#[derive(IntoPyObject, IntoPyObjectRef)] #[pyo3(transparent)] struct EmptyTransparentStruct {} -#[derive(IntoPyObject)] +#[derive(IntoPyObject, IntoPyObjectRef)] enum EnumWithTransparentEmptyTupleVar { #[pyo3(transparent)] EmptyTuple(), Valid(String), } -#[derive(IntoPyObject)] +#[derive(IntoPyObject, IntoPyObjectRef)] enum EnumWithTransparentEmptyStructVar { #[pyo3(transparent)] EmptyStruct {}, Valid(String), } -#[derive(IntoPyObject)] +#[derive(IntoPyObject, IntoPyObjectRef)] #[pyo3(transparent)] struct TransparentTupTooManyFields(String, String); -#[derive(IntoPyObject)] +#[derive(IntoPyObject, IntoPyObjectRef)] #[pyo3(transparent)] struct TransparentStructTooManyFields { foo: String, bar: String, } -#[derive(IntoPyObject)] +#[derive(IntoPyObject, IntoPyObjectRef)] enum EnumWithTransparentTupleTooMany { #[pyo3(transparent)] EmptyTuple(String, String), Valid(String), } -#[derive(IntoPyObject)] +#[derive(IntoPyObject, IntoPyObjectRef)] enum EnumWithTransparentStructTooMany { #[pyo3(transparent)] EmptyStruct { @@ -71,35 +71,35 @@ enum EnumWithTransparentStructTooMany { Valid(String), } -#[derive(IntoPyObject)] +#[derive(IntoPyObject, IntoPyObjectRef)] #[pyo3(unknown = "should not work")] struct UnknownContainerAttr { a: String, } -#[derive(IntoPyObject)] +#[derive(IntoPyObject, IntoPyObjectRef)] union Union { a: usize, } -#[derive(IntoPyObject)] +#[derive(IntoPyObject, IntoPyObjectRef)] enum UnitEnum { Unit, } -#[derive(IntoPyObject)] +#[derive(IntoPyObject, IntoPyObjectRef)] struct TupleAttribute(#[pyo3(attribute)] String, usize); -#[derive(IntoPyObject)] +#[derive(IntoPyObject, IntoPyObjectRef)] struct TupleItem(#[pyo3(item)] String, usize); -#[derive(IntoPyObject)] +#[derive(IntoPyObject, IntoPyObjectRef)] struct StructAttribute { #[pyo3(attribute)] foo: String, } -#[derive(IntoPyObject)] +#[derive(IntoPyObject, IntoPyObjectRef)] #[pyo3(transparent)] struct StructTransparentItem { #[pyo3(item)] From 63f21892215a0dd911744a420eb077c094f39e2b Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Mon, 4 Nov 2024 17:44:20 +0000 Subject: [PATCH 357/495] add blanket `IntoPyObject` implementation for `&&T` (#4680) --- src/conversion.rs | 14 ++++++++++++++ src/tests/hygiene/misc.rs | 2 +- tests/ui/invalid_property_args.stderr | 2 +- tests/ui/missing_intopy.stderr | 2 +- 4 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/conversion.rs b/src/conversion.rs index a280055cc7c..46ff4af3a62 100644 --- a/src/conversion.rs +++ b/src/conversion.rs @@ -333,6 +333,20 @@ impl<'a, 'py, T> IntoPyObject<'py> for &'a Py { } } +impl<'a, 'py, T> IntoPyObject<'py> for &&'a T +where + &'a T: IntoPyObject<'py>, +{ + type Target = <&'a T as IntoPyObject<'py>>::Target; + type Output = <&'a T as IntoPyObject<'py>>::Output; + type Error = <&'a T as IntoPyObject<'py>>::Error; + + #[inline] + fn into_pyobject(self, py: Python<'py>) -> Result { + (*self).into_pyobject(py) + } +} + /// Extract a type from a Python object. /// /// diff --git a/src/tests/hygiene/misc.rs b/src/tests/hygiene/misc.rs index cecc8991f4a..6e00167ddb6 100644 --- a/src/tests/hygiene/misc.rs +++ b/src/tests/hygiene/misc.rs @@ -71,7 +71,7 @@ struct IntoPyObject2<'a> { #[pyo3(crate = "crate")] struct IntoPyObject3<'py>(i32, crate::Bound<'py, crate::PyAny>); // tuple case -#[derive(crate::IntoPyObject)] +#[derive(crate::IntoPyObject, crate::IntoPyObjectRef)] #[pyo3(crate = "crate")] struct IntoPyObject4<'a, 'py> { callable: &'a crate::Bound<'py, crate::PyAny>, // struct case diff --git a/tests/ui/invalid_property_args.stderr b/tests/ui/invalid_property_args.stderr index 03f3ba963d8..f2fea2a1dd5 100644 --- a/tests/ui/invalid_property_args.stderr +++ b/tests/ui/invalid_property_args.stderr @@ -55,6 +55,7 @@ error[E0277]: `PhantomData` cannot be converted to a Python object = help: the trait `IntoPyObject<'_>` is not implemented for `PhantomData`, which is required by `for<'py> PhantomData: PyO3GetField<'py>` = note: implement `IntoPyObject` for `&PhantomData` or `IntoPyObject + Clone` for `PhantomData` to define the conversion = help: the following other types implement trait `IntoPyObject<'py>`: + &&'a T &&OsStr &&Path &&str @@ -62,7 +63,6 @@ error[E0277]: `PhantomData` cannot be converted to a Python object &'a (T0, T1, T2) &'a (T0, T1, T2, T3) &'a (T0, T1, T2, T3, T4) - &'a (T0, T1, T2, T3, T4, T5) and $N others = note: required for `PhantomData` to implement `for<'py> PyO3GetField<'py>` note: required by a bound in `PyClassGetterGenerator::::generate` diff --git a/tests/ui/missing_intopy.stderr b/tests/ui/missing_intopy.stderr index 587ebd479de..afa8d9e48a4 100644 --- a/tests/ui/missing_intopy.stderr +++ b/tests/ui/missing_intopy.stderr @@ -8,6 +8,7 @@ error[E0277]: `Blah` cannot be converted to a Python object = note: if you do not wish to have a corresponding Python type, implement it manually = note: if you do not own `Blah` you can perform a manual conversion to one of the types in `pyo3::types::*` = help: the following other types implement trait `IntoPyObject<'py>`: + &&'a T &&OsStr &&Path &&str @@ -15,7 +16,6 @@ error[E0277]: `Blah` cannot be converted to a Python object &'a (T0, T1, T2) &'a (T0, T1, T2, T3) &'a (T0, T1, T2, T3, T4) - &'a (T0, T1, T2, T3, T4, T5) and $N others note: required by a bound in `UnknownReturnType::::wrap` --> src/impl_/wrap.rs From d45e0bd55428bb3c0cdb90e5842dfe308abcbedb Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Mon, 4 Nov 2024 14:32:23 -0700 Subject: [PATCH 358/495] docs: Add and fix links in free-threading guide. (#4673) * Add and fix links in free-threading guide. * use PYO3_DOCS_URL instead of latest * add more links and use link anchors * revert change to pyclass-parameters.md --- guide/src/free-threading.md | 103 ++++++++++++++++++++++-------------- guide/src/migration.md | 2 +- 2 files changed, 65 insertions(+), 40 deletions(-) diff --git a/guide/src/free-threading.md b/guide/src/free-threading.md index 8100a3d45ef..6d3493d2db6 100644 --- a/guide/src/free-threading.md +++ b/guide/src/free-threading.md @@ -24,15 +24,18 @@ cannot be sped up using parallelism. The free-threaded build removes this limit on multithreaded Python scaling. This means it's much more straightforward to achieve parallelism using the Python -`threading` module. If you have ever needed to use `multiprocessing` to achieve -a parallel speedup for some Python code, free-threading will likely allow the -use of Python threads instead for the same workflow. +[`threading`] module. If you +have ever needed to use +[`multiprocessing`](https://docs.python.org/3/library/multiprocessing.html) to +achieve a parallel speedup for some Python code, free-threading will likely +allow the use of Python threads instead for the same workflow. PyO3's support for free-threaded Python will enable authoring native Python extensions that are thread-safe by construction, with much stronger safety guarantees than C extensions. Our goal is to enable ["fearless concurrency"](https://doc.rust-lang.org/book/ch16-00-concurrency.html) in the -native Python runtime by building on the Rust `Send` and `Sync` traits. +native Python runtime by building on the Rust [`Send` and +`Sync`](https://doc.rust-lang.org/nomicon/send-and-sync.html) traits. This document provides advice for porting Rust code using PyO3 to run under free-threaded Python. While many simple PyO3 uses, like defining an immutable @@ -45,7 +48,8 @@ We are aware that there are some naming issues in the PyO3 API that make it awkward to think about a runtime environment where there is no GIL. We plan to change the names of these types to de-emphasize the role of the GIL in future versions of PyO3, but for now you should remember that the use of the term `GIL` -in functions and types like `with_gil` and `GILOnceCell` is historical. +in functions and types like [`Python::with_gil`] and [`GILOnceCell`] is +historical. Instead, you can think about whether or not a Rust thread is attached to a Python interpreter runtime. See [PEP @@ -64,16 +68,16 @@ The main reason for attaching to the Python runtime is to interact with Python objects or call into the CPython C API. To interact with the Python runtime, the thread must register itself by attaching to the interpreter runtime. If you are not yet attached to the Python runtime, you can register the thread using the -[`Python::with_gil`] function. Threads created via the Python `threading` module -do not not need to do this, but all other OS threads that interact with the -Python runtime must explicitly attach using `with_gil` and obtain a `'py` +[`Python::with_gil`] function. Threads created via the Python [`threading`] +module do not not need to do this, but all other OS threads that interact with +the Python runtime must explicitly attach using `with_gil` and obtain a `'py` liftime. -In the GIL-enabled build, PyO3 uses the `Python<'py>` type and the `'py` lifetime -to signify that the global interpreter lock is held. In the freethreaded build, -holding a `'py` lifetime means the thread is currently attached to the Python -interpreter but other threads might be simultaneously interacting with the -Python runtime. +In the GIL-enabled build, PyO3 uses the [`Python<'py>`] type and the `'py` +lifetime to signify that the global interpreter lock is held. In the +freethreaded build, holding a `'py` lifetime means only that the thread is +currently attached to the Python interpreter -- other threads can be +simultaneously interacting with the interpreter. Since there is no GIL in the free-threaded build, releasing the GIL for long-running tasks is no longer necessary to ensure other threads run, but you @@ -89,15 +93,16 @@ Data attached to `pyclass` instances is protected from concurrent access by a `RefCell`-like pattern of runtime borrow checking. Like a `RefCell`, PyO3 will raise exceptions (or in some cases panic) to enforce exclusive access for mutable borrows. It was always possible to generate panics like this in PyO3 in -code that releases the GIL with `allow_threads` or caling a `pymethod` accepting -`&self` from a `&mut self` (see [the docs on interior +code that releases the GIL with [`Python::allow_threads`] or calling a python +method accepting `&self` from a `&mut self` (see [the docs on interior mutability](./class.md#bound-and-interior-mutability),) but now in free-threaded Python there are more opportunities to trigger these panics from Python because there is no GIL to lock concurrent access to mutably borrowed data from Python. The most straightforward way to trigger this problem to use the Python -`threading` module to simultaneously call a rust function that mutably borrows a -`pyclass`. For example, consider the following `PyClass` implementation: +[`threading`] module to simultaneously call a rust function that mutably borrows a +[`pyclass`]({{#PYO3_DOCS_URL}}/pyo3/attr.pyclass.html). For example, +consider the following implementation: ``` # use pyo3::prelude::*; @@ -154,30 +159,30 @@ needed. ## Thread-safe single initialization -Until version 0.23, PyO3 provided only `GILOnceCell` to enable deadlock-free +Until version 0.23, PyO3 provided only [`GILOnceCell`] to enable deadlock-free single initialization of data in contexts that might execute arbitrary Python -code. While we have updated `GILOnceCell` to avoid thread safety issues -triggered only under the free-threaded build, the design of `GILOnceCell` is +code. While we have updated [`GILOnceCell`] to avoid thread safety issues +triggered only under the free-threaded build, the design of [`GILOnceCell`] is inherently thread-unsafe, in a manner that can be problematic even in the GIL-enabled build. -If, for example, the function executed by `GILOnceCell` releases the GIL or +If, for example, the function executed by [`GILOnceCell`] releases the GIL or calls code that releases the GIL, then it is possible for multiple threads to try to race to initialize the cell. While the cell will only ever be intialized -once, it can be problematic in some contexts that `GILOnceCell` does not block -like the standard library `OnceLock`. +once, it can be problematic in some contexts that [`GILOnceCell`] does not block +like the standard library [`OnceLock`]. In cases where the initialization function must run exactly once, you can bring -the `OnceExt` or `OnceLockExt` traits into scope. The `OnceExt` trait adds -`OnceExt::call_once_py_attached` and `OnceExt::call_once_force_py_attached` -functions to the api of `std::sync::Once`, enabling use of `Once` in contexts -where the GIL is held. Similarly, `OnceLockExt` adds -`OnceLockExt::get_or_init_py_attached`. These functions are analogous to -`Once::call_once`, `Once::call_once_force`, and `OnceLock::get_or_init` except -they accept a `Python<'py>` token in addition to an `FnOnce`. All of these +the [`OnceExt`] or [`OnceLockExt`] traits into scope. The [`OnceExt`] trait adds +[`OnceExt::call_once_py_attached`] and [`OnceExt::call_once_force_py_attached`] +functions to the api of `std::sync::Once`, enabling use of [`Once`] in contexts +where the GIL is held. Similarly, [`OnceLockExt`] adds +[`OnceLockExt::get_or_init_py_attached`]. These functions are analogous to +[`Once::call_once`], [`Once::call_once_force`], and [`OnceLock::get_or_init`] except +they accept a [`Python<'py>`] token in addition to an `FnOnce`. All of these functions release the GIL and re-acquire it before executing the function, avoiding deadlocks with the GIL that are possible without using the PyO3 -extension traits. Here is an example of how to use `OnceExt` to +extension traits. Here is an example of how to use [`OnceExt`] to enable single-initialization of a runtime cache holding a `Py`. ```rust @@ -208,11 +213,13 @@ Python::with_gil(|py| { ## `GILProtected` is not exposed -`GILProtected` is a PyO3 type that allows mutable access to static data by +[`GILProtected`] is a PyO3 type that allows mutable access to static data by leveraging the GIL to lock concurrent access from other threads. In free-threaded Python there is no GIL, so you will need to replace this type with -some other form of locking. In many cases, a type from `std::sync::atomic` or -a `std::sync::Mutex` will be sufficient. +some other form of locking. In many cases, a type from +[`std::sync::atomic`](https://doc.rust-lang.org/std/sync/atomic/) or a +[`std::sync::Mutex`](https://doc.rust-lang.org/std/sync/struct.Mutex.html) will +be sufficient. Before: @@ -258,10 +265,28 @@ Python::with_gil(|py| { ``` If you are executing arbitrary Python code while holding the lock, then you will -need to use conditional compilation to use `GILProtected` on GIL-enabled Python -builds and mutexes otherwise. If your use of `GILProtected` does not guard the +need to use conditional compilation to use [`GILProtected`] on GIL-enabled Python +builds and mutexes otherwise. If your use of [`GILProtected`] does not guard the execution of arbitrary Python code or use of the CPython C API, then conditional -compilation is likely unnecessary since `GILProtected` was not needed in the +compilation is likely unnecessary since [`GILProtected`] was not needed in the first place and instead Rust mutexes or atomics should be preferred. Python 3.13 -introduces `PyMutex`, which releases the GIL while the waiting for the lock, so -that is another option if you only need to support newer Python versions. +introduces [`PyMutex`](https://docs.python.org/3/c-api/init.html#c.PyMutex), +which releases the GIL while the waiting for the lock, so that is another option +if you only need to support newer Python versions. + +[`GILOnceCell`]: {{#PYO3_DOCS_URL}}/pyo3/sync/struct.GILOnceCell.html +[`GILProtected]: {{#PYO3_DOCS_URL}}/pyo3/sync/struct.GILProtected.html +[`Once`]: https://doc.rust-lang.org/stable/std/sync/struct.Once.html +[`Once::call_once`]: https://doc.rust-lang.org/stable/std/sync/struct.Once.html#tymethod.call_once +[`Once::call_once_force`]: https://doc.rust-lang.org/stable/std/sync/struct.Once.html#tymethod.call_once_force +[`OnceExt]: {{#PYO3_DOCS_URL}}/pyo3/sync/trait.OnceExt.html +[`OnceExt::call_once_py_attached`]: {{#PYO3_DOCS_URL}}/pyo3/sync/trait.OnceExt.html#tymethod.call_once_py_attached +[`OnceExt::call_once_force_py_attached`]: {{#PYO3_DOCS_URL}}/pyo3/sync/trait.OnceExt.html#tymethod.call_once_force_py_attached +[`OnceLockExt]: {{#PYO3_DOCS_URL}}/pyo3/sync/trait.OnceLockExt.html +[`OnceLockExt::get_or_init_py_attached]: {{#PYO3_DOCS_URL}}/pyo3/sync/trait.OnceLockExt.html#tymethod.get_or_init_py_attached +[`OnceLock`]: https://doc.rust-lang.org/stable/std/sync/struct.OnceLock.html +[`OnceLock::get_or_init`]: https://doc.rust-lang.org/stable/std/sync/struct.OnceLock.html#tymethod.get_or_init +[`Python::allow_threads`]: {{#PYO3_DOCS_URL}}/pyo3/marker/struct.Python.html#method.allow_threads +[`Python::with_gil`]: {{#PYO3_DOCS_URL}}/pyo3/marker/struct.Python.html#method.with_gil +[`Python<'py>`]: {{#PYO3_DOCS_URL}}/pyo3/marker/struct.Python.html +[`threading`]: https://docs.python.org/3/library/threading.html diff --git a/guide/src/migration.md b/guide/src/migration.md index 0f56498043b..2cecc73d278 100644 --- a/guide/src/migration.md +++ b/guide/src/migration.md @@ -1064,7 +1064,7 @@ Python::with_gil(|py| { }); ``` -Furthermore, `Python::acquire_gil` provides ownership of a `GILGuard` which can be freely stored and passed around. This is usually not helpful as it may keep the lock held for a long time thereby blocking progress in other parts of the program. Due to the generative lifetime attached to the GIL token supplied by `Python::with_gil`, the problem is avoided as the GIL token can only be passed down the call chain. Often, this issue can also be avoided entirely as any GIL-bound reference `&'py PyAny` implies access to a GIL token `Python<'py>` via the [`PyAny::py`](https://docs.rs/pyo3/latest/pyo3/types/struct.PyAny.html#method.py) method. +Furthermore, `Python::acquire_gil` provides ownership of a `GILGuard` which can be freely stored and passed around. This is usually not helpful as it may keep the lock held for a long time thereby blocking progress in other parts of the program. Due to the generative lifetime attached to the GIL token supplied by `Python::with_gil`, the problem is avoided as the GIL token can only be passed down the call chain. Often, this issue can also be avoided entirely as any GIL-bound reference `&'py PyAny` implies access to a GIL token `Python<'py>` via the [`PyAny::py`](https://docs.rs/pyo3/0.22.5/pyo3/types/struct.PyAny.html#method.py) method. ## from 0.17.* to 0.18 From 9f955e4ebf5f38d1f9de4837951588ef6c850622 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Tue, 5 Nov 2024 03:13:16 +0000 Subject: [PATCH 359/495] make `PyErrState` thread-safe (#4671) * make `PyErrState` thread-safe * fix clippy * add test of reentrancy, fix deadlock * newsfragment * fix MSRV * fix nightly build * Update err_state.rs --------- Co-authored-by: Nathan Goldbaum --- newsfragments/4671.fixed.md | 1 + src/err/err_state.rs | 163 ++++++++++++++++++++++++++++++------ 2 files changed, 138 insertions(+), 26 deletions(-) create mode 100644 newsfragments/4671.fixed.md diff --git a/newsfragments/4671.fixed.md b/newsfragments/4671.fixed.md new file mode 100644 index 00000000000..9b0cd9d8f0c --- /dev/null +++ b/newsfragments/4671.fixed.md @@ -0,0 +1 @@ +Make `PyErr` internals thread-safe. diff --git a/src/err/err_state.rs b/src/err/err_state.rs index 2ba153b6ef8..3b9e9800b6e 100644 --- a/src/err/err_state.rs +++ b/src/err/err_state.rs @@ -1,4 +1,8 @@ -use std::cell::UnsafeCell; +use std::{ + cell::UnsafeCell, + sync::{Mutex, Once}, + thread::ThreadId, +}; use crate::{ exceptions::{PyBaseException, PyTypeError}, @@ -11,15 +15,18 @@ use crate::{ pub(crate) struct PyErrState { // Safety: can only hand out references when in the "normalized" state. Will never change // after normalization. - // - // The state is temporarily removed from the PyErr during normalization, to avoid - // concurrent modifications. + normalized: Once, + // Guard against re-entrancy when normalizing the exception state. + normalizing_thread: Mutex>, inner: UnsafeCell>, } -// The inner value is only accessed through ways that require the gil is held. +// Safety: The inner value is protected by locking to ensure that only the normalized state is +// handed out as a reference. unsafe impl Send for PyErrState {} unsafe impl Sync for PyErrState {} +#[cfg(feature = "nightly")] +unsafe impl crate::marker::Ungil for PyErrState {} impl PyErrState { pub(crate) fn lazy(f: Box) -> Self { @@ -48,17 +55,22 @@ impl PyErrState { fn from_inner(inner: PyErrStateInner) -> Self { Self { + normalized: Once::new(), + normalizing_thread: Mutex::new(None), inner: UnsafeCell::new(Some(inner)), } } #[inline] pub(crate) fn as_normalized(&self, py: Python<'_>) -> &PyErrStateNormalized { - if let Some(PyErrStateInner::Normalized(n)) = unsafe { - // Safety: self.inner will never be written again once normalized. - &*self.inner.get() - } { - return n; + if self.normalized.is_completed() { + match unsafe { + // Safety: self.inner will never be written again once normalized. + &*self.inner.get() + } { + Some(PyErrStateInner::Normalized(n)) => return n, + _ => unreachable!(), + } } self.make_normalized(py) @@ -69,25 +81,47 @@ impl PyErrState { // This process is safe because: // - Access is guaranteed not to be concurrent thanks to `Python` GIL token // - Write happens only once, and then never will change again. - // - State is set to None during the normalization process, so that a second - // concurrent normalization attempt will panic before changing anything. - // FIXME: this needs to be rewritten to deal with free-threaded Python - // see https://github.com/PyO3/pyo3/issues/4584 + // Guard against re-entrant normalization, because `Once` does not provide + // re-entrancy guarantees. + if let Some(thread) = self.normalizing_thread.lock().unwrap().as_ref() { + assert!( + !(*thread == std::thread::current().id()), + "Re-entrant normalization of PyErrState detected" + ); + } - let state = unsafe { - (*self.inner.get()) - .take() - .expect("Cannot normalize a PyErr while already normalizing it.") - }; + // avoid deadlock of `.call_once` with the GIL + py.allow_threads(|| { + self.normalized.call_once(|| { + self.normalizing_thread + .lock() + .unwrap() + .replace(std::thread::current().id()); + + // Safety: no other thread can access the inner value while we are normalizing it. + let state = unsafe { + (*self.inner.get()) + .take() + .expect("Cannot normalize a PyErr while already normalizing it.") + }; + + let normalized_state = + Python::with_gil(|py| PyErrStateInner::Normalized(state.normalize(py))); + + // Safety: no other thread can access the inner value while we are normalizing it. + unsafe { + *self.inner.get() = Some(normalized_state); + } + }) + }); - unsafe { - let self_state = &mut *self.inner.get(); - *self_state = Some(PyErrStateInner::Normalized(state.normalize(py))); - match self_state { - Some(PyErrStateInner::Normalized(n)) => n, - _ => unreachable!(), - } + match unsafe { + // Safety: self.inner will never be written again once normalized. + &*self.inner.get() + } { + Some(PyErrStateInner::Normalized(n)) => n, + _ => unreachable!(), } } } @@ -321,3 +355,80 @@ fn raise_lazy(py: Python<'_>, lazy: Box) { } } } + +#[cfg(test)] +mod tests { + + use crate::{ + exceptions::PyValueError, sync::GILOnceCell, PyErr, PyErrArguments, PyObject, Python, + }; + + #[test] + #[should_panic(expected = "Re-entrant normalization of PyErrState detected")] + fn test_reentrant_normalization() { + static ERR: GILOnceCell = GILOnceCell::new(); + + struct RecursiveArgs; + + impl PyErrArguments for RecursiveArgs { + fn arguments(self, py: Python<'_>) -> PyObject { + // .value(py) triggers normalization + ERR.get(py) + .expect("is set just below") + .value(py) + .clone() + .into() + } + } + + Python::with_gil(|py| { + ERR.set(py, PyValueError::new_err(RecursiveArgs)).unwrap(); + ERR.get(py).expect("is set just above").value(py); + }) + } + + #[test] + #[cfg(not(target_arch = "wasm32"))] // We are building wasm Python with pthreads disabled + fn test_no_deadlock_thread_switch() { + static ERR: GILOnceCell = GILOnceCell::new(); + + struct GILSwitchArgs; + + impl PyErrArguments for GILSwitchArgs { + fn arguments(self, py: Python<'_>) -> PyObject { + // releasing the GIL potentially allows for other threads to deadlock + // with the normalization going on here + py.allow_threads(|| { + std::thread::sleep(std::time::Duration::from_millis(10)); + }); + py.None() + } + } + + Python::with_gil(|py| ERR.set(py, PyValueError::new_err(GILSwitchArgs)).unwrap()); + + // Let many threads attempt to read the normalized value at the same time + let handles = (0..10) + .map(|_| { + std::thread::spawn(|| { + Python::with_gil(|py| { + ERR.get(py).expect("is set just above").value(py); + }); + }) + }) + .collect::>(); + + for handle in handles { + handle.join().unwrap(); + } + + // We should never have deadlocked, and should be able to run + // this assertion + Python::with_gil(|py| { + assert!(ERR + .get(py) + .expect("is set above") + .is_instance_of::(py)) + }); + } +} From 76f4503aec1804f430cd4fc690fadb859b0052a0 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Tue, 5 Nov 2024 12:34:33 -0700 Subject: [PATCH 360/495] Declare free-threaded support for PyModule (#4588) * WIP: declare free-threaded support in pymodule macro * ignore ruff lint about unused import * eliminate gil re-enabling in pytests * fix clippy nit * fix return type of PyUnstable_Module_SetGIL binding * add a way to declare free-threaded support without macros * fix ruff * fix changed ui test answer * fix build issues on old python versions * fix runtime warnings in examples * ensure that the GIL does not get re-enabled in the pytests * add changelog entry * fix ruff * fix compiler error on older pythons * fix clippy * really fix clippy and expose supports_free_threaded on all builds * fix clippy and msrv * fix examples on gil-disabled python * fix free-threaded clippy * fix unused import in example * Add pyo3-build-config as a build dependency to examples that need it * add docs * add rust tests so coverage picks up the new code * fix some formatting issues * Apply cleanups * fix cargo fmt --check * revert changes to non-FFI examples * apply David's suggestion for the guide * link to raw FFI examples in the guide * fix config guards in moduleobject.rs * rename supports_free_threaded to gil_used * remove ensure_gil_enabled from pyo3-ffi/build.rs * update docs for PyModule::gil_used * remove UNSAFE_PYO3_BUILD_FREE_THREADED from the CI config * fix merge conflict screwup * fix nox -s test-py * fix guide links * remove redundant pytest test * fix issue with wrap_pymodule not respecting user choice for GIL support * replace map.unwrap_or with map_or * fix refcounting error in ffi example --- .github/workflows/ci.yml | 2 - guide/src/free-threading.md | 86 ++++++++++++++++++++-- newsfragments/4588.added.md | 3 + noxfile.py | 8 -- pyo3-ffi/build.rs | 37 +++------- pyo3-ffi/examples/sequential/Cargo.toml | 3 + pyo3-ffi/examples/sequential/build.rs | 3 + pyo3-ffi/examples/sequential/src/module.rs | 5 ++ pyo3-ffi/examples/string-sum/Cargo.toml | 3 + pyo3-ffi/examples/string-sum/build.rs | 3 + pyo3-ffi/examples/string-sum/src/lib.rs | 13 +++- pyo3-ffi/src/moduleobject.rs | 14 +++- pyo3-macros-backend/src/attributes.rs | 4 +- pyo3-macros-backend/src/module.rs | 33 +++++++-- pytests/conftest.py | 22 ++++++ pytests/src/awaitable.rs | 2 +- pytests/src/buf_and_str.rs | 2 +- pytests/src/comparisons.rs | 2 +- pytests/src/datetime.rs | 2 +- pytests/src/enums.rs | 2 +- pytests/src/lib.rs | 2 +- pytests/src/misc.rs | 2 +- pytests/src/objstore.rs | 2 +- pytests/src/othermod.rs | 2 +- pytests/src/path.rs | 2 +- pytests/src/pyclasses.rs | 2 +- pytests/src/pyfunctions.rs | 2 +- pytests/src/sequence.rs | 2 +- pytests/src/subclassing.rs | 2 +- src/impl_/pymodule.rs | 33 +++++++-- src/macros.rs | 2 +- src/types/module.rs | 53 +++++++++++++ tests/test_module.rs | 4 +- tests/ui/invalid_pymodule_args.stderr | 2 +- 34 files changed, 288 insertions(+), 73 deletions(-) create mode 100644 newsfragments/4588.added.md create mode 100644 pyo3-ffi/examples/sequential/build.rs create mode 100644 pyo3-ffi/examples/string-sum/build.rs create mode 100644 pytests/conftest.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dcc4dd8bf0b..eba8676f01d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -553,8 +553,6 @@ jobs: test-free-threaded: needs: [fmt] runs-on: ubuntu-latest - env: - UNSAFE_PYO3_BUILD_FREE_THREADED: 1 steps: - uses: actions/checkout@v4 - uses: Swatinem/rust-cache@v2 diff --git a/guide/src/free-threading.md b/guide/src/free-threading.md index 6d3493d2db6..b466269c4e3 100644 --- a/guide/src/free-threading.md +++ b/guide/src/free-threading.md @@ -38,11 +38,83 @@ native Python runtime by building on the Rust [`Send` and `Sync`](https://doc.rust-lang.org/nomicon/send-and-sync.html) traits. This document provides advice for porting Rust code using PyO3 to run under -free-threaded Python. While many simple PyO3 uses, like defining an immutable -Python class, will likely work "out of the box", there are currently some -limitations. +free-threaded Python. + +## Supporting free-threaded Python with PyO3 + +Many simple uses of PyO3, like exposing bindings for a "pure" Rust function +with no side-effects or defining an immutable Python class, will likely work +"out of the box" on the free-threaded build. All that will be necessary is to +annotate Python modules declared by rust code in your project to declare that +they support free-threaded Python, for example by declaring the module with +`#[pymodule(gil_used = false)]`. + +At a low-level, annotating a module sets the `Py_MOD_GIL` slot on modules +defined by an extension to `Py_MOD_GIL_NOT_USED`, which allows the interpreter +to see at runtime that the author of the extension thinks the extension is +thread-safe. You should only do this if you know that your extension is +thread-safe. Because of Rust's guarantees, this is already true for many +extensions, however see below for more discussion about how to evaluate the +thread safety of existing Rust extensions and how to think about the PyO3 API +using a Python runtime with no GIL. + +If you do not explicitly mark that modules are thread-safe, the Python +interpreter will re-enable the GIL at runtime and print a `RuntimeWarning` +explaining which module caused it to re-enable the GIL. You can also force the +GIL to remain disabled by setting the `PYTHON_GIL=0` as an environment variable +or passing `-Xgil=0` when starting Python (`0` means the GIL is turned off). + +If you are sure that all data structures exposed in a `PyModule` are +thread-safe, then pass `gil_used = false` as a parameter to the +`pymodule` procedural macro declaring the module or call +`PyModule::gil_used` on a `PyModule` instance. For example: -## Many symbols exposed by PyO3 have `GIL` in the name +```rust +use pyo3::prelude::*; + +/// This module supports free-threaded Python +#[pymodule(gil_used = false)] +fn my_extension(m: &Bound<'_, PyModule>) -> PyResult<()> { + // add members to the module that you know are thread-safe + Ok(()) +} +``` + +Or for a module that is set up without using the `pymodule` macro: + +```rust +use pyo3::prelude::*; + +# #[allow(dead_code)] +fn register_child_module(parent_module: &Bound<'_, PyModule>) -> PyResult<()> { + let child_module = PyModule::new(parent_module.py(), "child_module")?; + child_module.gil_used(false)?; + parent_module.add_submodule(&child_module) +} + +``` + +See the +[`string-sum`](https://github.com/PyO3/pyo3/tree/main/pyo3-ffi/examples/string-sum) +example for how to declare free-threaded support using raw FFI calls for modules +using single-phase initialization and the +[`sequential`](https://github.com/PyO3/pyo3/tree/main/pyo3-ffi/examples/sequential) +example for modules using multi-phase initialization. + +## Special considerations for the free-threaded build + +The free-threaded interpreter does not have a GIL, and this can make interacting +with the PyO3 API confusing, since the API was originally designed around strong +assumptions about the GIL providing locking. Additionally, since the GIL +provided locking for operations on Python objects, many existing extensions that +provide mutable data structures relied on the GIL to make interior mutability +thread-safe. + +Working with PyO3 under the free-threaded interpreter therefore requires some +additional care and mental overhead compared with a GIL-enabled interpreter. We +discuss how to handle this below. + +### Many symbols exposed by PyO3 have `GIL` in the name We are aware that there are some naming issues in the PyO3 API that make it awkward to think about a runtime environment where there is no GIL. We plan to @@ -87,7 +159,7 @@ garbage collector can only run if all threads are detached from the runtime (in a stop-the-world state), so detaching from the runtime allows freeing unused memory. -## Exceptions and panics for multithreaded access of mutable `pyclass` instances +### Exceptions and panics for multithreaded access of mutable `pyclass` instances Data attached to `pyclass` instances is protected from concurrent access by a `RefCell`-like pattern of runtime borrow checking. Like a `RefCell`, PyO3 will @@ -104,7 +176,7 @@ The most straightforward way to trigger this problem to use the Python [`pyclass`]({{#PYO3_DOCS_URL}}/pyo3/attr.pyclass.html). For example, consider the following implementation: -``` +```rust # use pyo3::prelude::*; # fn main() { #[pyclass] @@ -211,7 +283,7 @@ Python::with_gil(|py| { # } ``` -## `GILProtected` is not exposed +### `GILProtected` is not exposed [`GILProtected`] is a PyO3 type that allows mutable access to static data by leveraging the GIL to lock concurrent access from other threads. In diff --git a/newsfragments/4588.added.md b/newsfragments/4588.added.md new file mode 100644 index 00000000000..42b5b8e219a --- /dev/null +++ b/newsfragments/4588.added.md @@ -0,0 +1,3 @@ +* It is now possible to declare that a module supports the free-threaded build + by either calling `PyModule::gil_used` or passing + `gil_used = false` as a parameter to the `pymodule` proc macro. diff --git a/noxfile.py b/noxfile.py index 32176240f59..25cb8d1eb92 100644 --- a/noxfile.py +++ b/noxfile.py @@ -676,14 +676,6 @@ def test_version_limits(session: nox.Session): config_file.set("PyPy", "3.11") _run_cargo(session, "check", env=env, expect_error=True) - # Python build with GIL disabled should fail building - config_file.set("CPython", "3.13", build_flags=["Py_GIL_DISABLED"]) - _run_cargo(session, "check", env=env, expect_error=True) - - # Python build with GIL disabled should pass with env flag on - env["UNSAFE_PYO3_BUILD_FREE_THREADED"] = "1" - _run_cargo(session, "check", env=env) - @nox.session(name="check-feature-powerset", venv_backend="none") def check_feature_powerset(session: nox.Session): diff --git a/pyo3-ffi/build.rs b/pyo3-ffi/build.rs index 622c2707110..931838b5e5d 100644 --- a/pyo3-ffi/build.rs +++ b/pyo3-ffi/build.rs @@ -6,7 +6,6 @@ use pyo3_build_config::{ }, warn, BuildFlag, PythonImplementation, }; -use std::ops::Not; /// Minimum Python version PyO3 supports. struct SupportedVersions { @@ -107,7 +106,17 @@ fn ensure_python_version(interpreter_config: &InterpreterConfig) -> Result<()> { if interpreter_config.abi3 { match interpreter_config.implementation { - PythonImplementation::CPython => {} + PythonImplementation::CPython => { + if interpreter_config + .build_flags + .0 + .contains(&BuildFlag::Py_GIL_DISABLED) + { + warn!( + "The free-threaded build of CPython does not yet support abi3 so the build artifacts will be version-specific." + ) + } + } PythonImplementation::PyPy => warn!( "PyPy does not yet support abi3 so the build artifacts will be version-specific. \ See https://github.com/pypy/pypy/issues/3397 for more information." @@ -121,29 +130,6 @@ fn ensure_python_version(interpreter_config: &InterpreterConfig) -> Result<()> { Ok(()) } -fn ensure_gil_enabled(interpreter_config: &InterpreterConfig) -> Result<()> { - let gil_enabled = interpreter_config - .build_flags - .0 - .contains(&BuildFlag::Py_GIL_DISABLED) - .not(); - ensure!( - gil_enabled || std::env::var("UNSAFE_PYO3_BUILD_FREE_THREADED").map_or(false, |os_str| os_str == "1"), - "the Python interpreter was built with the GIL disabled, which is not yet supported by PyO3\n\ - = help: see https://github.com/PyO3/pyo3/issues/4265 for more information\n\ - = help: please check if an updated version of PyO3 is available. Current version: {}\n\ - = help: set UNSAFE_PYO3_BUILD_FREE_THREADED=1 to suppress this check and build anyway for free-threaded Python", - std::env::var("CARGO_PKG_VERSION").unwrap() - ); - if !gil_enabled && interpreter_config.abi3 { - warn!( - "The free-threaded build of CPython does not yet support abi3 so the build artifacts will be version-specific." - ) - } - - Ok(()) -} - fn ensure_target_pointer_width(interpreter_config: &InterpreterConfig) -> Result<()> { if let Some(pointer_width) = interpreter_config.pointer_width { // Try to check whether the target architecture matches the python library @@ -209,7 +195,6 @@ fn configure_pyo3() -> Result<()> { ensure_python_version(&interpreter_config)?; ensure_target_pointer_width(&interpreter_config)?; - ensure_gil_enabled(&interpreter_config)?; // Serialize the whole interpreter config into DEP_PYTHON_PYO3_CONFIG env var. interpreter_config.to_cargo_dep_env()?; diff --git a/pyo3-ffi/examples/sequential/Cargo.toml b/pyo3-ffi/examples/sequential/Cargo.toml index 3348595b4e9..288eb1ba326 100644 --- a/pyo3-ffi/examples/sequential/Cargo.toml +++ b/pyo3-ffi/examples/sequential/Cargo.toml @@ -10,4 +10,7 @@ crate-type = ["cdylib", "lib"] [dependencies] pyo3-ffi = { path = "../../", features = ["extension-module"] } +[build-dependencies] +pyo3-build-config = { path = "../../../pyo3-build-config" } + [workspace] diff --git a/pyo3-ffi/examples/sequential/build.rs b/pyo3-ffi/examples/sequential/build.rs new file mode 100644 index 00000000000..0475124bb4e --- /dev/null +++ b/pyo3-ffi/examples/sequential/build.rs @@ -0,0 +1,3 @@ +fn main() { + pyo3_build_config::use_pyo3_cfgs(); +} diff --git a/pyo3-ffi/examples/sequential/src/module.rs b/pyo3-ffi/examples/sequential/src/module.rs index 5e71f07a865..baa7c66f206 100644 --- a/pyo3-ffi/examples/sequential/src/module.rs +++ b/pyo3-ffi/examples/sequential/src/module.rs @@ -23,6 +23,11 @@ static mut SEQUENTIAL_SLOTS: &[PyModuleDef_Slot] = &[ slot: Py_mod_multiple_interpreters, value: Py_MOD_PER_INTERPRETER_GIL_SUPPORTED, }, + #[cfg(Py_GIL_DISABLED)] + PyModuleDef_Slot { + slot: Py_mod_gil, + value: Py_MOD_GIL_NOT_USED, + }, PyModuleDef_Slot { slot: 0, value: ptr::null_mut(), diff --git a/pyo3-ffi/examples/string-sum/Cargo.toml b/pyo3-ffi/examples/string-sum/Cargo.toml index 6fb72141cdc..3c9893b3e8a 100644 --- a/pyo3-ffi/examples/string-sum/Cargo.toml +++ b/pyo3-ffi/examples/string-sum/Cargo.toml @@ -10,4 +10,7 @@ crate-type = ["cdylib"] [dependencies] pyo3-ffi = { path = "../../", features = ["extension-module"] } +[build-dependencies] +pyo3-build-config = { path = "../../../pyo3-build-config" } + [workspace] diff --git a/pyo3-ffi/examples/string-sum/build.rs b/pyo3-ffi/examples/string-sum/build.rs new file mode 100644 index 00000000000..0475124bb4e --- /dev/null +++ b/pyo3-ffi/examples/string-sum/build.rs @@ -0,0 +1,3 @@ +fn main() { + pyo3_build_config::use_pyo3_cfgs(); +} diff --git a/pyo3-ffi/examples/string-sum/src/lib.rs b/pyo3-ffi/examples/string-sum/src/lib.rs index 9f0d6c6435f..7af80bc08d7 100644 --- a/pyo3-ffi/examples/string-sum/src/lib.rs +++ b/pyo3-ffi/examples/string-sum/src/lib.rs @@ -32,7 +32,18 @@ static mut METHODS: &[PyMethodDef] = &[ #[allow(non_snake_case)] #[no_mangle] pub unsafe extern "C" fn PyInit_string_sum() -> *mut PyObject { - PyModule_Create(ptr::addr_of_mut!(MODULE_DEF)) + let module = PyModule_Create(ptr::addr_of_mut!(MODULE_DEF)); + if module.is_null() { + return module; + } + #[cfg(Py_GIL_DISABLED)] + { + if PyUnstable_Module_SetGIL(module, Py_MOD_GIL_NOT_USED) < 0 { + Py_DECREF(module); + return std::ptr::null_mut(); + } + } + module } /// A helper to parse function arguments diff --git a/pyo3-ffi/src/moduleobject.rs b/pyo3-ffi/src/moduleobject.rs index ff6458f4b15..2417664a421 100644 --- a/pyo3-ffi/src/moduleobject.rs +++ b/pyo3-ffi/src/moduleobject.rs @@ -88,6 +88,10 @@ pub const Py_mod_create: c_int = 1; pub const Py_mod_exec: c_int = 2; #[cfg(Py_3_12)] pub const Py_mod_multiple_interpreters: c_int = 3; +#[cfg(Py_3_13)] +pub const Py_mod_gil: c_int = 4; + +// skipped private _Py_mod_LAST_SLOT #[cfg(Py_3_12)] pub const Py_MOD_MULTIPLE_INTERPRETERS_NOT_SUPPORTED: *mut c_void = 0 as *mut c_void; @@ -96,7 +100,15 @@ pub const Py_MOD_MULTIPLE_INTERPRETERS_SUPPORTED: *mut c_void = 1 as *mut c_void #[cfg(Py_3_12)] pub const Py_MOD_PER_INTERPRETER_GIL_SUPPORTED: *mut c_void = 2 as *mut c_void; -// skipped non-limited _Py_mod_LAST_SLOT +#[cfg(Py_3_13)] +pub const Py_MOD_GIL_USED: *mut c_void = 0 as *mut c_void; +#[cfg(Py_3_13)] +pub const Py_MOD_GIL_NOT_USED: *mut c_void = 1 as *mut c_void; + +#[cfg(all(not(Py_LIMITED_API), Py_GIL_DISABLED))] +extern "C" { + pub fn PyUnstable_Module_SetGIL(module: *mut PyObject, gil: *mut c_void) -> c_int; +} #[repr(C)] pub struct PyModuleDef { diff --git a/pyo3-macros-backend/src/attributes.rs b/pyo3-macros-backend/src/attributes.rs index 94526e7dafc..6fe75e44302 100644 --- a/pyo3-macros-backend/src/attributes.rs +++ b/pyo3-macros-backend/src/attributes.rs @@ -7,7 +7,7 @@ use syn::{ punctuated::Punctuated, spanned::Spanned, token::Comma, - Attribute, Expr, ExprPath, Ident, Index, LitStr, Member, Path, Result, Token, + Attribute, Expr, ExprPath, Ident, Index, LitBool, LitStr, Member, Path, Result, Token, }; pub mod kw { @@ -44,6 +44,7 @@ pub mod kw { syn::custom_keyword!(transparent); syn::custom_keyword!(unsendable); syn::custom_keyword!(weakref); + syn::custom_keyword!(gil_used); } fn take_int(read: &mut &str, tracker: &mut usize) -> String { @@ -308,6 +309,7 @@ pub type RenameAllAttribute = KeywordAttribute; pub type TextSignatureAttribute = KeywordAttribute; pub type SubmoduleAttribute = kw::submodule; +pub type GILUsedAttribute = KeywordAttribute; impl Parse for KeywordAttribute { fn parse(input: ParseStream<'_>) -> Result { diff --git a/pyo3-macros-backend/src/module.rs b/pyo3-macros-backend/src/module.rs index 5aaf7740461..d9fac3cbd7b 100644 --- a/pyo3-macros-backend/src/module.rs +++ b/pyo3-macros-backend/src/module.rs @@ -2,8 +2,8 @@ use crate::{ attributes::{ - self, kw, take_attributes, take_pyo3_options, CrateAttribute, ModuleAttribute, - NameAttribute, SubmoduleAttribute, + self, kw, take_attributes, take_pyo3_options, CrateAttribute, GILUsedAttribute, + ModuleAttribute, NameAttribute, SubmoduleAttribute, }, get_doc, pyclass::PyClassPyO3Option, @@ -29,6 +29,7 @@ pub struct PyModuleOptions { name: Option, module: Option, submodule: Option, + gil_used: Option, } impl Parse for PyModuleOptions { @@ -72,6 +73,9 @@ impl PyModuleOptions { submodule, " (it is implicitly always specified for nested modules)" ), + PyModulePyO3Option::GILUsed(gil_used) => { + set_option!(gil_used) + } } } Ok(()) @@ -344,7 +348,13 @@ pub fn pymodule_module_impl( ) } }}; - let initialization = module_initialization(&name, ctx, module_def, options.submodule.is_some()); + let initialization = module_initialization( + &name, + ctx, + module_def, + options.submodule.is_some(), + options.gil_used.map_or(true, |op| op.value.value), + ); Ok(quote!( #(#attrs)* @@ -383,7 +393,13 @@ pub fn pymodule_function_impl( let vis = &function.vis; let doc = get_doc(&function.attrs, None, ctx); - let initialization = module_initialization(&name, ctx, quote! { MakeDef::make_def() }, false); + let initialization = module_initialization( + &name, + ctx, + quote! { MakeDef::make_def() }, + false, + options.gil_used.map_or(true, |op| op.value.value), + ); // Module function called with optional Python<'_> marker as first arg, followed by the module. let mut module_args = Vec::new(); @@ -428,6 +444,7 @@ fn module_initialization( ctx: &Ctx, module_def: TokenStream, is_submodule: bool, + gil_used: bool, ) -> TokenStream { let Ctx { pyo3_path, .. } = ctx; let pyinit_symbol = format!("PyInit_{}", name); @@ -441,6 +458,9 @@ fn module_initialization( pub(super) struct MakeDef; #[doc(hidden)] pub static _PYO3_DEF: #pyo3_path::impl_::pymodule::ModuleDef = #module_def; + #[doc(hidden)] + // so wrapped submodules can see what gil_used is + pub static __PYO3_GIL_USED: bool = #gil_used; }; if !is_submodule { result.extend(quote! { @@ -449,7 +469,7 @@ fn module_initialization( #[doc(hidden)] #[export_name = #pyinit_symbol] pub unsafe extern "C" fn __pyo3_init() -> *mut #pyo3_path::ffi::PyObject { - unsafe { #pyo3_path::impl_::trampoline::module_init(|py| _PYO3_DEF.make_module(py)) } + unsafe { #pyo3_path::impl_::trampoline::module_init(|py| _PYO3_DEF.make_module(py, #gil_used)) } } }); } @@ -596,6 +616,7 @@ enum PyModulePyO3Option { Crate(CrateAttribute), Name(NameAttribute), Module(ModuleAttribute), + GILUsed(GILUsedAttribute), } impl Parse for PyModulePyO3Option { @@ -609,6 +630,8 @@ impl Parse for PyModulePyO3Option { input.parse().map(PyModulePyO3Option::Module) } else if lookahead.peek(attributes::kw::submodule) { input.parse().map(PyModulePyO3Option::Submodule) + } else if lookahead.peek(attributes::kw::gil_used) { + input.parse().map(PyModulePyO3Option::GILUsed) } else { Err(lookahead.error()) } diff --git a/pytests/conftest.py b/pytests/conftest.py new file mode 100644 index 00000000000..ce729689355 --- /dev/null +++ b/pytests/conftest.py @@ -0,0 +1,22 @@ +import sysconfig +import sys +import pytest + +FREE_THREADED_BUILD = bool(sysconfig.get_config_var("Py_GIL_DISABLED")) + +gil_enabled_at_start = True +if FREE_THREADED_BUILD: + gil_enabled_at_start = sys._is_gil_enabled() + + +def pytest_terminal_summary(terminalreporter, exitstatus, config): + if FREE_THREADED_BUILD and not gil_enabled_at_start and sys._is_gil_enabled(): + tr = terminalreporter + tr.ensure_newline() + tr.section("GIL re-enabled", sep="=", red=True, bold=True) + tr.line("The GIL was re-enabled at runtime during the tests.") + tr.line("") + tr.line("Please ensure all new modules declare support for running") + tr.line("without the GIL. Any new tests that intentionally imports ") + tr.line("code that re-enables the GIL should do so in a subprocess.") + pytest.exit("GIL re-enabled during tests", returncode=1) diff --git a/pytests/src/awaitable.rs b/pytests/src/awaitable.rs index 5e3b98e14ea..fb04c33ed05 100644 --- a/pytests/src/awaitable.rs +++ b/pytests/src/awaitable.rs @@ -78,7 +78,7 @@ impl FutureAwaitable { } } -#[pymodule] +#[pymodule(gil_used = false)] pub fn awaitable(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_class::()?; diff --git a/pytests/src/buf_and_str.rs b/pytests/src/buf_and_str.rs index bbaad40f312..15230a5e153 100644 --- a/pytests/src/buf_and_str.rs +++ b/pytests/src/buf_and_str.rs @@ -47,7 +47,7 @@ fn return_memoryview(py: Python<'_>) -> PyResult> { PyMemoryView::from(&bytes) } -#[pymodule] +#[pymodule(gil_used = false)] pub fn buf_and_str(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_function(wrap_pyfunction!(return_memoryview, m)?)?; diff --git a/pytests/src/comparisons.rs b/pytests/src/comparisons.rs index 5c7f659c9b3..4ed79e42790 100644 --- a/pytests/src/comparisons.rs +++ b/pytests/src/comparisons.rs @@ -112,7 +112,7 @@ impl OrderedDefaultNe { } } -#[pymodule] +#[pymodule(gil_used = false)] pub fn comparisons(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_class::()?; diff --git a/pytests/src/datetime.rs b/pytests/src/datetime.rs index 3bdf103b62e..5162b3508a5 100644 --- a/pytests/src/datetime.rs +++ b/pytests/src/datetime.rs @@ -203,7 +203,7 @@ impl TzClass { } } -#[pymodule] +#[pymodule(gil_used = false)] pub fn datetime(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(make_date, m)?)?; m.add_function(wrap_pyfunction!(get_date_tuple, m)?)?; diff --git a/pytests/src/enums.rs b/pytests/src/enums.rs index fb96c0a9366..8652321700a 100644 --- a/pytests/src/enums.rs +++ b/pytests/src/enums.rs @@ -4,7 +4,7 @@ use pyo3::{ wrap_pyfunction, Bound, PyResult, }; -#[pymodule] +#[pymodule(gil_used = false)] pub fn enums(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_class::()?; diff --git a/pytests/src/lib.rs b/pytests/src/lib.rs index 72f5feaa0f4..b6c32230dac 100644 --- a/pytests/src/lib.rs +++ b/pytests/src/lib.rs @@ -17,7 +17,7 @@ pub mod pyfunctions; pub mod sequence; pub mod subclassing; -#[pymodule] +#[pymodule(gil_used = false)] fn pyo3_pytests(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_wrapped(wrap_pymodule!(awaitable::awaitable))?; #[cfg(not(Py_LIMITED_API))] diff --git a/pytests/src/misc.rs b/pytests/src/misc.rs index ed9c9333ec2..e44d1aa0ecf 100644 --- a/pytests/src/misc.rs +++ b/pytests/src/misc.rs @@ -32,7 +32,7 @@ fn get_item_and_run_callback(dict: Bound<'_, PyDict>, callback: Bound<'_, PyAny> Ok(()) } -#[pymodule] +#[pymodule(gil_used = false)] pub fn misc(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(issue_219, m)?)?; m.add_function(wrap_pyfunction!(get_type_fully_qualified_name, m)?)?; diff --git a/pytests/src/objstore.rs b/pytests/src/objstore.rs index 844cee946ad..8e729052992 100644 --- a/pytests/src/objstore.rs +++ b/pytests/src/objstore.rs @@ -18,7 +18,7 @@ impl ObjStore { } } -#[pymodule] +#[pymodule(gil_used = false)] pub fn objstore(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::() } diff --git a/pytests/src/othermod.rs b/pytests/src/othermod.rs index 36ad4b5e23e..0de912d7d04 100644 --- a/pytests/src/othermod.rs +++ b/pytests/src/othermod.rs @@ -28,7 +28,7 @@ fn double(x: i32) -> i32 { x * 2 } -#[pymodule] +#[pymodule(gil_used = false)] pub fn othermod(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(double, m)?)?; diff --git a/pytests/src/path.rs b/pytests/src/path.rs index 0675e56d13a..b52c038ed34 100644 --- a/pytests/src/path.rs +++ b/pytests/src/path.rs @@ -11,7 +11,7 @@ fn take_pathbuf(path: PathBuf) -> PathBuf { path } -#[pymodule] +#[pymodule(gil_used = false)] pub fn path(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(make_path, m)?)?; m.add_function(wrap_pyfunction!(take_pathbuf, m)?)?; diff --git a/pytests/src/pyclasses.rs b/pytests/src/pyclasses.rs index 4e52dbc8712..3af08c053cc 100644 --- a/pytests/src/pyclasses.rs +++ b/pytests/src/pyclasses.rs @@ -104,7 +104,7 @@ impl ClassWithDict { } } -#[pymodule] +#[pymodule(gil_used = false)] pub fn pyclasses(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_class::()?; diff --git a/pytests/src/pyfunctions.rs b/pytests/src/pyfunctions.rs index 77496198bb9..024641d3d2e 100644 --- a/pytests/src/pyfunctions.rs +++ b/pytests/src/pyfunctions.rs @@ -67,7 +67,7 @@ fn args_kwargs<'py>( (args, kwargs) } -#[pymodule] +#[pymodule(gil_used = false)] pub fn pyfunctions(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(none, m)?)?; m.add_function(wrap_pyfunction!(simple, m)?)?; diff --git a/pytests/src/sequence.rs b/pytests/src/sequence.rs index f552b4048b8..175f5fba8aa 100644 --- a/pytests/src/sequence.rs +++ b/pytests/src/sequence.rs @@ -16,7 +16,7 @@ fn vec_to_vec_pystring(vec: Vec>) -> Vec vec } -#[pymodule] +#[pymodule(gil_used = false)] pub fn sequence(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(vec_to_vec_i32, m)?)?; m.add_function(wrap_pyfunction!(array_to_array_i32, m)?)?; diff --git a/pytests/src/subclassing.rs b/pytests/src/subclassing.rs index 8e451cd9183..0f00e74c19d 100644 --- a/pytests/src/subclassing.rs +++ b/pytests/src/subclassing.rs @@ -17,7 +17,7 @@ impl Subclassable { } } -#[pymodule] +#[pymodule(gil_used = false)] pub fn subclassing(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; Ok(()) diff --git a/src/impl_/pymodule.rs b/src/impl_/pymodule.rs index da17fe4bbdc..08b1ead7584 100644 --- a/src/impl_/pymodule.rs +++ b/src/impl_/pymodule.rs @@ -8,17 +8,20 @@ use std::{cell::UnsafeCell, ffi::CStr, marker::PhantomData}; not(all(windows, Py_LIMITED_API, not(Py_3_10))), not(target_has_atomic = "64"), ))] -use portable_atomic::{AtomicI64, Ordering}; +use portable_atomic::AtomicI64; #[cfg(all( not(any(PyPy, GraalPy)), Py_3_9, not(all(windows, Py_LIMITED_API, not(Py_3_10))), target_has_atomic = "64", ))] -use std::sync::atomic::{AtomicI64, Ordering}; +use std::sync::atomic::AtomicI64; +use std::sync::atomic::{AtomicBool, Ordering}; #[cfg(not(any(PyPy, GraalPy)))] use crate::exceptions::PyImportError; +#[cfg(all(not(Py_LIMITED_API), Py_GIL_DISABLED))] +use crate::PyErr; use crate::{ ffi, impl_::pymethods::PyMethodDef, @@ -41,6 +44,8 @@ pub struct ModuleDef { interpreter: AtomicI64, /// Initialized module object, cached to avoid reinitialization. module: GILOnceCell>, + /// Whether or not the module supports running without the GIL + gil_used: AtomicBool, } /// Wrapper to enable initializer to be used in const fns. @@ -85,10 +90,12 @@ impl ModuleDef { ))] interpreter: AtomicI64::new(-1), module: GILOnceCell::new(), + gil_used: AtomicBool::new(true), } } /// Builds a module using user given initializer. Used for [`#[pymodule]`][crate::pymodule]. - pub fn make_module(&'static self, py: Python<'_>) -> PyResult> { + #[cfg_attr(any(Py_LIMITED_API, not(Py_GIL_DISABLED)), allow(unused_variables))] + pub fn make_module(&'static self, py: Python<'_>, gil_used: bool) -> PyResult> { // Check the interpreter ID has not changed, since we currently have no way to guarantee // that static data is not reused across interpreters. // @@ -134,6 +141,19 @@ impl ModuleDef { ffi::PyModule_Create(self.ffi_def.get()), )? }; + #[cfg(all(not(Py_LIMITED_API), Py_GIL_DISABLED))] + { + let gil_used_ptr = { + if gil_used { + ffi::Py_MOD_GIL_USED + } else { + ffi::Py_MOD_GIL_NOT_USED + } + }; + if unsafe { ffi::PyUnstable_Module_SetGIL(module.as_ptr(), gil_used_ptr) } < 0 { + return Err(PyErr::fetch(py)); + } + } self.initializer.0(module.bind(py))?; Ok(module) }) @@ -190,7 +210,10 @@ impl PyAddToModule for PyMethodDef { /// For adding a module to a module. impl PyAddToModule for ModuleDef { fn add_to_module(&'static self, module: &Bound<'_, PyModule>) -> PyResult<()> { - module.add_submodule(self.make_module(module.py())?.bind(module.py())) + module.add_submodule( + self.make_module(module.py(), self.gil_used.load(Ordering::Relaxed))? + .bind(module.py()), + ) } } @@ -223,7 +246,7 @@ mod tests { ) }; Python::with_gil(|py| { - let module = MODULE_DEF.make_module(py).unwrap().into_bound(py); + let module = MODULE_DEF.make_module(py, false).unwrap().into_bound(py); assert_eq!( module .getattr("__name__") diff --git a/src/macros.rs b/src/macros.rs index d2fa6f31ada..53fcbcaad3d 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -176,7 +176,7 @@ macro_rules! wrap_pymodule { &|py| { use $module as wrapped_pymodule; wrapped_pymodule::_PYO3_DEF - .make_module(py) + .make_module(py, wrapped_pymodule::__PYO3_GIL_USED) .expect("failed to wrap pymodule") } }; diff --git a/src/types/module.rs b/src/types/module.rs index f3490385721..d3e59c85198 100644 --- a/src/types/module.rs +++ b/src/types/module.rs @@ -9,6 +9,8 @@ use crate::types::{ }; use crate::{exceptions, ffi, Borrowed, Bound, BoundObject, Py, PyObject, Python}; use std::ffi::{CStr, CString}; +#[cfg(all(not(Py_LIMITED_API), Py_GIL_DISABLED))] +use std::os::raw::c_int; use std::str; /// Represents a Python [`module`][1] object. @@ -385,6 +387,40 @@ pub trait PyModuleMethods<'py>: crate::sealed::Sealed { /// [1]: crate::prelude::pyfunction /// [2]: crate::wrap_pyfunction fn add_function(&self, fun: Bound<'_, PyCFunction>) -> PyResult<()>; + + /// Declare whether or not this module supports running with the GIL disabled + /// + /// If the module does not rely on the GIL for thread safety, you can pass + /// `false` to this function to indicate the module does not rely on the GIL + /// for thread-safety. + /// + /// This function sets the [`Py_MOD_GIL` + /// slot](https://docs.python.org/3/c-api/module.html#c.Py_mod_gil) on the + /// module object. The default is `Py_MOD_GIL_USED`, so passing `true` to + /// this function is a no-op unless you have already set `Py_MOD_GIL` to + /// `Py_MOD_GIL_NOT_USED` elsewhere. + /// + /// # Examples + /// + /// ```rust + /// use pyo3::prelude::*; + /// + /// #[pymodule(gil_used = false)] + /// fn my_module(py: Python<'_>, module: &Bound<'_, PyModule>) -> PyResult<()> { + /// let submodule = PyModule::new(py, "submodule")?; + /// submodule.gil_used(false)?; + /// module.add_submodule(&submodule)?; + /// Ok(()) + /// } + /// ``` + /// + /// The resulting module will not print a `RuntimeWarning` and re-enable the + /// GIL when Python imports it on the free-threaded build, since all module + /// objects defined in the extension have `Py_MOD_GIL` set to + /// `Py_MOD_GIL_NOT_USED`. + /// + /// This is a no-op on the GIL-enabled build. + fn gil_used(&self, gil_used: bool) -> PyResult<()>; } impl<'py> PyModuleMethods<'py> for Bound<'py, PyModule> { @@ -511,6 +547,23 @@ impl<'py> PyModuleMethods<'py> for Bound<'py, PyModule> { let name = fun.getattr(__name__(self.py()))?; self.add(name.downcast_into::()?, fun) } + + #[cfg_attr(any(Py_LIMITED_API, not(Py_GIL_DISABLED)), allow(unused_variables))] + fn gil_used(&self, gil_used: bool) -> PyResult<()> { + #[cfg(all(not(Py_LIMITED_API), Py_GIL_DISABLED))] + { + let gil_used = match gil_used { + true => ffi::Py_MOD_GIL_USED, + false => ffi::Py_MOD_GIL_NOT_USED, + }; + match unsafe { ffi::PyUnstable_Module_SetGIL(self.as_ptr(), gil_used) } { + c_int::MIN..=-1 => Err(PyErr::fetch(self.py())), + 0..=c_int::MAX => Ok(()), + } + } + #[cfg(any(Py_LIMITED_API, not(Py_GIL_DISABLED)))] + Ok(()) + } } fn __all__(py: Python<'_>) -> &Bound<'_, PyString> { diff --git a/tests/test_module.rs b/tests/test_module.rs index 7b97fb3a889..36d21abe9b0 100644 --- a/tests/test_module.rs +++ b/tests/test_module.rs @@ -37,7 +37,7 @@ fn double(x: usize) -> usize { } /// This module is implemented in Rust. -#[pymodule] +#[pymodule(gil_used = false)] fn module_with_functions(m: &Bound<'_, PyModule>) -> PyResult<()> { #[pyfn(m)] #[pyo3(name = "no_parameters")] @@ -182,6 +182,8 @@ fn test_module_from_code_bound() { .extract() .expect("The value should be able to be converted to an i32"); + adder_mod.gil_used(false).expect("Disabling the GIL failed"); + assert_eq!(ret_value, 3); }); } diff --git a/tests/ui/invalid_pymodule_args.stderr b/tests/ui/invalid_pymodule_args.stderr index 261d8115e15..23a5109b4cb 100644 --- a/tests/ui/invalid_pymodule_args.stderr +++ b/tests/ui/invalid_pymodule_args.stderr @@ -1,4 +1,4 @@ -error: expected one of: `name`, `crate`, `module`, `submodule` +error: expected one of: `name`, `crate`, `module`, `submodule`, `gil_used` --> tests/ui/invalid_pymodule_args.rs:3:12 | 3 | #[pymodule(some_arg)] From 1befe1d2584589a9038cfcebc76d0731175c5673 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Tue, 5 Nov 2024 19:44:26 +0000 Subject: [PATCH 361/495] require `#[pyclass]` to be `Sync` (#4566) * require `#[pyclass]` to be `Sync` * silence deprecation warning in pyo3 internals * make 'cargo test' pass (modulo test_getter_setter) * fix nox -s test-py * silence new deprecation warning * use `unsendable` to test cell get / set * add WIP changelog entry * fix wasm clippy * add a test for assert_pyclass_sync to fix coverage error * use a better name * fix cargo doc --lib * fix building with no default features * apply Bruno's wording suggestions * simplify compile-time checking for PyClass being Sync * update discussion around the decorator example in the guide --------- Co-authored-by: Nathan Goldbaum --- examples/decorator/src/lib.rs | 14 ++-- guide/src/class/call.md | 7 +- guide/src/class/protocols.md | 8 ++- guide/src/migration.md | 2 +- newsfragments/4566.changed.md | 5 ++ pyo3-macros-backend/src/pyclass.rs | 14 ++++ src/coroutine.rs | 4 ++ src/impl_/pyclass.rs | 66 ++---------------- src/impl_/pyclass/assertions.rs | 40 +++++++++++ src/impl_/pyclass/probes.rs | 72 +++++++++++++++++++ tests/test_gc.rs | 33 +++++---- tests/test_getter_setter.rs | 2 +- tests/test_proto_methods.rs | 9 +-- tests/ui/pyclass_send.rs | 48 +++++++------ tests/ui/pyclass_send.stderr | 108 +++++++++++++++++++++++++---- 15 files changed, 301 insertions(+), 131 deletions(-) create mode 100644 newsfragments/4566.changed.md create mode 100644 src/impl_/pyclass/assertions.rs create mode 100644 src/impl_/pyclass/probes.rs diff --git a/examples/decorator/src/lib.rs b/examples/decorator/src/lib.rs index 8d257aecb2e..4c5471c9945 100644 --- a/examples/decorator/src/lib.rs +++ b/examples/decorator/src/lib.rs @@ -1,6 +1,6 @@ use pyo3::prelude::*; use pyo3::types::{PyDict, PyTuple}; -use std::cell::Cell; +use std::sync::atomic::{AtomicU64, Ordering}; /// A function decorator that keeps track how often it is called. /// @@ -9,8 +9,8 @@ use std::cell::Cell; pub struct PyCounter { // Keeps track of how many calls have gone through. // - // See the discussion at the end for why `Cell` is used. - count: Cell, + // See the discussion at the end for why `AtomicU64` is used. + count: AtomicU64, // This is the actual function being wrapped. wraps: Py, @@ -26,14 +26,14 @@ impl PyCounter { #[new] fn __new__(wraps: Py) -> Self { PyCounter { - count: Cell::new(0), + count: AtomicU64::new(0), wraps, } } #[getter] fn count(&self) -> u64 { - self.count.get() + self.count.load(Ordering::Relaxed) } #[pyo3(signature = (*args, **kwargs))] @@ -43,9 +43,7 @@ impl PyCounter { args: &Bound<'_, PyTuple>, kwargs: Option<&Bound<'_, PyDict>>, ) -> PyResult> { - let old_count = self.count.get(); - let new_count = old_count + 1; - self.count.set(new_count); + let new_count = self.count.fetch_add(1, Ordering::Relaxed); let name = self.wraps.getattr(py, "__name__")?; println!("{} has been called {} time(s).", name, new_count); diff --git a/guide/src/class/call.md b/guide/src/class/call.md index 0890df9561a..5242a4f41a8 100644 --- a/guide/src/class/call.md +++ b/guide/src/class/call.md @@ -66,7 +66,7 @@ def Counter(wraps): return call ``` -### What is the `Cell` for? +### What is the `AtomicU64` for? A [previous implementation] used a normal `u64`, which meant it required a `&mut self` receiver to update the count: @@ -108,14 +108,15 @@ say_hello() # RuntimeError: Already borrowed ``` -The implementation in this chapter fixes that by never borrowing exclusively; all the methods take `&self` as receivers, of which multiple may exist simultaneously. This requires a shared counter and the easiest way to do that is to use [`Cell`], so that's what is used here. +The implementation in this chapter fixes that by never borrowing exclusively; all the methods take `&self` as receivers, of which multiple may exist simultaneously. This requires a shared counter and the most straightforward way to implement thread-safe interior mutability (e.g. the type does not need to accept `&mut self` to modify the "interior" state) for a `u64` is to use [`AtomicU64`], so that's what is used here. This shows the dangers of running arbitrary Python code - note that "running arbitrary Python code" can be far more subtle than the example above: - Python's asynchronous executor may park the current thread in the middle of Python code, even in Python code that *you* control, and let other Python code run. - Dropping arbitrary Python objects may invoke destructors defined in Python (`__del__` methods). - Calling Python's C-api (most PyO3 apis call C-api functions internally) may raise exceptions, which may allow Python code in signal handlers to run. +- On the free-threaded build, users might use Python's `threading` module to work with your types simultaneously from multiple OS threads. This is especially important if you are writing unsafe code; Python code must never be able to cause undefined behavior. You must ensure that your Rust code is in a consistent state before doing any of the above things. [previous implementation]: https://github.com/PyO3/pyo3/discussions/2598 "Thread Safe Decorator · Discussion #2598 · PyO3/pyo3" -[`Cell`]: https://doc.rust-lang.org/std/cell/struct.Cell.html "Cell in std::cell - Rust" +[`AtomicU64`]: https://doc.rust-lang.org/std/sync/atomic/struct.AtomicU64.html "AtomicU64 in std::sync::atomic - Rust" diff --git a/guide/src/class/protocols.md b/guide/src/class/protocols.md index 4d553c276eb..8a361a1442e 100644 --- a/guide/src/class/protocols.md +++ b/guide/src/class/protocols.md @@ -158,9 +158,11 @@ Example: ```rust use pyo3::prelude::*; +use std::sync::Mutex; + #[pyclass] struct MyIterator { - iter: Box + Send>, + iter: Mutex + Send>>, } #[pymethods] @@ -168,8 +170,8 @@ impl MyIterator { fn __iter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> { slf } - fn __next__(mut slf: PyRefMut<'_, Self>) -> Option { - slf.iter.next() + fn __next__(slf: PyRefMut<'_, Self>) -> Option { + slf.iter.lock().unwrap().next() } } ``` diff --git a/guide/src/migration.md b/guide/src/migration.md index 2cecc73d278..1b8400604cb 100644 --- a/guide/src/migration.md +++ b/guide/src/migration.md @@ -1805,7 +1805,7 @@ There can be two fixes: ``` After: - ```rust + ```rust,ignore # #![allow(dead_code)] use pyo3::prelude::*; use std::sync::{Arc, Mutex}; diff --git a/newsfragments/4566.changed.md b/newsfragments/4566.changed.md new file mode 100644 index 00000000000..2e9db108df1 --- /dev/null +++ b/newsfragments/4566.changed.md @@ -0,0 +1,5 @@ +* The `pyclass` macro now creates a rust type that is `Sync` by default. If you + would like to opt out of this, annotate your class with + `pyclass(unsendable)`. See the migraiton guide entry (INSERT GUIDE LINK HERE) + for more information on updating to accommadate this change. + diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index e44ac890c9c..ae3082e3785 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -2310,7 +2310,21 @@ impl<'a> PyClassImplsBuilder<'a> { } }); + let assertions = if attr.options.unsendable.is_some() { + TokenStream::new() + } else { + quote_spanned! { + cls.span() => + const _: () = { + use #pyo3_path::impl_::pyclass::*; + assert_pyclass_sync::<#cls>(); + }; + } + }; + Ok(quote! { + #assertions + #pyclass_base_type_impl impl #pyo3_path::impl_::pyclass::PyClassImpl for #cls { diff --git a/src/coroutine.rs b/src/coroutine.rs index 56ed58f7460..8fff91dece1 100644 --- a/src/coroutine.rs +++ b/src/coroutine.rs @@ -35,6 +35,10 @@ pub struct Coroutine { waker: Option>, } +// Safety: `Coroutine` is allowed to be `Sync` even though the future is not, +// because the future is polled with `&mut self` receiver +unsafe impl Sync for Coroutine {} + impl Coroutine { /// Wrap a future into a Python coroutine. /// diff --git a/src/impl_/pyclass.rs b/src/impl_/pyclass.rs index 85cf95a9e11..d5f292f9ebd 100644 --- a/src/impl_/pyclass.rs +++ b/src/impl_/pyclass.rs @@ -23,8 +23,13 @@ use std::{ thread, }; +mod assertions; mod lazy_type_object; +mod probes; + +pub use assertions::*; pub use lazy_type_object::LazyTypeObject; +pub use probes::*; /// Gets the offset of the dictionary from the start of the object in bytes. #[inline] @@ -1418,67 +1423,6 @@ impl> } } -/// Trait used to combine with zero-sized types to calculate at compile time -/// some property of a type. -/// -/// The trick uses the fact that an associated constant has higher priority -/// than a trait constant, so we can use the trait to define the false case. -/// -/// The true case is defined in the zero-sized type's impl block, which is -/// gated on some property like trait bound or only being implemented -/// for fixed concrete types. -pub trait Probe { - const VALUE: bool = false; -} - -macro_rules! probe { - ($name:ident) => { - pub struct $name(PhantomData); - impl Probe for $name {} - }; -} - -probe!(IsPyT); - -impl IsPyT> { - pub const VALUE: bool = true; -} - -probe!(IsToPyObject); - -#[allow(deprecated)] -impl IsToPyObject { - pub const VALUE: bool = true; -} - -probe!(IsIntoPy); - -#[allow(deprecated)] -impl> IsIntoPy { - pub const VALUE: bool = true; -} - -probe!(IsIntoPyObjectRef); - -// Possible clippy beta regression, -// see https://github.com/rust-lang/rust-clippy/issues/13578 -#[allow(clippy::extra_unused_lifetimes)] -impl<'a, 'py, T: 'a> IsIntoPyObjectRef -where - &'a T: IntoPyObject<'py>, -{ - pub const VALUE: bool = true; -} - -probe!(IsIntoPyObject); - -impl<'py, T> IsIntoPyObject -where - T: IntoPyObject<'py>, -{ - pub const VALUE: bool = true; -} - /// ensures `obj` is not mutably aliased #[inline] unsafe fn ensure_no_mutable_alias<'py, ClassT: PyClass>( diff --git a/src/impl_/pyclass/assertions.rs b/src/impl_/pyclass/assertions.rs new file mode 100644 index 00000000000..daf2ed5310b --- /dev/null +++ b/src/impl_/pyclass/assertions.rs @@ -0,0 +1,40 @@ +/// Helper function that can be used at compile time to emit a diagnostic if +/// the type does not implement `Sync` when it should. +/// +/// The mere act of invoking this function will cause the diagnostic to be +/// emitted if `T` does not implement `Sync` when it should. +/// +/// The additional `const IS_SYNC: bool` parameter is used to allow the custom +/// diagnostic to be emitted; if `PyClassSync` +#[allow(unused)] +pub const fn assert_pyclass_sync() +where + T: PyClassSync + Sync, +{ +} + +#[cfg_attr( + diagnostic_namespace, + diagnostic::on_unimplemented( + message = "the trait `Sync` is not implemented for `{Self}`", + label = "required by `#[pyclass]`", + note = "replace thread-unsafe fields with thread-safe alternatives", + note = "see for more information", + ) +)] +pub trait PyClassSync {} + +impl PyClassSync for T where T: Sync {} + +mod tests { + #[cfg(feature = "macros")] + #[test] + fn test_assert_pyclass_sync() { + use super::assert_pyclass_sync; + + #[crate::pyclass(crate = "crate")] + struct MyClass {} + + assert_pyclass_sync::(); + } +} diff --git a/src/impl_/pyclass/probes.rs b/src/impl_/pyclass/probes.rs new file mode 100644 index 00000000000..f1c3468cf9b --- /dev/null +++ b/src/impl_/pyclass/probes.rs @@ -0,0 +1,72 @@ +use std::marker::PhantomData; + +use crate::{conversion::IntoPyObject, Py}; +#[allow(deprecated)] +use crate::{IntoPy, ToPyObject}; + +/// Trait used to combine with zero-sized types to calculate at compile time +/// some property of a type. +/// +/// The trick uses the fact that an associated constant has higher priority +/// than a trait constant, so we can use the trait to define the false case. +/// +/// The true case is defined in the zero-sized type's impl block, which is +/// gated on some property like trait bound or only being implemented +/// for fixed concrete types. +pub trait Probe { + const VALUE: bool = false; +} + +macro_rules! probe { + ($name:ident) => { + pub struct $name(PhantomData); + impl Probe for $name {} + }; +} + +probe!(IsPyT); + +impl IsPyT> { + pub const VALUE: bool = true; +} + +probe!(IsToPyObject); + +#[allow(deprecated)] +impl IsToPyObject { + pub const VALUE: bool = true; +} + +probe!(IsIntoPy); + +#[allow(deprecated)] +impl> IsIntoPy { + pub const VALUE: bool = true; +} + +probe!(IsIntoPyObjectRef); + +// Possible clippy beta regression, +// see https://github.com/rust-lang/rust-clippy/issues/13578 +#[allow(clippy::extra_unused_lifetimes)] +impl<'a, 'py, T: 'a> IsIntoPyObjectRef +where + &'a T: IntoPyObject<'py>, +{ + pub const VALUE: bool = true; +} + +probe!(IsIntoPyObject); + +impl<'py, T> IsIntoPyObject +where + T: IntoPyObject<'py>, +{ + pub const VALUE: bool = true; +} + +probe!(IsSync); + +impl IsSync { + pub const VALUE: bool = true; +} diff --git a/tests/test_gc.rs b/tests/test_gc.rs index 9483819c220..4b293449b36 100644 --- a/tests/test_gc.rs +++ b/tests/test_gc.rs @@ -5,10 +5,11 @@ use pyo3::class::PyVisit; use pyo3::ffi; use pyo3::prelude::*; use pyo3::py_run; +#[cfg(not(target_arch = "wasm32"))] use std::cell::Cell; use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::Arc; use std::sync::Once; +use std::sync::{Arc, Mutex}; #[path = "../src/tests/common.rs"] mod common; @@ -403,20 +404,23 @@ fn tries_gil_in_traverse() { fn traverse_cannot_be_hijacked() { #[pyclass] struct HijackedTraverse { - traversed: Cell, - hijacked: Cell, + traversed: AtomicBool, + hijacked: AtomicBool, } impl HijackedTraverse { fn new() -> Self { Self { - traversed: Cell::new(false), - hijacked: Cell::new(false), + traversed: AtomicBool::new(false), + hijacked: AtomicBool::new(false), } } fn traversed_and_hijacked(&self) -> (bool, bool) { - (self.traversed.get(), self.hijacked.get()) + ( + self.traversed.load(Ordering::Acquire), + self.hijacked.load(Ordering::Acquire), + ) } } @@ -424,7 +428,7 @@ fn traverse_cannot_be_hijacked() { impl HijackedTraverse { #[allow(clippy::unnecessary_wraps)] fn __traverse__(&self, _visit: PyVisit<'_>) -> Result<(), PyTraverseError> { - self.traversed.set(true); + self.traversed.store(true, Ordering::Release); Ok(()) } } @@ -436,7 +440,7 @@ fn traverse_cannot_be_hijacked() { impl Traversable for PyRef<'_, HijackedTraverse> { fn __traverse__(&self, _visit: PyVisit<'_>) -> Result<(), PyTraverseError> { - self.hijacked.set(true); + self.hijacked.store(true, Ordering::Release); Ok(()) } } @@ -455,7 +459,7 @@ fn traverse_cannot_be_hijacked() { #[pyclass] struct DropDuringTraversal { - cycle: Cell>>, + cycle: Mutex>>, _guard: DropGuard, } @@ -463,7 +467,8 @@ struct DropDuringTraversal { impl DropDuringTraversal { #[allow(clippy::unnecessary_wraps)] fn __traverse__(&self, _visit: PyVisit<'_>) -> Result<(), PyTraverseError> { - self.cycle.take(); + let mut cycle_ref = self.cycle.lock().unwrap(); + *cycle_ref = None; Ok(()) } } @@ -474,7 +479,7 @@ fn drop_during_traversal_with_gil() { let (guard, check) = drop_check(); let ptr = Python::with_gil(|py| { - let cycle = Cell::new(None); + let cycle = Mutex::new(None); let inst = Py::new( py, DropDuringTraversal { @@ -484,7 +489,7 @@ fn drop_during_traversal_with_gil() { ) .unwrap(); - inst.borrow_mut(py).cycle.set(Some(inst.clone_ref(py))); + *inst.borrow_mut(py).cycle.lock().unwrap() = Some(inst.clone_ref(py)); check.assert_not_dropped(); let ptr = inst.as_ptr(); @@ -508,7 +513,7 @@ fn drop_during_traversal_without_gil() { let (guard, check) = drop_check(); let inst = Python::with_gil(|py| { - let cycle = Cell::new(None); + let cycle = Mutex::new(None); let inst = Py::new( py, DropDuringTraversal { @@ -518,7 +523,7 @@ fn drop_during_traversal_without_gil() { ) .unwrap(); - inst.borrow_mut(py).cycle.set(Some(inst.clone_ref(py))); + *inst.borrow_mut(py).cycle.lock().unwrap() = Some(inst.clone_ref(py)); check.assert_not_dropped(); inst diff --git a/tests/test_getter_setter.rs b/tests/test_getter_setter.rs index 064511772da..cdc8136bede 100644 --- a/tests/test_getter_setter.rs +++ b/tests/test_getter_setter.rs @@ -217,7 +217,7 @@ fn get_all_and_set() { }); } -#[pyclass] +#[pyclass(unsendable)] struct CellGetterSetter { #[pyo3(get, set)] cell_inner: Cell, diff --git a/tests/test_proto_methods.rs b/tests/test_proto_methods.rs index bd4847c46bf..23f7f6cf213 100644 --- a/tests/test_proto_methods.rs +++ b/tests/test_proto_methods.rs @@ -4,6 +4,7 @@ use pyo3::exceptions::{PyAttributeError, PyIndexError, PyValueError}; use pyo3::types::{PyDict, PyList, PyMapping, PySequence, PySlice, PyType}; use pyo3::{prelude::*, py_run}; use std::iter; +use std::sync::Mutex; #[path = "../src/tests/common.rs"] mod common; @@ -361,7 +362,7 @@ fn sequence() { #[pyclass] struct Iterator { - iter: Box + Send>, + iter: Mutex + Send>>, } #[pymethods] @@ -370,8 +371,8 @@ impl Iterator { slf } - fn __next__(mut slf: PyRefMut<'_, Self>) -> Option { - slf.iter.next() + fn __next__(slf: PyRefMut<'_, Self>) -> Option { + slf.iter.lock().unwrap().next() } } @@ -381,7 +382,7 @@ fn iterator() { let inst = Py::new( py, Iterator { - iter: Box::new(5..8), + iter: Mutex::new(Box::new(5..8)), }, ) .unwrap(); diff --git a/tests/ui/pyclass_send.rs b/tests/ui/pyclass_send.rs index a587c071f51..5faded9874f 100644 --- a/tests/ui/pyclass_send.rs +++ b/tests/ui/pyclass_send.rs @@ -1,26 +1,28 @@ use pyo3::prelude::*; -use std::rc::Rc; +use std::os::raw::c_void; #[pyclass] -struct NotThreadSafe { - data: Rc, -} - -fn main() { - let obj = Python::with_gil(|py| { - Bound::new(py, NotThreadSafe { data: Rc::new(5) }) - .unwrap() - .unbind() - }); - - std::thread::spawn(move || { - Python::with_gil(|py| { - // Uh oh, moved Rc to a new thread! - let c = obj.bind(py).downcast::().unwrap(); - - assert_eq!(*c.borrow().data, 5); - }) - }) - .join() - .unwrap(); -} +struct NotSyncNotSend(*mut c_void); + +#[pyclass] +struct SendNotSync(*mut c_void); +unsafe impl Send for SendNotSync {} + +#[pyclass] +struct SyncNotSend(*mut c_void); +unsafe impl Sync for SyncNotSend {} + +// None of the `unsendable` forms below should fail to compile + +#[pyclass(unsendable)] +struct NotSyncNotSendUnsendable(*mut c_void); + +#[pyclass(unsendable)] +struct SendNotSyncUnsendable(*mut c_void); +unsafe impl Send for SendNotSyncUnsendable {} + +#[pyclass(unsendable)] +struct SyncNotSendUnsendable(*mut c_void); +unsafe impl Sync for SyncNotSendUnsendable {} + +fn main() {} diff --git a/tests/ui/pyclass_send.stderr b/tests/ui/pyclass_send.stderr index 0db9106ab42..3ef9591f820 100644 --- a/tests/ui/pyclass_send.stderr +++ b/tests/ui/pyclass_send.stderr @@ -1,17 +1,38 @@ -error[E0277]: `Rc` cannot be sent between threads safely +error[E0277]: `*mut c_void` cannot be shared between threads safely + --> tests/ui/pyclass_send.rs:5:8 + | +5 | struct NotSyncNotSend(*mut c_void); + | ^^^^^^^^^^^^^^ `*mut c_void` cannot be shared between threads safely + | + = help: within `NotSyncNotSend`, the trait `Sync` is not implemented for `*mut c_void`, which is required by `NotSyncNotSend: Sync` +note: required because it appears within the type `NotSyncNotSend` + --> tests/ui/pyclass_send.rs:5:8 + | +5 | struct NotSyncNotSend(*mut c_void); + | ^^^^^^^^^^^^^^ +note: required by a bound in `pyo3::impl_::pyclass::assertions::assert_pyclass_sync` + --> src/impl_/pyclass/assertions.rs + | + | pub const fn assert_pyclass_sync() + | ------------------- required by a bound in this function + | where + | T: PyClassSync + Sync, + | ^^^^ required by this bound in `assert_pyclass_sync` + +error[E0277]: `*mut c_void` cannot be sent between threads safely --> tests/ui/pyclass_send.rs:4:1 | 4 | #[pyclass] - | ^^^^^^^^^^ `Rc` cannot be sent between threads safely + | ^^^^^^^^^^ `*mut c_void` cannot be sent between threads safely | - = help: within `NotThreadSafe`, the trait `Send` is not implemented for `Rc`, which is required by `SendablePyClass: pyo3::impl_::pyclass::PyClassThreadChecker` + = help: within `NotSyncNotSend`, the trait `Send` is not implemented for `*mut c_void`, which is required by `SendablePyClass: pyo3::impl_::pyclass::PyClassThreadChecker` = help: the trait `pyo3::impl_::pyclass::PyClassThreadChecker` is implemented for `SendablePyClass` -note: required because it appears within the type `NotThreadSafe` +note: required because it appears within the type `NotSyncNotSend` --> tests/ui/pyclass_send.rs:5:8 | -5 | struct NotThreadSafe { - | ^^^^^^^^^^^^^ - = note: required for `SendablePyClass` to implement `pyo3::impl_::pyclass::PyClassThreadChecker` +5 | struct NotSyncNotSend(*mut c_void); + | ^^^^^^^^^^^^^^ + = note: required for `SendablePyClass` to implement `pyo3::impl_::pyclass::PyClassThreadChecker` note: required by a bound in `PyClassImpl::ThreadChecker` --> src/impl_/pyclass.rs | @@ -19,21 +40,82 @@ note: required by a bound in `PyClassImpl::ThreadChecker` | ^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `PyClassImpl::ThreadChecker` = note: this error originates in the attribute macro `pyclass` (in Nightly builds, run with -Z macro-backtrace for more info) -error[E0277]: `Rc` cannot be sent between threads safely +error[E0277]: `*mut c_void` cannot be shared between threads safely + --> tests/ui/pyclass_send.rs:8:8 + | +8 | struct SendNotSync(*mut c_void); + | ^^^^^^^^^^^ `*mut c_void` cannot be shared between threads safely + | + = help: within `SendNotSync`, the trait `Sync` is not implemented for `*mut c_void`, which is required by `SendNotSync: Sync` +note: required because it appears within the type `SendNotSync` + --> tests/ui/pyclass_send.rs:8:8 + | +8 | struct SendNotSync(*mut c_void); + | ^^^^^^^^^^^ +note: required by a bound in `pyo3::impl_::pyclass::assertions::assert_pyclass_sync` + --> src/impl_/pyclass/assertions.rs + | + | pub const fn assert_pyclass_sync() + | ------------------- required by a bound in this function + | where + | T: PyClassSync + Sync, + | ^^^^ required by this bound in `assert_pyclass_sync` + +error[E0277]: `*mut c_void` cannot be sent between threads safely + --> tests/ui/pyclass_send.rs:11:1 + | +11 | #[pyclass] + | ^^^^^^^^^^ `*mut c_void` cannot be sent between threads safely + | + = help: within `SyncNotSend`, the trait `Send` is not implemented for `*mut c_void`, which is required by `SendablePyClass: pyo3::impl_::pyclass::PyClassThreadChecker` + = help: the trait `pyo3::impl_::pyclass::PyClassThreadChecker` is implemented for `SendablePyClass` +note: required because it appears within the type `SyncNotSend` + --> tests/ui/pyclass_send.rs:12:8 + | +12 | struct SyncNotSend(*mut c_void); + | ^^^^^^^^^^^ + = note: required for `SendablePyClass` to implement `pyo3::impl_::pyclass::PyClassThreadChecker` +note: required by a bound in `PyClassImpl::ThreadChecker` + --> src/impl_/pyclass.rs + | + | type ThreadChecker: PyClassThreadChecker; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `PyClassImpl::ThreadChecker` + = note: this error originates in the attribute macro `pyclass` (in Nightly builds, run with -Z macro-backtrace for more info) + +error[E0277]: `*mut c_void` cannot be sent between threads safely --> tests/ui/pyclass_send.rs:4:1 | 4 | #[pyclass] - | ^^^^^^^^^^ `Rc` cannot be sent between threads safely + | ^^^^^^^^^^ `*mut c_void` cannot be sent between threads safely | - = help: within `NotThreadSafe`, the trait `Send` is not implemented for `Rc`, which is required by `NotThreadSafe: Send` -note: required because it appears within the type `NotThreadSafe` + = help: within `NotSyncNotSend`, the trait `Send` is not implemented for `*mut c_void`, which is required by `NotSyncNotSend: Send` +note: required because it appears within the type `NotSyncNotSend` --> tests/ui/pyclass_send.rs:5:8 | -5 | struct NotThreadSafe { - | ^^^^^^^^^^^^^ +5 | struct NotSyncNotSend(*mut c_void); + | ^^^^^^^^^^^^^^ note: required by a bound in `SendablePyClass` --> src/impl_/pyclass.rs | | 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]: `*mut c_void` cannot be sent between threads safely + --> tests/ui/pyclass_send.rs:11:1 + | +11 | #[pyclass] + | ^^^^^^^^^^ `*mut c_void` cannot be sent between threads safely + | + = help: within `SyncNotSend`, the trait `Send` is not implemented for `*mut c_void`, which is required by `SyncNotSend: Send` +note: required because it appears within the type `SyncNotSend` + --> tests/ui/pyclass_send.rs:12:8 + | +12 | struct SyncNotSend(*mut c_void); + | ^^^^^^^^^^^ +note: required by a bound in `SendablePyClass` + --> src/impl_/pyclass.rs + | + | 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) From b05ba4913a5e25fad55686a1ed4fedffebd3867f Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Tue, 5 Nov 2024 15:30:16 -0700 Subject: [PATCH 362/495] docs: fix links in free-threaded guide (#4683) * fix links in free-threaded guide * respond to review comment --- guide/src/free-threading.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/guide/src/free-threading.md b/guide/src/free-threading.md index b466269c4e3..f79b28667fe 100644 --- a/guide/src/free-threading.md +++ b/guide/src/free-threading.md @@ -347,15 +347,15 @@ which releases the GIL while the waiting for the lock, so that is another option if you only need to support newer Python versions. [`GILOnceCell`]: {{#PYO3_DOCS_URL}}/pyo3/sync/struct.GILOnceCell.html -[`GILProtected]: {{#PYO3_DOCS_URL}}/pyo3/sync/struct.GILProtected.html +[`GILProtected`]: https://docs.rs/pyo3/0.22/pyo3/sync/struct.GILProtected.html [`Once`]: https://doc.rust-lang.org/stable/std/sync/struct.Once.html [`Once::call_once`]: https://doc.rust-lang.org/stable/std/sync/struct.Once.html#tymethod.call_once [`Once::call_once_force`]: https://doc.rust-lang.org/stable/std/sync/struct.Once.html#tymethod.call_once_force -[`OnceExt]: {{#PYO3_DOCS_URL}}/pyo3/sync/trait.OnceExt.html +[`OnceExt`]: {{#PYO3_DOCS_URL}}/pyo3/sync/trait.OnceExt.html [`OnceExt::call_once_py_attached`]: {{#PYO3_DOCS_URL}}/pyo3/sync/trait.OnceExt.html#tymethod.call_once_py_attached [`OnceExt::call_once_force_py_attached`]: {{#PYO3_DOCS_URL}}/pyo3/sync/trait.OnceExt.html#tymethod.call_once_force_py_attached -[`OnceLockExt]: {{#PYO3_DOCS_URL}}/pyo3/sync/trait.OnceLockExt.html -[`OnceLockExt::get_or_init_py_attached]: {{#PYO3_DOCS_URL}}/pyo3/sync/trait.OnceLockExt.html#tymethod.get_or_init_py_attached +[`OnceLockExt`]: {{#PYO3_DOCS_URL}}/pyo3/sync/trait.OnceLockExt.html +[`OnceLockExt::get_or_init_py_attached`]: {{#PYO3_DOCS_URL}}/pyo3/sync/trait.OnceLockExt.html#tymethod.get_or_init_py_attached [`OnceLock`]: https://doc.rust-lang.org/stable/std/sync/struct.OnceLock.html [`OnceLock::get_or_init`]: https://doc.rust-lang.org/stable/std/sync/struct.OnceLock.html#tymethod.get_or_init [`Python::allow_threads`]: {{#PYO3_DOCS_URL}}/pyo3/marker/struct.Python.html#method.allow_threads From faa644afa4c0a88ecbc50c6c1b76d0bdbe795568 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Wed, 6 Nov 2024 02:19:57 -0700 Subject: [PATCH 363/495] Run free-threaded CI on MacOS (#4686) * run free-threaded CI on MacOS and Windows * build against 3.13 instead of 3.13-dev on build-full * skip windows build, see #4685 --- .github/workflows/ci.yml | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index eba8676f01d..220738168f8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -241,7 +241,7 @@ jobs: "3.10", "3.11", "3.12", - "3.13-dev", + "3.13", "pypy3.9", "pypy3.10", "graalpy24.0", @@ -552,7 +552,11 @@ jobs: test-free-threaded: needs: [fmt] - runs-on: ubuntu-latest + name: Free threaded tests - ${{ matrix.runner }} + runs-on: ${{ matrix.runner }} + strategy: + matrix: + runner: ["ubuntu-latest", "macos-latest"] steps: - uses: actions/checkout@v4 - uses: Swatinem/rust-cache@v2 @@ -561,11 +565,10 @@ jobs: - uses: dtolnay/rust-toolchain@stable with: components: rust-src - # TODO: replace with setup-python when there is support - - uses: deadsnakes/action@v3.2.0 + # TODO: replace with actions/setup-python when there is support + - uses: quansight-labs/setup-python@v5.3.1 with: - python-version: '3.13-dev' - nogil: true + python-version: '3.13t' - name: Install cargo-llvm-cov uses: taiki-e/install-action@cargo-llvm-cov - run: python3 -m sysconfig From de709e5d896df295a8a568e5e2f41229729f9bb9 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Fri, 8 Nov 2024 15:00:03 -0700 Subject: [PATCH 364/495] Fix windows free-threaded build issues (#4690) * use the correct library name for the free-threaded build * Query sysconfig on windows for Python 3.13 and newer * attempt to fix freethreaded python pyo3-ffi-check on windows * fix clippy lint * turn on windows free-threaded CI * only check python version on windows * remove debug prints --- .github/workflows/ci.yml | 2 +- pyo3-build-config/src/impl_.rs | 89 +++++++++++++++++++++++++++++----- pyo3-ffi-check/build.rs | 16 +++++- 3 files changed, 93 insertions(+), 14 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 220738168f8..af1217b31fa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -556,7 +556,7 @@ jobs: runs-on: ${{ matrix.runner }} strategy: matrix: - runner: ["ubuntu-latest", "macos-latest"] + runner: ["ubuntu-latest", "macos-latest", "windows-latest"] steps: - uses: actions/checkout@v4 - uses: Swatinem/rust-cache@v2 diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index ec65259115f..6d2326429d2 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -248,6 +248,7 @@ print("executable", sys.executable) print("calcsize_pointer", struct.calcsize("P")) print("mingw", get_platform().startswith("mingw")) print("ext_suffix", get_config_var("EXT_SUFFIX")) +print("gil_disabled", get_config_var("Py_GIL_DISABLED")) "#; let output = run_python_script(interpreter.as_ref(), SCRIPT)?; let map: HashMap = parse_script_output(&output); @@ -290,6 +291,13 @@ print("ext_suffix", get_config_var("EXT_SUFFIX")) let implementation = map["implementation"].parse()?; + let gil_disabled = match map["gil_disabled"].as_str() { + "1" => true, + "0" => false, + "None" => false, + _ => panic!("Unknown Py_GIL_DISABLED value"), + }; + let lib_name = if cfg!(windows) { default_lib_name_windows( version, @@ -300,12 +308,14 @@ print("ext_suffix", get_config_var("EXT_SUFFIX")) // on Windows from sysconfig - e.g. ext_suffix may be // `_d.cp312-win_amd64.pyd` for 3.12 debug build map["ext_suffix"].starts_with("_d."), + gil_disabled, ) } else { default_lib_name_unix( version, implementation, map.get("ld_version").map(String::as_str), + gil_disabled, ) }; @@ -375,10 +385,15 @@ print("ext_suffix", get_config_var("EXT_SUFFIX")) _ => false, }; let lib_dir = get_key!(sysconfigdata, "LIBDIR").ok().map(str::to_string); + let gil_disabled = match sysconfigdata.get_value("Py_GIL_DISABLED") { + Some(value) => value == "1", + None => false, + }; let lib_name = Some(default_lib_name_unix( version, implementation, sysconfigdata.get_value("LDVERSION"), + gil_disabled, )); let pointer_width = parse_key!(sysconfigdata, "SIZEOF_VOID_P") .map(|bytes_width: u32| bytes_width * 8) @@ -1106,10 +1121,15 @@ impl BuildFlags { /// the interpreter and printing variables of interest from /// sysconfig.get_config_vars. fn from_interpreter(interpreter: impl AsRef) -> Result { - // sysconfig is missing all the flags on windows, so we can't actually - // query the interpreter directly for its build flags. + // sysconfig is missing all the flags on windows for Python 3.12 and + // older, so we can't actually query the interpreter directly for its + // build flags on those versions. if cfg!(windows) { - return Ok(Self::new()); + let script = String::from("import sys;print(sys.version_info < (3, 13))"); + let stdout = run_python_script(interpreter.as_ref(), &script)?; + if stdout.trim_end() == "True" { + return Ok(Self::new()); + } } let mut script = String::from("import sysconfig\n"); @@ -1528,6 +1548,7 @@ fn default_abi3_config(host: &Triple, version: PythonVersion) -> InterpreterConf abi3, false, false, + false, )) } else { None @@ -1604,9 +1625,10 @@ fn default_lib_name_for_target( abi3, false, false, + false, )) } else if is_linking_libpython_for_target(target) { - Some(default_lib_name_unix(version, implementation, None)) + Some(default_lib_name_unix(version, implementation, None, false)) } else { None } @@ -1618,16 +1640,26 @@ fn default_lib_name_windows( abi3: bool, mingw: bool, debug: bool, + gil_disabled: bool, ) -> String { if debug { // CPython bug: linking against python3_d.dll raises error // https://github.com/python/cpython/issues/101614 - format!("python{}{}_d", version.major, version.minor) + if gil_disabled { + format!("python{}{}t_d", version.major, version.minor) + } else { + format!("python{}{}_d", version.major, version.minor) + } } else if abi3 && !(implementation.is_pypy() || implementation.is_graalpy()) { WINDOWS_ABI3_LIB_NAME.to_owned() } else if mingw { + if gil_disabled { + panic!("MinGW free-threaded builds are not currently tested or supported") + } // https://packages.msys2.org/base/mingw-w64-python format!("python{}.{}", version.major, version.minor) + } else if gil_disabled { + format!("python{}{}t", version.major, version.minor) } else { format!("python{}{}", version.major, version.minor) } @@ -1637,6 +1669,7 @@ fn default_lib_name_unix( version: PythonVersion, implementation: PythonImplementation, ld_version: Option<&str>, + gil_disabled: bool, ) -> String { match implementation { PythonImplementation::CPython => match ld_version { @@ -1644,7 +1677,11 @@ fn default_lib_name_unix( None => { if version > PythonVersion::PY37 { // PEP 3149 ABI version tags are finally gone - format!("python{}.{}", version.major, version.minor) + if gil_disabled { + format!("python{}.{}t", version.major, version.minor) + } else { + format!("python{}.{}", version.major, version.minor) + } } else { // Work around https://bugs.python.org/issue36707 format!("python{}.{}m", version.major, version.minor) @@ -2351,6 +2388,7 @@ mod tests { false, false, false, + false, ), "python39", ); @@ -2361,6 +2399,7 @@ mod tests { true, false, false, + false, ), "python3", ); @@ -2371,6 +2410,7 @@ mod tests { false, true, false, + false, ), "python3.9", ); @@ -2381,6 +2421,7 @@ mod tests { true, true, false, + false, ), "python3", ); @@ -2391,6 +2432,7 @@ mod tests { true, false, false, + false, ), "python39", ); @@ -2401,6 +2443,7 @@ mod tests { false, false, true, + false, ), "python39_d", ); @@ -2413,6 +2456,7 @@ mod tests { true, false, true, + false, ), "python39_d", ); @@ -2423,16 +2467,31 @@ mod tests { use PythonImplementation::*; // Defaults to python3.7m for CPython 3.7 assert_eq!( - super::default_lib_name_unix(PythonVersion { major: 3, minor: 7 }, CPython, None), + super::default_lib_name_unix( + PythonVersion { major: 3, minor: 7 }, + CPython, + None, + false + ), "python3.7m", ); // Defaults to pythonX.Y for CPython 3.8+ assert_eq!( - super::default_lib_name_unix(PythonVersion { major: 3, minor: 8 }, CPython, None), + super::default_lib_name_unix( + PythonVersion { major: 3, minor: 8 }, + CPython, + None, + false + ), "python3.8", ); assert_eq!( - super::default_lib_name_unix(PythonVersion { major: 3, minor: 9 }, CPython, None), + super::default_lib_name_unix( + PythonVersion { major: 3, minor: 9 }, + CPython, + None, + false + ), "python3.9", ); // Can use ldversion to override for CPython @@ -2440,19 +2499,25 @@ mod tests { super::default_lib_name_unix( PythonVersion { major: 3, minor: 9 }, CPython, - Some("3.7md") + Some("3.7md"), + false ), "python3.7md", ); // PyPy 3.9 includes ldversion assert_eq!( - super::default_lib_name_unix(PythonVersion { major: 3, minor: 9 }, PyPy, None), + super::default_lib_name_unix(PythonVersion { major: 3, minor: 9 }, PyPy, None, false), "pypy3.9-c", ); assert_eq!( - super::default_lib_name_unix(PythonVersion { major: 3, minor: 9 }, PyPy, Some("3.9d")), + super::default_lib_name_unix( + PythonVersion { major: 3, minor: 9 }, + PyPy, + Some("3.9d"), + false + ), "pypy3.9d-c", ); } diff --git a/pyo3-ffi-check/build.rs b/pyo3-ffi-check/build.rs index ca4a17b6a61..072e8c072b0 100644 --- a/pyo3-ffi-check/build.rs +++ b/pyo3-ffi-check/build.rs @@ -8,12 +8,26 @@ fn main() { "import sysconfig; print(sysconfig.get_config_var('INCLUDEPY'), end='');", ) .expect("failed to get lib dir"); + let gil_disabled_on_windows = config + .run_python_script( + "import sysconfig; import platform; print(sysconfig.get_config_var('Py_GIL_DISABLED') == 1 and platform.system() == 'Windows');", + ) + .expect("failed to get Py_GIL_DISABLED").trim_end() == "True"; + + let clang_args = if gil_disabled_on_windows { + vec![ + format!("-I{python_include_dir}"), + "-DPy_GIL_DISABLED".to_string(), + ] + } else { + vec![format!("-I{python_include_dir}")] + }; println!("cargo:rerun-if-changed=wrapper.h"); let bindings = bindgen::Builder::default() .header("wrapper.h") - .clang_arg(format!("-I{python_include_dir}")) + .clang_args(clang_args) .parse_callbacks(Box::new(bindgen::CargoCallbacks)) // blocklist some values which apparently have conflicting definitions on unix .blocklist_item("FP_NORMAL") From 3a6296e21556115f9cf57b573174a0e432b655ad Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Sat, 9 Nov 2024 15:06:40 +0000 Subject: [PATCH 365/495] silence deprecation warning for enum with custom `__eq__` (#4692) * silence deprecation warning for enum with custom `__eq__` * clippy * also support `__richcmp__` * fix ci --------- Co-authored-by: Icxolu <10486322+Icxolu@users.noreply.github.com> --- newsfragments/4692.fixed.md | 1 + pyo3-macros-backend/src/pyclass.rs | 7 +--- pyo3-macros-backend/src/pymethod.rs | 9 +++++ src/impl_/pyclass.rs | 46 +++++++++++++++++++++++++ tests/test_enum.rs | 46 +++++++++++++++++++++++++ tests/ui/deprecations.stderr | 2 +- tests/ui/invalid_proto_pymethods.stderr | 11 ++++++ tests/ui/invalid_pyclass_args.stderr | 11 ++++++ 8 files changed, 126 insertions(+), 7 deletions(-) create mode 100644 newsfragments/4692.fixed.md diff --git a/newsfragments/4692.fixed.md b/newsfragments/4692.fixed.md new file mode 100644 index 00000000000..a5dc6d098cf --- /dev/null +++ b/newsfragments/4692.fixed.md @@ -0,0 +1 @@ +Fix incorrect deprecation warning for `#[pyclass] enum`s with custom `__eq__` implementation. diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index ae3082e3785..9bb94e00d6f 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -1908,12 +1908,7 @@ fn pyclass_richcmp_simple_enum( let deprecation = (options.eq_int.is_none() && options.eq.is_none()) .then(|| { quote! { - #[deprecated( - since = "0.22.0", - note = "Implicit equality for simple enums is deprecated. Use `#[pyclass(eq, eq_int)]` to keep the current behavior." - )] - const DEPRECATION: () = (); - const _: () = DEPRECATION; + let _ = #pyo3_path::impl_::pyclass::DeprecationTest::<#cls>::new().autogenerated_equality(); } }) .unwrap_or_default(); diff --git a/pyo3-macros-backend/src/pymethod.rs b/pyo3-macros-backend/src/pymethod.rs index 1254a8d510b..560c3c9dcc1 100644 --- a/pyo3-macros-backend/src/pymethod.rs +++ b/pyo3-macros-backend/src/pymethod.rs @@ -1348,6 +1348,14 @@ impl SlotDef { )?; let name = spec.name; let holders = holders.init_holders(ctx); + let dep = if method_name == "__richcmp__" { + quote! { + #[allow(unknown_lints, non_local_definitions)] + impl #pyo3_path::impl_::pyclass::HasCustomRichCmp for #cls {} + } + } else { + TokenStream::default() + }; let associated_method = quote! { #[allow(non_snake_case)] unsafe fn #wrapper_ident( @@ -1355,6 +1363,7 @@ impl SlotDef { _raw_slf: *mut #pyo3_path::ffi::PyObject, #(#arg_idents: #arg_types),* ) -> #pyo3_path::PyResult<#ret_ty> { + #dep let function = #cls::#name; // Shadow the method name to avoid #3017 let _slf = _raw_slf; #holders diff --git a/src/impl_/pyclass.rs b/src/impl_/pyclass.rs index d5f292f9ebd..c947df6e432 100644 --- a/src/impl_/pyclass.rs +++ b/src/impl_/pyclass.rs @@ -878,6 +878,8 @@ macro_rules! generate_pyclass_richcompare_slot { other: *mut $crate::ffi::PyObject, op: ::std::os::raw::c_int, ) -> *mut $crate::ffi::PyObject { + impl $crate::impl_::pyclass::HasCustomRichCmp for $cls {} + $crate::impl_::trampoline::richcmpfunc(slf, other, op, |py, slf, other, op| { use $crate::class::basic::CompareOp; use $crate::impl_::pyclass::*; @@ -1519,6 +1521,50 @@ fn pyo3_get_value< Ok((unsafe { &*value }).clone().into_py(py).into_ptr()) } +/// Marker trait whether a class implemented a custom comparison. Used to +/// silence deprecation of autogenerated `__richcmp__` for enums. +pub trait HasCustomRichCmp {} + +/// Autoref specialization setup to emit deprecation warnings for autogenerated +/// pyclass equality. +pub struct DeprecationTest(Deprecation, ::std::marker::PhantomData); +pub struct Deprecation; + +impl DeprecationTest { + #[inline] + #[allow(clippy::new_without_default)] + pub const fn new() -> Self { + DeprecationTest(Deprecation, ::std::marker::PhantomData) + } +} + +impl std::ops::Deref for DeprecationTest { + type Target = Deprecation; + #[inline] + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DeprecationTest +where + T: HasCustomRichCmp, +{ + /// For `HasCustomRichCmp` types; no deprecation warning. + #[inline] + pub fn autogenerated_equality(&self) {} +} + +impl Deprecation { + #[deprecated( + since = "0.22.0", + note = "Implicit equality for simple enums is deprecated. Use `#[pyclass(eq, eq_int)]` to keep the current behavior." + )] + /// For types which don't implement `HasCustomRichCmp`; emits deprecation warning. + #[inline] + pub fn autogenerated_equality(&self) {} +} + #[cfg(test)] #[cfg(feature = "macros")] mod tests { diff --git a/tests/test_enum.rs b/tests/test_enum.rs index 9cb9d5fae65..c0a8f8b1e35 100644 --- a/tests/test_enum.rs +++ b/tests/test_enum.rs @@ -2,6 +2,7 @@ use pyo3::prelude::*; use pyo3::py_run; +use pyo3::types::PyString; #[path = "../src/tests/common.rs"] mod common; @@ -357,3 +358,48 @@ mod deprecated { }) } } + +#[test] +fn custom_eq() { + #[pyclass(frozen)] + #[derive(PartialEq)] + pub enum CustomPyEq { + A, + B, + } + + #[pymethods] + impl CustomPyEq { + fn __eq__(&self, other: &Bound<'_, PyAny>) -> bool { + if let Ok(rhs) = other.downcast::() { + rhs.to_cow().map_or(false, |rhs| self.__str__() == rhs) + } else if let Ok(rhs) = other.downcast::() { + self == rhs.get() + } else { + false + } + } + + fn __str__(&self) -> String { + match self { + CustomPyEq::A => "A".to_string(), + CustomPyEq::B => "B".to_string(), + } + } + } + + Python::with_gil(|py| { + let a = Bound::new(py, CustomPyEq::A).unwrap(); + let b = Bound::new(py, CustomPyEq::B).unwrap(); + + assert!(a.as_any().eq(&a).unwrap()); + assert!(a.as_any().eq("A").unwrap()); + assert!(a.as_any().ne(&b).unwrap()); + assert!(a.as_any().ne("B").unwrap()); + + assert!(b.as_any().eq(&b).unwrap()); + assert!(b.as_any().eq("B").unwrap()); + assert!(b.as_any().ne(&a).unwrap()); + assert!(b.as_any().ne("A").unwrap()); + }) +} diff --git a/tests/ui/deprecations.stderr b/tests/ui/deprecations.stderr index 567f5697c7f..6236dc55631 100644 --- a/tests/ui/deprecations.stderr +++ b/tests/ui/deprecations.stderr @@ -28,7 +28,7 @@ error: use of deprecated constant `__pyfunction_pyfunction_option_4::SIGNATURE`: 21 | fn pyfunction_option_4( | ^^^^^^^^^^^^^^^^^^^ -error: use of deprecated constant `SimpleEnumWithoutEq::__pyo3__generated____richcmp__::DEPRECATION`: Implicit equality for simple enums is deprecated. Use `#[pyclass(eq, eq_int)]` to keep the current behavior. +error: use of deprecated method `pyo3::impl_::pyclass::Deprecation::autogenerated_equality`: Implicit equality for simple enums is deprecated. Use `#[pyclass(eq, eq_int)]` to keep the current behavior. --> tests/ui/deprecations.rs:28:1 | 28 | #[pyclass] diff --git a/tests/ui/invalid_proto_pymethods.stderr b/tests/ui/invalid_proto_pymethods.stderr index 82c99c2ddc3..18c96113299 100644 --- a/tests/ui/invalid_proto_pymethods.stderr +++ b/tests/ui/invalid_proto_pymethods.stderr @@ -40,6 +40,17 @@ note: candidate #2 is defined in an impl for the type `EqAndRichcmp` | ^^^^^^^^^^^^ = note: this error originates in the macro `::pyo3::impl_::pyclass::generate_pyclass_richcompare_slot` which comes from the expansion of the attribute macro `pymethods` (in Nightly builds, run with -Z macro-backtrace for more info) +error[E0119]: conflicting implementations of trait `HasCustomRichCmp` for type `EqAndRichcmp` + --> tests/ui/invalid_proto_pymethods.rs:55:1 + | +55 | #[pymethods] + | ^^^^^^^^^^^^ + | | + | first implementation here + | conflicting implementation for `EqAndRichcmp` + | + = note: this error originates in the macro `::pyo3::impl_::pyclass::generate_pyclass_richcompare_slot` which comes from the expansion of the attribute macro `pymethods` (in Nightly builds, run with -Z macro-backtrace for more info) + error[E0592]: duplicate definitions with name `__pymethod___richcmp____` --> tests/ui/invalid_proto_pymethods.rs:55:1 | diff --git a/tests/ui/invalid_pyclass_args.stderr b/tests/ui/invalid_pyclass_args.stderr index 15aa0387cc6..d1335e0f1a1 100644 --- a/tests/ui/invalid_pyclass_args.stderr +++ b/tests/ui/invalid_pyclass_args.stderr @@ -162,6 +162,17 @@ error: The format string syntax cannot be used with enums 171 | #[pyclass(eq, str = "Stuff...")] | ^^^^^^^^^^ +error[E0119]: conflicting implementations of trait `HasCustomRichCmp` for type `EqOptAndManualRichCmp` + --> tests/ui/invalid_pyclass_args.rs:41:1 + | +37 | #[pyclass(eq)] + | -------------- first implementation here +... +41 | #[pymethods] + | ^^^^^^^^^^^^ conflicting implementation for `EqOptAndManualRichCmp` + | + = 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___richcmp____` --> tests/ui/invalid_pyclass_args.rs:37:1 | From be4407cde9a8739f7c1ea4875a0287c08fb90220 Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Sun, 10 Nov 2024 21:35:31 +0100 Subject: [PATCH 366/495] rework complex enum field conversion (#4694) * rework complex enum field conversion * add newsfragment --- newsfragments/4694.fixed.md | 1 + pyo3-macros-backend/src/pyclass.rs | 22 ++++++++++++++++---- src/impl_/pyclass.rs | 32 ++++++++++++++++++++++++++++++ tests/test_enum.rs | 3 ++- 4 files changed, 53 insertions(+), 5 deletions(-) create mode 100644 newsfragments/4694.fixed.md diff --git a/newsfragments/4694.fixed.md b/newsfragments/4694.fixed.md new file mode 100644 index 00000000000..80df8802cd4 --- /dev/null +++ b/newsfragments/4694.fixed.md @@ -0,0 +1 @@ +Complex enums now allow field types that either implement `IntoPyObject` by reference or by value together with `Clone`. This makes `Py` available as field type. \ No newline at end of file diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index 9bb94e00d6f..2dd4cbfab2e 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -1233,9 +1233,16 @@ fn impl_complex_enum_struct_variant_cls( complex_enum_variant_field_getter(&variant_cls_type, field_name, field.span, ctx)?; let field_getter_impl = quote! { - fn #field_name(slf: #pyo3_path::PyRef) -> #pyo3_path::PyResult<#field_type> { + fn #field_name(slf: #pyo3_path::PyRef) -> #pyo3_path::PyResult<#pyo3_path::PyObject> { + #[allow(unused_imports)] + use #pyo3_path::impl_::pyclass::Probe; + let py = slf.py(); match &*slf.into_super() { - #enum_name::#variant_ident { #field_name, .. } => ::std::result::Result::Ok(::std::clone::Clone::clone(&#field_name)), + #enum_name::#variant_ident { #field_name, .. } => + #pyo3_path::impl_::pyclass::ConvertField::< + { #pyo3_path::impl_::pyclass::IsIntoPyObjectRef::<#field_type>::VALUE }, + { #pyo3_path::impl_::pyclass::IsIntoPyObject::<#field_type>::VALUE }, + >::convert_field::<#field_type>(#field_name, py), _ => ::core::unreachable!("Wrong complex enum variant found in variant wrapper PyClass"), } } @@ -1302,9 +1309,16 @@ fn impl_complex_enum_tuple_variant_field_getters( }) .collect(); let field_getter_impl: syn::ImplItemFn = parse_quote! { - fn #field_name(slf: #pyo3_path::PyRef) -> #pyo3_path::PyResult<#field_type> { + fn #field_name(slf: #pyo3_path::PyRef) -> #pyo3_path::PyResult<#pyo3_path::PyObject> { + #[allow(unused_imports)] + use #pyo3_path::impl_::pyclass::Probe; + let py = slf.py(); match &*slf.into_super() { - #enum_name::#variant_ident ( #(#field_access_tokens), *) => ::std::result::Result::Ok(::std::clone::Clone::clone(&val)), + #enum_name::#variant_ident ( #(#field_access_tokens), *) => + #pyo3_path::impl_::pyclass::ConvertField::< + { #pyo3_path::impl_::pyclass::IsIntoPyObjectRef::<#field_type>::VALUE }, + { #pyo3_path::impl_::pyclass::IsIntoPyObject::<#field_type>::VALUE }, + >::convert_field::<#field_type>(val, py), _ => ::core::unreachable!("Wrong complex enum variant found in variant wrapper PyClass"), } } diff --git a/src/impl_/pyclass.rs b/src/impl_/pyclass.rs index c947df6e432..8e7e8cf844f 100644 --- a/src/impl_/pyclass.rs +++ b/src/impl_/pyclass.rs @@ -1521,6 +1521,38 @@ fn pyo3_get_value< Ok((unsafe { &*value }).clone().into_py(py).into_ptr()) } +pub struct ConvertField< + const IMPLEMENTS_INTOPYOBJECT_REF: bool, + const IMPLEMENTS_INTOPYOBJECT: bool, +>; + +impl ConvertField { + #[inline] + pub fn convert_field<'a, 'py, T>(obj: &'a T, py: Python<'py>) -> PyResult> + where + &'a T: IntoPyObject<'py>, + { + obj.into_pyobject(py) + .map(BoundObject::into_any) + .map(BoundObject::unbind) + .map_err(Into::into) + } +} + +impl ConvertField { + #[inline] + pub fn convert_field<'py, T>(obj: &T, py: Python<'py>) -> PyResult> + where + T: PyO3GetField<'py>, + { + obj.clone() + .into_pyobject(py) + .map(BoundObject::into_any) + .map(BoundObject::unbind) + .map_err(Into::into) + } +} + /// Marker trait whether a class implemented a custom comparison. Used to /// silence deprecation of autogenerated `__richcmp__` for enums. pub trait HasCustomRichCmp {} diff --git a/tests/test_enum.rs b/tests/test_enum.rs index c0a8f8b1e35..40c5f4681a8 100644 --- a/tests/test_enum.rs +++ b/tests/test_enum.rs @@ -202,9 +202,10 @@ fn test_renaming_all_enum_variants() { } #[pyclass(module = "custom_module")] -#[derive(Debug, Clone)] +#[derive(Debug)] enum CustomModuleComplexEnum { Variant(), + Py(Py), } #[test] From dd3e94e3044fac16e1451030efc134b9fac1f8ad Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Mon, 11 Nov 2024 13:08:31 -0700 Subject: [PATCH 367/495] docs: text improvements for guide/src/free-threading.md (#4696) --- guide/src/free-threading.md | 51 ++++++++++++++++++++----------------- 1 file changed, 27 insertions(+), 24 deletions(-) diff --git a/guide/src/free-threading.md b/guide/src/free-threading.md index f79b28667fe..4a326e58e98 100644 --- a/guide/src/free-threading.md +++ b/guide/src/free-threading.md @@ -59,10 +59,11 @@ thread safety of existing Rust extensions and how to think about the PyO3 API using a Python runtime with no GIL. If you do not explicitly mark that modules are thread-safe, the Python -interpreter will re-enable the GIL at runtime and print a `RuntimeWarning` -explaining which module caused it to re-enable the GIL. You can also force the -GIL to remain disabled by setting the `PYTHON_GIL=0` as an environment variable -or passing `-Xgil=0` when starting Python (`0` means the GIL is turned off). +interpreter will re-enable the GIL at runtime while importing your module and +print a `RuntimeWarning` with a message containing the name of the module +causing it to re-enable the GIL. You can force the GIL to remain disabled by +setting the `PYTHON_GIL=0` as an environment variable or passing `-Xgil=0` when +starting Python (`0` means the GIL is turned off). If you are sure that all data structures exposed in a `PyModule` are thread-safe, then pass `gil_used = false` as a parameter to the @@ -94,6 +95,10 @@ fn register_child_module(parent_module: &Bound<'_, PyModule>) -> PyResult<()> { ``` +For now you must explicitly opt in to free-threading support by annotating +modules defined in your extension. In a future version of `PyO3`, we plan to +make `gil_used = false` the default. + See the [`string-sum`](https://github.com/PyO3/pyo3/tree/main/pyo3-ffi/examples/string-sum) example for how to declare free-threaded support using raw FFI calls for modules @@ -123,34 +128,30 @@ versions of PyO3, but for now you should remember that the use of the term `GIL` in functions and types like [`Python::with_gil`] and [`GILOnceCell`] is historical. -Instead, you can think about whether or not a Rust thread is attached to a -Python interpreter runtime. See [PEP +Instead, you should think about whether or not a Rust thread is attached to a +Python interpreter runtime. Calling into the CPython C API is only legal when an +OS thread is explicitly attached to the interpreter runtime. In the GIL-enabled +build, this happens when the GIL is acquired. In the free-threaded build there +is no GIL, but the same C macros that release or acquire the GIL in the +GIL-enabled build instead ask the interpreter to attach the thread to the Python +runtime, and there can be many threads simultaneously attached. See [PEP 703](https://peps.python.org/pep-0703/#thread-states) for more background about how threads can be attached and detached from the interpreter runtime, in a manner analagous to releasing and acquiring the GIL in the GIL-enabled build. -Calling into the CPython C API is only legal when an OS thread is explicitly -attached to the interpreter runtime. In the GIL-enabled build, this happens when -the GIL is acquired. In the free-threaded build there is no GIL, but the same C -macros that release or acquire the GIL in the GIL-enabled build instead ask the -interpreter to attach the thread to the Python runtime, and there can be many -threads simultaneously attached. - -The main reason for attaching to the Python runtime is to interact with Python -objects or call into the CPython C API. To interact with the Python runtime, the -thread must register itself by attaching to the interpreter runtime. If you are -not yet attached to the Python runtime, you can register the thread using the -[`Python::with_gil`] function. Threads created via the Python [`threading`] -module do not not need to do this, but all other OS threads that interact with -the Python runtime must explicitly attach using `with_gil` and obtain a `'py` -liftime. - In the GIL-enabled build, PyO3 uses the [`Python<'py>`] type and the `'py` lifetime to signify that the global interpreter lock is held. In the freethreaded build, holding a `'py` lifetime means only that the thread is currently attached to the Python interpreter -- other threads can be simultaneously interacting with the interpreter. +The main reason for obtaining a `'py` lifetime is to interact with Python +objects or call into the CPython C API. If you are not yet attached to the +Python runtime, you can register a thread using the [`Python::with_gil`] +function. Threads created via the Python [`threading`] module do not not need to +do this, but all other OS threads that interact with the Python runtime must +explicitly attach using `with_gil` and obtain a `'py` liftime. + Since there is no GIL in the free-threaded build, releasing the GIL for long-running tasks is no longer necessary to ensure other threads run, but you should still detach from the interpreter runtime using [`Python::allow_threads`] @@ -227,7 +228,9 @@ RuntimeError: Already borrowed We plan to allow user-selectable semantics for mutable pyclass definitions in PyO3 0.24, allowing some form of opt-in locking to emulate the GIL if that is -needed. +needed. For now you should explicitly add locking, possibly using conditional +compilation or using the critical section API to avoid creating deadlocks with +the GIL. ## Thread-safe single initialization @@ -240,7 +243,7 @@ GIL-enabled build. If, for example, the function executed by [`GILOnceCell`] releases the GIL or calls code that releases the GIL, then it is possible for multiple threads to -try to race to initialize the cell. While the cell will only ever be intialized +race to initialize the cell. While the cell will only ever be intialized once, it can be problematic in some contexts that [`GILOnceCell`] does not block like the standard library [`OnceLock`]. From 735c773522bfaeeeff28b82b8fa5ae17ea52c0e3 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Mon, 11 Nov 2024 21:40:02 +0000 Subject: [PATCH 368/495] docs: add logo and mascot (#4697) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add logo and mascot * source files moved to branding repository --------- Co-authored-by: ⚫️ --- branding/favicon/pyo3_16x16.png | Bin 0 -> 456 bytes branding/favicon/pyo3_32x32.png | Bin 0 -> 737 bytes branding/pyo3logo.png | Bin 0 -> 348147 bytes branding/pyo3logo.svg | 1 + branding/pyotr.png | Bin 0 -> 345785 bytes branding/pyotr.svg | 1 + 6 files changed, 2 insertions(+) create mode 100644 branding/favicon/pyo3_16x16.png create mode 100644 branding/favicon/pyo3_32x32.png create mode 100644 branding/pyo3logo.png create mode 100644 branding/pyo3logo.svg create mode 100644 branding/pyotr.png create mode 100644 branding/pyotr.svg diff --git a/branding/favicon/pyo3_16x16.png b/branding/favicon/pyo3_16x16.png new file mode 100644 index 0000000000000000000000000000000000000000..0d2d77eb1516240061e2d11c20388ef95f24e78e GIT binary patch literal 456 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|Tv8)E(|mmy zw18|52FCVG1{RPKAeI7R1_tH@j10^`nh_+nfC(-uuz(rC1}QWNE&K#jX5{JO7-At< z8f2Jv$Uz`R`7>ubTc@6{h~|=pCZ$KrQ*0DYv0qx^C;XJT(&1E1gBC0AqJ<55LIEFG zc6szY?~Cj`f6nXL+rIR)_uuriyB2Nno+DPfEOGCwciE-U7ZqPTdc9O~pBCHZ^x1p# zc>ZtL=4wz-c=W{XyKMRU{&7h71TF6CI{54Eo!M`X@d~qEQPJMMSSZ@>C`+4?M+4jN}hY!!3bxd$k$9!z)E=8AJ>pQ^wa821 z&@{ml_V49*51$piU@UyxORKbLh*2~7airKCv! literal 0 HcmV?d00001 diff --git a/branding/favicon/pyo3_32x32.png b/branding/favicon/pyo3_32x32.png new file mode 100644 index 0000000000000000000000000000000000000000..ff1f97ae269f00ac32448a52c532120dc0941142 GIT binary patch literal 737 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdz#^NA%Cx&(BWL^R}E~ycoX}-P; zT0k}j17mw80}DtA5K93u0|WB{Mh0de%?J`(zyz07Sip>6gA}f5OZv>fz~t@e;usRa zc{Fsd7qg?lz7tPX7xgBjD|g-Ei1LVY$k{SY$K-->ECl=cDy-XW50QG_4IoQ20vE*+o(IGqTY8pv(DA&8A}Xo*E5;;Ox(H8 zqUQOA;`r#EpyZ-(zGGJ}{i>UBZ!Yi7m@DG4juS7*Y$>-}`(etqFxESF*W0n&-F1Kc zzW8mi{I8z$sqbP|QJvb$Ql{SS`!m?)eZ(K-hKF}dI`+Q{xlw!lnXG3pWAXQ6eOq_x zY)ogF7s|Pv=|*ne@e1Y}2?;X|@>FkxFeq&1N?E-xu6m8#wlsN-mSgJXj0ahCjJ*V! zD?=Ek1W%j7a`UhRZ~noH$1)`@%Lq$(PiFA?<087jK;z@z7g}5opDq6QSeW&>u-btZ zleR*0#!2?`^QHgY{_1Lcjrq||KjFQ77ReU{Cv?P3+7N6r&(cEVsKr%1bN9d%UsYW~ zW(n&aU29PwS6^{fy9Z{Ik&D`uZ-%pfbr3z=}1n(80jT7emny7Rp#&MSBqaz-248OEH zwmzw6JgM;Uv16h@ac|9|Agh}!3JND@RCi`1KS;fNCxuDj>5(M`CnIJzKfcG<9gvig z7!;MqB;X~ul(Ub+e^sT_JHd>D>)ann2Ja4LQmO2mG9!rnul~w6PkRy=qwj-~m#3?r J%Q~loCIEGLC7l2O literal 0 HcmV?d00001 diff --git a/branding/pyo3logo.png b/branding/pyo3logo.png new file mode 100644 index 0000000000000000000000000000000000000000..06cad61734ea5d0a16bad330d9ad689ab5b70c07 GIT binary patch literal 348147 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 z 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 literal 0 HcmV?d00001 diff --git a/branding/pyo3logo.svg b/branding/pyo3logo.svg new file mode 100644 index 00000000000..0315c63e56a --- /dev/null +++ b/branding/pyo3logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/branding/pyotr.png b/branding/pyotr.png new file mode 100644 index 0000000000000000000000000000000000000000..75ab9bb34b8b5e40f670349770fcb239736efad3 GIT binary patch literal 345785 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

+*t)Dlj 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 \ No newline at end of file From a42e53e888227724928c7f370cad47153a13b329 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Tue, 12 Nov 2024 00:01:13 -0500 Subject: [PATCH 369/495] Remove warning that's a bit outdated (#4698) I'm reasonably that at this point we've updated all the examples --- guide/src/index.md | 9 --------- 1 file changed, 9 deletions(-) diff --git a/guide/src/index.md b/guide/src/index.md index fe8b76b69c9..4e85d25d52b 100644 --- a/guide/src/index.md +++ b/guide/src/index.md @@ -10,15 +10,6 @@ The rough order of material in this user guide is as follows: Please choose from the chapters on the left to jump to individual topics, or continue below to start with PyO3's README. -

-
{{#include ../../README.md}} From f3eb62407b0ab98ba57906838e41f557c455ed06 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Thu, 14 Nov 2024 20:25:32 +0000 Subject: [PATCH 370/495] silence `non_upper_case_globals` lint on `__match_args__` (#4705) --- newsfragments/4705.fixed.md | 1 + pyo3-macros-backend/src/pyclass.rs | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) create mode 100644 newsfragments/4705.fixed.md diff --git a/newsfragments/4705.fixed.md b/newsfragments/4705.fixed.md new file mode 100644 index 00000000000..b7f49d40b0d --- /dev/null +++ b/newsfragments/4705.fixed.md @@ -0,0 +1 @@ +Fix `non_upper_case_globals` lint firing for generated `__match_args__` on complex enums. diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index 2dd4cbfab2e..747c0153b95 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -1186,19 +1186,21 @@ fn impl_complex_enum_variant_match_args( variant_cls_type: &syn::Type, field_names: &mut Vec, ) -> (MethodAndMethodDef, syn::ImplItemConst) { + let ident = format_ident!("__match_args__"); let match_args_const_impl: syn::ImplItemConst = { let args_tp = field_names.iter().map(|_| { quote! { &'static str } }); parse_quote! { - const __match_args__: ( #(#args_tp,)* ) = ( + #[allow(non_upper_case_globals)] + const #ident: ( #(#args_tp,)* ) = ( #(stringify!(#field_names),)* ); } }; let spec = ConstSpec { - rust_ident: format_ident!("__match_args__"), + rust_ident: ident, attributes: ConstAttributes { is_class_attr: true, name: None, From 147f7ef0891987c9633594d1bd0d612f50afba47 Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Thu, 14 Nov 2024 23:06:43 +0100 Subject: [PATCH 371/495] fix `clippy::wildcard_import` firing on `#[pyclass]` (#4707) --- pyo3-macros-backend/src/pyclass.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index 747c0153b95..a83ba880271 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -2324,11 +2324,11 @@ impl<'a> PyClassImplsBuilder<'a> { let assertions = if attr.options.unsendable.is_some() { TokenStream::new() } else { - quote_spanned! { - cls.span() => + let assert = quote_spanned! { cls.span() => assert_pyclass_sync::<#cls>(); }; + quote! { const _: () = { use #pyo3_path::impl_::pyclass::*; - assert_pyclass_sync::<#cls>(); + #assert }; } }; From ee229cfc3d64200684b58fd593ba037fe485d933 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Thu, 14 Nov 2024 22:46:39 +0000 Subject: [PATCH 372/495] simplify a few uses of `BoundObject` (#4706) --- src/coroutine.rs | 13 ++++++------- tests/test_methods.rs | 8 ++++---- tests/test_module.rs | 2 +- 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/src/coroutine.rs b/src/coroutine.rs index 8fff91dece1..aa4335e9d0b 100644 --- a/src/coroutine.rs +++ b/src/coroutine.rs @@ -137,14 +137,13 @@ impl Coroutine { } #[getter] - fn __qualname__(&self, py: Python<'_>) -> PyResult> { + fn __qualname__<'py>(&self, py: Python<'py>) -> PyResult> { match (&self.name, &self.qualname_prefix) { - (Some(name), Some(prefix)) => format!("{}.{}", prefix, name.bind(py).to_cow()?) - .as_str() - .into_pyobject(py) - .map(BoundObject::unbind) - .map_err(Into::into), - (Some(name), None) => Ok(name.clone_ref(py)), + (Some(name), Some(prefix)) => Ok(PyString::new( + py, + &format!("{}.{}", prefix, name.bind(py).to_cow()?), + )), + (Some(name), None) => Ok(name.bind(py).clone()), (None, _) => Err(PyAttributeError::new_err("__qualname__")), } } diff --git a/tests/test_methods.rs b/tests/test_methods.rs index 82258ab7b67..743fa6e6b4f 100644 --- a/tests/test_methods.rs +++ b/tests/test_methods.rs @@ -290,7 +290,7 @@ impl MethSignature { kwargs.into_pyobject(py)?.into_any().into_bound(), ] .into_pyobject(py) - .map(BoundObject::unbind) + .map(Bound::unbind) } #[pyo3(signature = (a=0, /, **kwargs))] @@ -305,7 +305,7 @@ impl MethSignature { kwargs.into_pyobject(py)?.into_any().into_bound(), ] .into_pyobject(py) - .map(BoundObject::unbind) + .map(Bound::unbind) } #[pyo3(signature = (*, a = 2, b = 3))] @@ -333,7 +333,7 @@ impl MethSignature { (args, a) .into_pyobject(py) .map(BoundObject::into_any) - .map(BoundObject::unbind) + .map(Bound::unbind) } #[pyo3(signature = (a, b = 2, *, c = 3))] @@ -358,7 +358,7 @@ impl MethSignature { kwargs.into_pyobject(py)?.into_any().into_bound(), ] .into_pyobject(py) - .map(BoundObject::unbind) + .map(Bound::unbind) } // "args" can be anything that can be extracted from PyTuple diff --git a/tests/test_module.rs b/tests/test_module.rs index 36d21abe9b0..35b7d354332 100644 --- a/tests/test_module.rs +++ b/tests/test_module.rs @@ -329,7 +329,7 @@ fn ext_vararg_fn(py: Python<'_>, a: i32, args: &Bound<'_, PyTuple>) -> PyResult< args.as_any().clone(), ] .into_pyobject(py) - .map(BoundObject::unbind) + .map(Bound::unbind) } #[pymodule] From 71100db6e9c2d94af3e27e31a4a66a0edc28cad8 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Fri, 15 Nov 2024 04:56:06 -0700 Subject: [PATCH 373/495] docs: add narrative docs for BoundObject (#4703) * docs: add narrative docs for BoundObject * grammar fixes * use where to break up signature * fix incorrect last sentence * Update guide/src/types.md Co-authored-by: Icxolu <10486322+Icxolu@users.noreply.github.com> * move example to traits.md and rework with a stronger motivation * fix cross-reference target * fix type * build result vec without copying * simplify example a little * update explanation sentence * fix API docs link --------- Co-authored-by: Icxolu <10486322+Icxolu@users.noreply.github.com> --- guide/src/conversions/traits.md | 68 +++++++++++++++++++++++++++++++++ guide/src/migration.md | 2 + 2 files changed, 70 insertions(+) diff --git a/guide/src/conversions/traits.md b/guide/src/conversions/traits.md index a0e6ec6db0e..5e3da6ea1b0 100644 --- a/guide/src/conversions/traits.md +++ b/guide/src/conversions/traits.md @@ -626,6 +626,73 @@ impl IntoPy for MyPyObjectWrapper { } ``` +#### `BoundObject` for conversions that may be `Bound` or `Borrowed` + +`IntoPyObject::into_py_object` returns either `Bound` or `Borrowed` depending on the implementation for a concrete type. For example, the `IntoPyObject` implementation for `u32` produces a `Bound<'py, PyInt>` and the `bool` implementation produces a `Borrowed<'py, 'py, PyBool>`: + +```rust +use pyo3::prelude::*; +use pyo3::IntoPyObject; +use pyo3::types::{PyBool, PyInt}; + +let ints: Vec = vec![1, 2, 3, 4]; +let bools = vec![true, false, false, true]; + +Python::with_gil(|py| { + let ints_as_pyint: Vec> = ints + .iter() + .map(|x| Ok(x.into_pyobject(py)?)) + .collect::>() + .unwrap(); + + let bools_as_pybool: Vec> = bools + .iter() + .map(|x| Ok(x.into_pyobject(py)?)) + .collect::>() + .unwrap(); +}); +``` + +In this example if we wanted to combine `ints_as_pyints` and `bools_as_pybool` into a single `Vec>` to return from the `with_gil` closure, we would have to manually convert the concrete types for the smart pointers and the python types. + +Instead, we can write a function that generically converts vectors of either integers or bools into a vector of `Py` using the [`BoundObject`] trait: + +```rust +# use pyo3::prelude::*; +# use pyo3::BoundObject; +# use pyo3::IntoPyObject; + +# let bools = vec![true, false, false, true]; +# let ints = vec![1, 2, 3, 4]; + +fn convert_to_vec_of_pyobj<'py, T>(py: Python<'py>, the_vec: Vec) -> PyResult>> +where + T: IntoPyObject<'py> + Copy +{ + the_vec.iter() + .map(|x| { + Ok( + x.into_pyobject(py) + .map_err(Into::into)? + .into_any() + .unbind() + ) + }) + .collect() +} + +let vec_of_pyobjs: Vec> = Python::with_gil(|py| { + let mut bools_as_pyany = convert_to_vec_of_pyobj(py, bools).unwrap(); + let mut ints_as_pyany = convert_to_vec_of_pyobj(py, ints).unwrap(); + let mut result: Vec> = vec![]; + result.append(&mut bools_as_pyany); + result.append(&mut ints_as_pyany); + result +}); +``` + +In the example above we used `BoundObject::into_any` and `BoundObject::unbind` to manipulate the python types and smart pointers into the result type we wanted to produce from the function. + ### The `ToPyObject` trait
@@ -647,3 +714,4 @@ same purpose, except that it consumes `self`. [`PyRef`]: {{#PYO3_DOCS_URL}}/pyo3/pycell/struct.PyRef.html [`PyRefMut`]: {{#PYO3_DOCS_URL}}/pyo3/pycell/struct.PyRefMut.html +[`BoundObject`]: {{#PYO3_DOCS_URL}}/pyo3/instance/trait.BoundObject.html diff --git a/guide/src/migration.md b/guide/src/migration.md index 1b8400604cb..3ad09f7eef0 100644 --- a/guide/src/migration.md +++ b/guide/src/migration.md @@ -157,6 +157,8 @@ need to adapt an implementation of `IntoPyObject` to stay compatible with the Py the new [`#[derive(IntoPyObject)]`](#intopyobject-derive-macro) macro can be used instead of [manual implementations](#intopyobject-manual-implementation). +Since `IntoPyObject::into_pyobject` may return either a `Bound` or `Borrowed`, you may find the [`BoundObject`](conversions/traits.md#boundobject-for-conversions-that-may-be-bound-or-borrowed) trait to be useful to write code that generically handles either type of smart pointer. + Together with the introduction of `IntoPyObject` the old conversion traits `ToPyObject` and `IntoPy` are deprecated and will be removed in a future PyO3 version. From e7ec730766460875f16016808c31f9f44fd6c804 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Fri, 15 Nov 2024 13:07:25 +0000 Subject: [PATCH 374/495] docs: extend documentation on `Sync` and thread-safety (#4695) * guide: extend documentation on `Sync` and thread-safety * Update guide/src/class/thread-safety.md Co-authored-by: Alex Gaynor * Apply suggestions from code review Co-authored-by: Bruno Kolenbrander <59372212+mejrs@users.noreply.github.com> Co-authored-by: Nathan Goldbaum * threadsafe -> thread-safe * datastructure -> data structure * fill out missing sections * remove dead paragraph * fix guide build --------- Co-authored-by: Alex Gaynor Co-authored-by: Bruno Kolenbrander <59372212+mejrs@users.noreply.github.com> Co-authored-by: Nathan Goldbaum --- guide/pyclass-parameters.md | 2 +- guide/src/SUMMARY.md | 1 + guide/src/class.md | 10 +- guide/src/class/thread-safety.md | 108 +++++++++++ guide/src/free-threading.md | 2 + guide/src/migration.md | 306 ++++++++++++++++--------------- src/instance.rs | 2 +- src/lib.rs | 1 + src/marker.rs | 2 +- src/pycell.rs | 2 +- 10 files changed, 283 insertions(+), 153 deletions(-) create mode 100644 guide/src/class/thread-safety.md diff --git a/guide/pyclass-parameters.md b/guide/pyclass-parameters.md index b471f5dd3ae..863f447080e 100644 --- a/guide/pyclass-parameters.md +++ b/guide/pyclass-parameters.md @@ -22,7 +22,7 @@ | `str` | Implements `__str__` using the `Display` implementation of the underlying Rust datatype or by passing an optional format string `str=""`. *Note: The optional format string is only allowed for structs. `name` and `rename_all` are incompatible with the optional format string. Additional details can be found in the discussion on this [PR](https://github.com/PyO3/pyo3/pull/4233).* | | `subclass` | Allows other Python classes and `#[pyclass]` to inherit from this class. Enums cannot be subclassed. | | `text_signature = "(arg1, arg2, ...)"` | Sets the text signature for the Python class' `__new__` method. | -| `unsendable` | Required if your struct is not [`Send`][params-3]. Rather than using `unsendable`, consider implementing your struct in a threadsafe way by e.g. substituting [`Rc`][params-4] with [`Arc`][params-5]. By using `unsendable`, your class will panic when accessed by another thread. Also note the Python's GC is multi-threaded and while unsendable classes will not be traversed on foreign threads to avoid UB, this can lead to memory leaks. | +| `unsendable` | Required if your struct is not [`Send`][params-3]. Rather than using `unsendable`, consider implementing your struct in a thread-safe way by e.g. substituting [`Rc`][params-4] with [`Arc`][params-5]. By using `unsendable`, your class will panic when accessed by another thread. Also note the Python's GC is multi-threaded and while unsendable classes will not be traversed on foreign threads to avoid UB, this can lead to memory leaks. | | `weakref` | Allows this class to be [weakly referenceable][params-6]. | All of these parameters can either be passed directly on the `#[pyclass(...)]` annotation, or as one or diff --git a/guide/src/SUMMARY.md b/guide/src/SUMMARY.md index f025d790b5d..f8cc899d75c 100644 --- a/guide/src/SUMMARY.md +++ b/guide/src/SUMMARY.md @@ -15,6 +15,7 @@ - [Basic object customization](class/object.md) - [Emulating numeric types](class/numeric.md) - [Emulating callable objects](class/call.md) + - [Thread safety](class/thread-safety.md) - [Calling Python from Rust](python-from-rust.md) - [Python object types](types.md) - [Python exceptions](exception.md) diff --git a/guide/src/class.md b/guide/src/class.md index 6a80fd7ad9c..5d2c8435416 100644 --- a/guide/src/class.md +++ b/guide/src/class.md @@ -73,7 +73,7 @@ The above example generates implementations for [`PyTypeInfo`] and [`PyClass`] f ### Restrictions -To integrate Rust types with Python, PyO3 needs to place some restrictions on the types which can be annotated with `#[pyclass]`. In particular, they must have no lifetime parameters, no generic parameters, and must implement `Send`. The reason for each of these is explained below. +To integrate Rust types with Python, PyO3 needs to place some restrictions on the types which can be annotated with `#[pyclass]`. In particular, they must have no lifetime parameters, no generic parameters, and must be thread-safe. The reason for each of these is explained below. #### No lifetime parameters @@ -119,9 +119,13 @@ create_interface!(IntClass, i64); create_interface!(FloatClass, String); ``` -#### Must be Send +#### Must be thread-safe -Because Python objects are freely shared between threads by the Python interpreter, there is no guarantee which thread will eventually drop the object. Therefore all types annotated with `#[pyclass]` must implement `Send` (unless annotated with [`#[pyclass(unsendable)]`](#customizing-the-class)). +Python objects are freely shared between threads by the Python interpreter. This means that: +- Python objects may be created and destroyed by different Python threads; therefore #[pyclass]` objects must be `Send`. +- Python objects may be accessed by multiple python threads simultaneously; therefore `#[pyclass]` objects must be `Sync`. + +For now, don't worry about these requirements; simple classes will already be thread-safe. There is a [detailed discussion on thread-safety](./class/thread-safety.md) later in the guide. ## Constructor diff --git a/guide/src/class/thread-safety.md b/guide/src/class/thread-safety.md new file mode 100644 index 00000000000..841ee6ae2db --- /dev/null +++ b/guide/src/class/thread-safety.md @@ -0,0 +1,108 @@ +# `#[pyclass]` thread safety + +Python objects are freely shared between threads by the Python interpreter. This means that: +- there is no control which thread might eventually drop the `#[pyclass]` object, meaning `Send` is required. +- multiple threads can potentially be reading the `#[pyclass]` data simultaneously, meaning `Sync` is required. + +This section of the guide discusses various data structures which can be used to make types satisfy these requirements. + +In special cases where it is known that your Python application is never going to use threads (this is rare!), these thread-safety requirements can be opted-out with [`#[pyclass(unsendable)]`](../class.md#customizing-the-class), at the cost of making concurrent access to the Rust data be runtime errors. This is only for very specific use cases; it is almost always better to make proper thread-safe types. + +## Making `#[pyclass]` types thread-safe + +The general challenge with thread-safety is to make sure that two threads cannot produce a data race, i.e. unsynchronized writes to the same data at the same time. A data race produces an unpredictable result and is forbidden by Rust. + +By default, `#[pyclass]` employs an ["interior mutability" pattern](../class.md#bound-and-interior-mutability) to allow for either multiple `&T` references or a single exclusive `&mut T` reference to access the data. This allows for simple `#[pyclass]` types to be thread-safe automatically, at the cost of runtime checking for concurrent access. Errors will be raised if the usage overlaps. + +For example, the below simple class is thread-safe: + +```rust +# use pyo3::prelude::*; + +#[pyclass] +struct MyClass { + x: i32, + y: i32, +} + +#[pymethods] +impl MyClass { + fn get_x(&self) -> i32 { + self.x + } + + fn set_y(&mut self, value: i32) { + self.y = value; + } +} +``` + +In the above example, if calls to `get_x` and `set_y` overlap (from two different threads) then at least one of those threads will experience a runtime error indicating that the data was "already borrowed". + +To avoid these errors, you can take control of the interior mutability yourself in one of the following ways. + +### Using atomic data structures + +To remove the possibility of having overlapping `&self` and `&mut self` references produce runtime errors, consider using `#[pyclass(frozen)]` and use [atomic data structures](https://doc.rust-lang.org/std/sync/atomic/) to control modifications directly. + +For example, a thread-safe version of the above `MyClass` using atomic integers would be as follows: + +```rust +# use pyo3::prelude::*; +use std::sync::atomic::{AtomicI32, Ordering}; + +#[pyclass(frozen)] +struct MyClass { + x: AtomicI32, + y: AtomicI32, +} + +#[pymethods] +impl MyClass { + fn get_x(&self) -> i32 { + self.x.load(Ordering::Relaxed) + } + + fn set_y(&self, value: i32) { + self.y.store(value, Ordering::Relaxed) + } +} +``` + +### Using locks + +An alternative to atomic data structures is to use [locks](https://doc.rust-lang.org/std/sync/struct.Mutex.html) to make threads wait for access to shared data. + +For example, a thread-safe version of the above `MyClass` using locks would be as follows: + +```rust +# use pyo3::prelude::*; +use std::sync::Mutex; + +struct MyClassInner { + x: i32, + y: i32, +} + +#[pyclass(frozen)] +struct MyClass { + inner: Mutex +} + +#[pymethods] +impl MyClass { + fn get_x(&self) -> i32 { + self.inner.lock().expect("lock not poisoned").x + } + + fn set_y(&self, value: i32) { + self.inner.lock().expect("lock not poisoned").y = value; + } +} +``` + +### Wrapping unsynchronized data + +In some cases, the data structures stored within a `#[pyclass]` may themselves not be thread-safe. Rust will therefore not implement `Send` and `Sync` on the `#[pyclass]` type. + +To achieve thread-safety, a manual `Send` and `Sync` implementation is required which is `unsafe` and should only be done following careful review of the soundness of the implementation. Doing this for PyO3 types is no different than for any other Rust code, [the Rustonomicon](https://doc.rust-lang.org/nomicon/send-and-sync.html) has a great discussion on this. diff --git a/guide/src/free-threading.md b/guide/src/free-threading.md index 4a326e58e98..d867a707795 100644 --- a/guide/src/free-threading.md +++ b/guide/src/free-threading.md @@ -49,6 +49,8 @@ annotate Python modules declared by rust code in your project to declare that they support free-threaded Python, for example by declaring the module with `#[pymodule(gil_used = false)]`. +More complicated `#[pyclass]` types may need to deal with thread-safety directly; there is [a dedicated section of the guide](./class/thread-safety.md) to discuss this. + At a low-level, annotating a module sets the `Py_MOD_GIL` slot on modules defined by an extension to `Py_MOD_GIL_NOT_USED`, which allows the interpreter to see at runtime that the author of the extension thinks the extension is diff --git a/guide/src/migration.md b/guide/src/migration.md index 3ad09f7eef0..8a8f3694f6d 100644 --- a/guide/src/migration.md +++ b/guide/src/migration.md @@ -4,107 +4,138 @@ This guide can help you upgrade code through breaking changes from one PyO3 vers For a detailed list of all changes, see the [CHANGELOG](changelog.md). ## from 0.22.* to 0.23 +
+Click to expand -### `gil-refs` feature removed +PyO3 0.23 is a significant rework of PyO3's internals for two major improvements: + - Support of Python 3.13's new freethreaded build (aka "3.13t") + - Rework of to-Python conversions with a new `IntoPyObject` trait. + +These changes are both substantial and reasonable efforts have been made to allow as much code as possible to continue to work as-is despite the changes. The impacts are likely to be seen in three places when upgrading: + - PyO3's data structures [are now thread-safe](#free-threaded-python-support) instead of reliant on the GIL for synchronization. In particular, `#[pyclass]` types are [now required to be `Sync`](./class/thread-safety.md). + - The [`IntoPyObject` trait](#new-intopyobject-trait-unifies-to-python-conversions) may need to be implemented for types in your codebase. In most cases this can simply be done with [`#[derive(IntoPyObject)]`](#intopyobject-and-intopyobjectref-derive-macros). There will be many deprecation warnings from the replacement of `IntoPy` and `ToPyObject` traits. + - There will be many deprecation warnings from the [final removal of the `gil-refs` feature](#gil-refs-feature-removed), which opened up API space for a cleanup and simplification to PyO3's "Bound" API. + +The sections below discuss the rationale and details of each change in more depth. +
+ +### Free-threaded Python Support
Click to expand -PyO3 0.23 completes the removal of the "GIL Refs" API in favour of the new "Bound" API introduced in PyO3 0.21. +PyO3 0.23 introduces initial support for the new free-threaded build of +CPython 3.13, aka "3.13t". -With the removal of the old API, many "Bound" API functions which had been introduced with `_bound` suffixes no longer need the suffixes as these names has been freed up. For example, `PyTuple::new_bound` is now just `PyTuple::new` (the existing name remains but is deprecated). +Because this build allows multiple Python threads to operate simultaneously on underlying Rust data, the `#[pyclass]` macro now requires that types it operates on implement `Sync`. -Before: +Aside from the change to `#[pyclass]`, most features of PyO3 work unchanged, as the changes have been to the internal data structures to make them thread-safe. An example of this is the `GILOnceCell` type, which used the GIL to synchronize single-initialization. It now uses internal locks to guarantee that only one write ever succeeds, however it allows for multiple racing runs of the initialization closure. It may be preferable to instead use `std::sync::OnceLock` in combination with the `pyo3::sync::OnceLockExt` trait which adds `OnceLock::get_or_init_py_attached` for single-initialization where the initialization closure is guaranteed only ever to run once and without deadlocking with the GIL. -```rust -# #![allow(deprecated)] -# use pyo3::prelude::*; -# use pyo3::types::PyTuple; -# fn main() { -# Python::with_gil(|py| { -// For example, for PyTuple. Many such APIs have been changed. -let tup = PyTuple::new_bound(py, [1, 2, 3]); -# }) -# } -``` +Future PyO3 versions will likely add more traits and data structures to make working with free-threaded Python easier. -After: +Some features are unaccessible on the free-threaded build: + - The `GILProtected` type, which relied on the GIL to expose synchronized access to inner contents + - `PyList::get_item_unchecked`, which cannot soundly be used due to races between time-of-check and time-of-use -```rust -# use pyo3::prelude::*; -# use pyo3::types::PyTuple; -# fn main() { -# Python::with_gil(|py| { -// For example, for PyTuple. Many such APIs have been changed. -let tup = PyTuple::new(py, [1, 2, 3]); -# }) -# } -``` +If you make use of these features then you will need to account for the +unavailability of the API in the free-threaded build. One way to handle it is via conditional compilation -- extensions can use `pyo3-build-config` to get access to a `#[cfg(Py_GIL_DISABLED)]` guard. + +See [the guide section on free-threaded Python](free-threading.md) for more details about supporting free-threaded Python in your PyO3 extensions.
-### Renamed `IntoPyDict::into_py_dict_bound` into `IntoPyDict::into_py_dict`. +### New `IntoPyObject` trait unifies to-Python conversions
Click to expand -The `IntoPyDict::into_py_dict_bound` method has been renamed to `IntoPyDict::into_py_dict` and is now fallible. If you implemented `IntoPyDict` for your type, you should implement `into_py_dict` instead of `into_py_dict_bound`. The old name is still available but deprecated. +PyO3 0.23 introduces a new `IntoPyObject` trait to convert Rust types into Python objects which replaces both `IntoPy` and `ToPyObject`. +Notable features of this new trait include: +- conversions can now return an error +- it is designed to work efficiently for both `T` owned types and `&T` references +- compared to `IntoPy` the generic `T` moved into an associated type, so + - there is now only one way to convert a given type + - the output type is stronger typed and may return any Python type instead of just `PyAny` +- byte collections are specialized to convert into `PyBytes` now, see [below](#to-python-conversions-changed-for-byte-collections-vecu8-u8-n-and-smallvecu8-n) +- `()` (unit) is now only specialized in return position of `#[pyfunction]` and `#[pymethods]` to return `None`, in normal usage it converts into an empty `PyTuple` -Before: +All PyO3 provided types as well as `#[pyclass]`es already implement `IntoPyObject`. Other types will +need to adapt an implementation of `IntoPyObject` to stay compatible with the Python APIs. In many cases +the new [`#[derive(IntoPyObject)]`](#intopyobject-and-intopyobjectref-derive-macros) macro can be used instead of +[manual implementations](#intopyobject-manual-implementation). + +Since `IntoPyObject::into_pyobject` may return either a `Bound` or `Borrowed`, you may find the [`BoundObject`](conversions/traits.md#boundobject-for-conversions-that-may-be-bound-or-borrowed) trait to be useful to write code that generically handles either type of smart pointer. + +Together with the introduction of `IntoPyObject` the old conversion traits `ToPyObject` and `IntoPy` +are deprecated and will be removed in a future PyO3 version. + +#### `IntoPyObject` and `IntoPyObjectRef` derive macros + +To implement the new trait you may use the new `IntoPyObject` and `IntoPyObjectRef` derive macros as below. + +```rust +# use pyo3::prelude::*; +#[derive(IntoPyObject, IntoPyObjectRef)] +struct Struct { + count: usize, + obj: Py, +} +``` + +The `IntoPyObjectRef` derive macro derives implementations for references (e.g. for `&Struct` in the example above), which is a replacement for the `ToPyObject` trait. + +#### `IntoPyObject` manual implementation +Before: ```rust,ignore # use pyo3::prelude::*; -# use pyo3::types::{PyDict, IntoPyDict}; -# use std::collections::HashMap; +# #[allow(dead_code)] +struct MyPyObjectWrapper(PyObject); -struct MyMap(HashMap); +impl IntoPy for MyPyObjectWrapper { + fn into_py(self, py: Python<'_>) -> PyObject { + self.0 + } +} -impl IntoPyDict for MyMap -where - K: ToPyObject, - V: ToPyObject, -{ - fn into_py_dict_bound(self, py: Python<'_>) -> Bound<'_, PyDict> { - let dict = PyDict::new_bound(py); - for (key, value) in self.0 { - dict.set_item(key, value) - .expect("Failed to set_item on dict"); - } - dict +impl ToPyObject for MyPyObjectWrapper { + fn to_object(&self, py: Python<'_>) -> PyObject { + self.0.clone_ref(py) } } ``` After: - ```rust # use pyo3::prelude::*; -# use pyo3::types::{PyDict, IntoPyDict}; -# use std::collections::HashMap; - # #[allow(dead_code)] -# struct MyMap(HashMap); +# struct MyPyObjectWrapper(PyObject); -impl<'py, K, V> IntoPyDict<'py> for MyMap -where - K: IntoPyObject<'py>, - V: IntoPyObject<'py>, -{ - fn into_py_dict(self, py: Python<'py>) -> PyResult> { - let dict = PyDict::new(py); - for (key, value) in self.0 { - dict.set_item(key, value)?; - } - Ok(dict) +impl<'py> IntoPyObject<'py> for MyPyObjectWrapper { + type Target = PyAny; // the Python type + type Output = Bound<'py, Self::Target>; // in most cases this will be `Bound` + type Error = std::convert::Infallible; + + fn into_pyobject(self, py: Python<'py>) -> Result { + Ok(self.0.into_bound(py)) + } +} + +// `ToPyObject` implementations should be converted to implementations on reference types +impl<'a, 'py> IntoPyObject<'py> for &'a MyPyObjectWrapper { + type Target = PyAny; + type Output = Borrowed<'a, 'py, Self::Target>; // `Borrowed` can be used to optimized reference counting + type Error = std::convert::Infallible; + + fn into_pyobject(self, py: Python<'py>) -> Result { + Ok(self.0.bind_borrowed(py)) } } ```
-### Macro conversion changed for byte collections (`Vec`, `[u8; N]` and `SmallVec<[u8; N]>`). +### To-Python conversions changed for byte collections (`Vec`, `[u8; N]` and `SmallVec<[u8; N]>`).
Click to expand -PyO3 0.23 introduced the new fallible conversion trait `IntoPyObject`. The `#[pyfunction]` and -`#[pymethods]` macros prefer `IntoPyObject` implementations over `IntoPy`. This also -applies to `#[pyo3(get)]`. +With the introduction of the `IntoPyObject` trait, PyO3's macros now prefer `IntoPyObject` implementations over `IntoPy` when producing Python values. This applies to `#[pyfunction]` and `#[pymethods]` return values and also fields accessed via `#[pyo3(get)]`. This change has an effect on functions and methods returning _byte_ collections like - `Vec` @@ -130,8 +161,7 @@ fn bar() -> Vec { // unaffected, returns `PyList` If this conversion is _not_ desired, consider building a list manually using `PyList::new`. -The following types were previously _only_ implemented for `u8` and now allow other `T`s turn into -`PyList` +The following types were previously _only_ implemented for `u8` and now allow other `T`s turn into `PyList`: - `&[T]` - `Cow<[T]>` @@ -139,118 +169,102 @@ This is purely additional and should just extend the possible return types.
-### Python API trait bounds changed +### `gil-refs` feature removed
Click to expand -PyO3 0.23 introduces a new unified `IntoPyObject` trait to convert Rust types into Python objects. -Notable features of this new trait: -- conversions can now return an error -- compared to `IntoPy` the generic `T` moved into an associated type, so - - there is now only one way to convert a given type - - the output type is stronger typed and may return any Python type instead of just `PyAny` -- byte collections are special handled and convert into `PyBytes` now, see [above](#macro-conversion-changed-for-byte-collections-vecu8-u8-n-and-smallvecu8-n) -- `()` (unit) is now only special handled in return position and otherwise converts into an empty `PyTuple` - -All PyO3 provided types as well as `#[pyclass]`es already implement `IntoPyObject`. Other types will -need to adapt an implementation of `IntoPyObject` to stay compatible with the Python APIs. In many cases -the new [`#[derive(IntoPyObject)]`](#intopyobject-derive-macro) macro can be used instead of -[manual implementations](#intopyobject-manual-implementation). +PyO3 0.23 completes the removal of the "GIL Refs" API in favour of the new "Bound" API introduced in PyO3 0.21. -Since `IntoPyObject::into_pyobject` may return either a `Bound` or `Borrowed`, you may find the [`BoundObject`](conversions/traits.md#boundobject-for-conversions-that-may-be-bound-or-borrowed) trait to be useful to write code that generically handles either type of smart pointer. +With the removal of the old API, many "Bound" API functions which had been introduced with `_bound` suffixes no longer need the suffixes as these names have been freed up. For example, `PyTuple::new_bound` is now just `PyTuple::new` (the existing name remains but is deprecated). -Together with the introduction of `IntoPyObject` the old conversion traits `ToPyObject` and `IntoPy` -are deprecated and will be removed in a future PyO3 version. +Before: -#### `IntoPyObject` derive macro +```rust +# #![allow(deprecated)] +# use pyo3::prelude::*; +# use pyo3::types::PyTuple; +# fn main() { +# Python::with_gil(|py| { +// For example, for PyTuple. Many such APIs have been changed. +let tup = PyTuple::new_bound(py, [1, 2, 3]); +# }) +# } +``` -To migrate you may use the new `IntoPyObject` derive macro as below. +After: ```rust # use pyo3::prelude::*; -#[derive(IntoPyObject)] -struct Struct { - count: usize, - obj: Py, -} +# use pyo3::types::PyTuple; +# fn main() { +# Python::with_gil(|py| { +// For example, for PyTuple. Many such APIs have been changed. +let tup = PyTuple::new(py, [1, 2, 3]); +# }) +# } ``` +#### `IntoPyDict` trait adjusted for removal of `gil-refs` -#### `IntoPyObject` manual implementation +As part of this API simplification, the `IntoPyDict` trait has had a small breaking change: `IntoPyDict::into_py_dict_bound` method has been renamed to `IntoPyDict::into_py_dict`. It is also now fallible as part of the `IntoPyObject` trait addition. + +If you implemented `IntoPyDict` for your type, you should implement `into_py_dict` instead of `into_py_dict_bound`. The old name is still available for calling but deprecated. Before: + ```rust,ignore # use pyo3::prelude::*; -# #[allow(dead_code)] -struct MyPyObjectWrapper(PyObject); +# use pyo3::types::{PyDict, IntoPyDict}; +# use std::collections::HashMap; -impl IntoPy for MyPyObjectWrapper { - fn into_py(self, py: Python<'_>) -> PyObject { - self.0 - } -} +struct MyMap(HashMap); -impl ToPyObject for MyPyObjectWrapper { - fn to_object(&self, py: Python<'_>) -> PyObject { - self.0.clone_ref(py) +impl IntoPyDict for MyMap +where + K: ToPyObject, + V: ToPyObject, +{ + fn into_py_dict_bound(self, py: Python<'_>) -> Bound<'_, PyDict> { + let dict = PyDict::new_bound(py); + for (key, value) in self.0 { + dict.set_item(key, value) + .expect("Failed to set_item on dict"); + } + dict } } ``` After: + ```rust # use pyo3::prelude::*; -# #[allow(dead_code)] -# struct MyPyObjectWrapper(PyObject); - -impl<'py> IntoPyObject<'py> for MyPyObjectWrapper { - type Target = PyAny; // the Python type - type Output = Bound<'py, Self::Target>; // in most cases this will be `Bound` - type Error = std::convert::Infallible; - - fn into_pyobject(self, py: Python<'py>) -> Result { - Ok(self.0.into_bound(py)) - } -} +# use pyo3::types::{PyDict, IntoPyDict}; +# use std::collections::HashMap; -// `ToPyObject` implementations should be converted to implementations on reference types -impl<'a, 'py> IntoPyObject<'py> for &'a MyPyObjectWrapper { - type Target = PyAny; - type Output = Borrowed<'a, 'py, Self::Target>; // `Borrowed` can be used to optimized reference counting - type Error = std::convert::Infallible; +# #[allow(dead_code)] +struct MyMap(HashMap); - fn into_pyobject(self, py: Python<'py>) -> Result { - Ok(self.0.bind_borrowed(py)) +impl<'py, K, V> IntoPyDict<'py> for MyMap +where + K: IntoPyObject<'py>, + V: IntoPyObject<'py>, +{ + fn into_py_dict(self, py: Python<'py>) -> PyResult> { + let dict = PyDict::new(py); + for (key, value) in self.0 { + dict.set_item(key, value)?; + } + Ok(dict) } } ```
-### Free-threaded Python Support - -PyO3 0.23 introduces preliminary support for the new free-threaded build of -CPython 3.13. PyO3 features that implicitly assumed the existence of the GIL are -not exposed in the free-threaded build, since they are no longer safe. Other -features, such as `GILOnceCell`, have been internally rewritten to be threadsafe -without the GIL, although note that `GILOnceCell` is inherently racey. You can -also use `OnceExt::call_once_py_attached` or -`OnceExt::call_once_force_py_attached` to enable use of `std::sync::Once` in -code that has the GIL acquired without risking a dealock with the GIL. We plan -We plan to expose more extension traits in the future that make it easier to -write code for the GIL-enabled and free-threaded builds of Python. - -If you make use of these features then you will need to account for the -unavailability of this API in the free-threaded build. One way to handle it is -via conditional compilation -- extensions built for the free-threaded build will -have the `Py_GIL_DISABLED` attribute defined. - -See [the guide section on free-threaded Python](free-threading.md) for more -details about supporting free-threaded Python in your PyO3 extensions. - ## from 0.21.* to 0.22 ### Deprecation of `gil-refs` feature continues -
+
Click to expand Following the introduction of the "Bound" API in PyO3 0.21 and the planned removal of the "GIL Refs" API, all functionality related to GIL Refs is now gated behind the `gil-refs` feature and emits a deprecation warning on use. @@ -259,7 +273,7 @@ See the 0.21 migration entry for help upgrading.
### Deprecation of implicit default for trailing optional arguments -
+
Click to expand With `pyo3` 0.22 the implicit `None` default for trailing `Option` type argument is deprecated. To migrate, place a `#[pyo3(signature = (...))]` attribute on affected functions or methods and specify the desired behavior. @@ -291,7 +305,7 @@ fn increment(x: u64, amount: Option) -> u64 {
### `Py::clone` is now gated behind the `py-clone` feature -
+
Click to expand If you rely on `impl Clone for Py` to fulfil trait requirements imposed by existing Rust code written without PyO3-based code in mind, the newly introduced feature `py-clone` must be enabled. @@ -303,7 +317,7 @@ Related to this, we also added a `pyo3_disable_reference_pool` conditional compi
### Require explicit opt-in for comparison for simple enums -
+
Click to expand With `pyo3` 0.22 the new `#[pyo3(eq)]` options allows automatic implementation of Python equality using Rust's `PartialEq`. Previously simple enums automatically implemented equality in terms of their discriminants. To make PyO3 more consistent, this automatic equality implementation is deprecated in favour of having opt-ins for all `#[pyclass]` types. Similarly, simple enums supported comparison with integers, which is not covered by Rust's `PartialEq` derive, so has been split out into the `#[pyo3(eq_int)]` attribute. @@ -337,7 +351,7 @@ enum SimpleEnum {
### `PyType::name` reworked to better match Python `__name__` -
+
Click to expand This function previously would try to read directly from Python type objects' C API field (`tp_name`), in which case it diff --git a/src/instance.rs b/src/instance.rs index 99643e12eb1..14d2d11b5d7 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -1059,7 +1059,7 @@ impl<'a, 'py, T> BoundObject<'py, T> for Borrowed<'a, 'py, T> { /// /// # A note on `Send` and `Sync` /// -/// Accessing this object is threadsafe, since any access to its API requires a [`Python<'py>`](crate::Python) token. +/// Accessing this object is thread-safe, since any access to its API requires a [`Python<'py>`](crate::Python) token. /// As you can only get this by acquiring the GIL, `Py<...>` implements [`Send`] and [`Sync`]. /// /// [`Rc`]: std::rc::Rc diff --git a/src/lib.rs b/src/lib.rs index c71e12b8649..265824adab1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -506,6 +506,7 @@ pub mod doc_test { "guide/src/class/object.md" => guide_class_object, "guide/src/class/numeric.md" => guide_class_numeric, "guide/src/class/protocols.md" => guide_class_protocols_md, + "guide/src/class/thread-safety.md" => guide_class_thread_safety_md, "guide/src/conversions.md" => guide_conversions_md, "guide/src/conversions/tables.md" => guide_conversions_tables_md, "guide/src/conversions/traits.md" => guide_conversions_traits_md, diff --git a/src/marker.rs b/src/marker.rs index 1a4c0482569..5962b47b60b 100644 --- a/src/marker.rs +++ b/src/marker.rs @@ -1,6 +1,6 @@ //! Fundamental properties of objects tied to the Python interpreter. //! -//! The Python interpreter is not threadsafe. To protect the Python interpreter in multithreaded +//! The Python interpreter is not thread-safe. To protect the Python interpreter in multithreaded //! scenarios there is a global lock, the *global interpreter lock* (hereafter referred to as *GIL*) //! that must be held to safely interact with Python objects. This is why in PyO3 when you acquire //! the GIL you get a [`Python`] marker token that carries the *lifetime* of holding the GIL and all diff --git a/src/pycell.rs b/src/pycell.rs index 51c9f201068..c7e5226a292 100644 --- a/src/pycell.rs +++ b/src/pycell.rs @@ -7,7 +7,7 @@ //! PyO3 deals with these differences by employing the [Interior Mutability] //! pattern. This requires that PyO3 enforces the borrowing rules and it has two mechanisms for //! doing so: -//! - Statically it can enforce threadsafe access with the [`Python<'py>`](crate::Python) token. +//! - Statically it can enforce thread-safe access with the [`Python<'py>`](crate::Python) token. //! All Rust code holding that token, or anything derived from it, can assume that they have //! safe access to the Python interpreter's state. For this reason all the native Python objects //! can be mutated through shared references. From 8e9b497b78b35ad8289d2052b32d996f52fdb588 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Fri, 15 Nov 2024 14:41:13 +0000 Subject: [PATCH 375/495] release: 0.23.0 (#4651) --- CHANGELOG.md | 100 +++++++++++++++++++++++++++++++- Cargo.toml | 8 +-- newsfragments/4060.added.md | 1 - newsfragments/4060.changed.md | 1 - newsfragments/4233.added.md | 1 - newsfragments/4243.changed.md | 1 - newsfragments/4308.changed.md | 1 - newsfragments/4317.added.md | 1 - newsfragments/4322.changed.md | 1 - newsfragments/4322.removed.md | 1 - newsfragments/4323.removed.md | 1 - newsfragments/4347.changed.md | 1 - newsfragments/4348.added.md | 1 - newsfragments/4351.added.md | 1 - newsfragments/4370.removed.md | 1 - newsfragments/4378.removed.md | 1 - newsfragments/4388.changed.md | 1 - newsfragments/4389.fixed.md | 1 - newsfragments/4404.changed.md | 1 - newsfragments/4415.added.md | 1 - newsfragments/4415.changed.md | 1 - newsfragments/4421.added.md | 1 - newsfragments/4421.fixed.md | 1 - newsfragments/4434.changed.md | 5 -- newsfragments/4435.changed.md | 1 - newsfragments/4439.changed.md | 3 - newsfragments/4441.changed.md | 1 - newsfragments/4442.changed.md | 2 - newsfragments/4447.added.md | 1 - newsfragments/4447.fixed.md | 1 - newsfragments/4447.removed.md | 1 - newsfragments/4453.changed.md | 1 - newsfragments/4477.added.md | 1 - newsfragments/4493.changed.md | 1 - newsfragments/4495.added.md | 1 - newsfragments/4497.changed.md | 1 - newsfragments/4504.changed.md | 2 - newsfragments/4509.fixed.md | 1 - newsfragments/4512.changed.md | 1 - newsfragments/4520.added.md | 1 - newsfragments/4521.removed.md | 1 - newsfragments/4528.removed.md | 7 --- newsfragments/4529.added.md | 1 - newsfragments/4534.changed.md | 3 - newsfragments/4534.removed.md | 1 - newsfragments/4539.removed.md | 3 - newsfragments/4542.changed.md | 1 - newsfragments/4544.changed.md | 2 - newsfragments/4553.fixed.md | 1 - newsfragments/4566.changed.md | 5 -- newsfragments/4567.fixed.md | 1 - newsfragments/4577.added.md | 1 - newsfragments/4580.changed.md | 1 - newsfragments/4582.packaging.md | 1 - newsfragments/4587.added.md | 2 - newsfragments/4588.added.md | 3 - newsfragments/4595.changed.md | 2 - newsfragments/4597.changed.md | 1 - newsfragments/4598.changed.md | 1 - newsfragments/4604.packaging.md | 1 - newsfragments/4606.changed.md | 1 - newsfragments/4611.changed.md | 1 - newsfragments/4617.packaging.md | 13 ----- newsfragments/4618.changed.md | 1 - newsfragments/4623.fixed.md | 1 - newsfragments/4634.added.md | 1 - newsfragments/4644.added.md | 1 - newsfragments/4645.fixed.md | 1 - newsfragments/4654.fixed.md | 1 - newsfragments/4655.changed.md | 1 - newsfragments/4657.changed.md | 1 - newsfragments/4661.changed.md | 1 - newsfragments/4665.changed.md | 2 - newsfragments/4667.added.md | 1 - newsfragments/4671.fixed.md | 1 - newsfragments/4674.fixed.md | 1 - newsfragments/4676.added.md | 1 - newsfragments/4692.fixed.md | 1 - newsfragments/4694.fixed.md | 1 - newsfragments/4705.fixed.md | 1 - pyo3-build-config/Cargo.toml | 2 +- pyo3-ffi/Cargo.toml | 4 +- pyo3-macros-backend/Cargo.toml | 6 +- pyo3-macros/Cargo.toml | 4 +- pyproject.toml | 2 +- tests/ui/reject_generics.stderr | 4 +- 86 files changed, 114 insertions(+), 134 deletions(-) delete mode 100644 newsfragments/4060.added.md delete mode 100644 newsfragments/4060.changed.md delete mode 100644 newsfragments/4233.added.md delete mode 100644 newsfragments/4243.changed.md delete mode 100644 newsfragments/4308.changed.md delete mode 100644 newsfragments/4317.added.md delete mode 100644 newsfragments/4322.changed.md delete mode 100644 newsfragments/4322.removed.md delete mode 100644 newsfragments/4323.removed.md delete mode 100644 newsfragments/4347.changed.md delete mode 100644 newsfragments/4348.added.md delete mode 100644 newsfragments/4351.added.md delete mode 100644 newsfragments/4370.removed.md delete mode 100644 newsfragments/4378.removed.md delete mode 100644 newsfragments/4388.changed.md delete mode 100644 newsfragments/4389.fixed.md delete mode 100644 newsfragments/4404.changed.md delete mode 100644 newsfragments/4415.added.md delete mode 100644 newsfragments/4415.changed.md delete mode 100644 newsfragments/4421.added.md delete mode 100644 newsfragments/4421.fixed.md delete mode 100644 newsfragments/4434.changed.md delete mode 100644 newsfragments/4435.changed.md delete mode 100644 newsfragments/4439.changed.md delete mode 100644 newsfragments/4441.changed.md delete mode 100644 newsfragments/4442.changed.md delete mode 100644 newsfragments/4447.added.md delete mode 100644 newsfragments/4447.fixed.md delete mode 100644 newsfragments/4447.removed.md delete mode 100644 newsfragments/4453.changed.md delete mode 100644 newsfragments/4477.added.md delete mode 100644 newsfragments/4493.changed.md delete mode 100644 newsfragments/4495.added.md delete mode 100644 newsfragments/4497.changed.md delete mode 100644 newsfragments/4504.changed.md delete mode 100644 newsfragments/4509.fixed.md delete mode 100644 newsfragments/4512.changed.md delete mode 100644 newsfragments/4520.added.md delete mode 100644 newsfragments/4521.removed.md delete mode 100644 newsfragments/4528.removed.md delete mode 100644 newsfragments/4529.added.md delete mode 100644 newsfragments/4534.changed.md delete mode 100644 newsfragments/4534.removed.md delete mode 100644 newsfragments/4539.removed.md delete mode 100644 newsfragments/4542.changed.md delete mode 100644 newsfragments/4544.changed.md delete mode 100644 newsfragments/4553.fixed.md delete mode 100644 newsfragments/4566.changed.md delete mode 100644 newsfragments/4567.fixed.md delete mode 100644 newsfragments/4577.added.md delete mode 100644 newsfragments/4580.changed.md delete mode 100644 newsfragments/4582.packaging.md delete mode 100644 newsfragments/4587.added.md delete mode 100644 newsfragments/4588.added.md delete mode 100644 newsfragments/4595.changed.md delete mode 100644 newsfragments/4597.changed.md delete mode 100644 newsfragments/4598.changed.md delete mode 100644 newsfragments/4604.packaging.md delete mode 100644 newsfragments/4606.changed.md delete mode 100644 newsfragments/4611.changed.md delete mode 100644 newsfragments/4617.packaging.md delete mode 100644 newsfragments/4618.changed.md delete mode 100644 newsfragments/4623.fixed.md delete mode 100644 newsfragments/4634.added.md delete mode 100644 newsfragments/4644.added.md delete mode 100644 newsfragments/4645.fixed.md delete mode 100644 newsfragments/4654.fixed.md delete mode 100644 newsfragments/4655.changed.md delete mode 100644 newsfragments/4657.changed.md delete mode 100644 newsfragments/4661.changed.md delete mode 100644 newsfragments/4665.changed.md delete mode 100644 newsfragments/4667.added.md delete mode 100644 newsfragments/4671.fixed.md delete mode 100644 newsfragments/4674.fixed.md delete mode 100644 newsfragments/4676.added.md delete mode 100644 newsfragments/4692.fixed.md delete mode 100644 newsfragments/4694.fixed.md delete mode 100644 newsfragments/4705.fixed.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 34727da8b24..eef2151e07c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,103 @@ To see unreleased changes, please see the [CHANGELOG on the main branch guide](h +## [0.23.0] - 2024-11-15 + +### Packaging + +- Drop support for PyPy 3.7 and 3.8. [#4582](https://github.com/PyO3/pyo3/pull/4582) +- Extend range of supported versions of `hashbrown` optional dependency to include version 0.15. [#4604](https://github.com/PyO3/pyo3/pull/4604) +- Bump minimum version of `eyre` optional dependency to 0.6.8. [#4617](https://github.com/PyO3/pyo3/pull/4617) +- Bump minimum version of `hashbrown` optional dependency to 0.14.5. [#4617](https://github.com/PyO3/pyo3/pull/4617) +- Bump minimum version of `indexmap` optional dependency to 2.5.0. [#4617](https://github.com/PyO3/pyo3/pull/4617) +- Bump minimum version of `num-complex` optional dependency to 0.4.6. [#4617](https://github.com/PyO3/pyo3/pull/4617) +- Bump minimum version of `chrono-tz` optional dependency to 0.10. [#4617](https://github.com/PyO3/pyo3/pull/4617) +- Support free-threaded Python 3.13t. [#4588](https://github.com/PyO3/pyo3/pull/4588) + +### Added + +- Add `IntoPyObject` (fallible) conversion trait to convert from Rust to Python values. [#4060](https://github.com/PyO3/pyo3/pull/4060) +- Add `#[pyclass(str="")]` option to generate `__str__` based on a `Display` implementation or format string. [#4233](https://github.com/PyO3/pyo3/pull/4233) +- Implement `PartialEq` for `Bound<'py, PyInt>` with `u8`, `u16`, `u32`, `u64`, `u128`, `usize`, `i8`, `i16`, `i32`, `i64`, `i128` and `isize`. [#4317](https://github.com/PyO3/pyo3/pull/4317) +- Implement `PartialEq` and `PartialEq` for `Bound<'py, PyFloat>`. [#4348](https://github.com/PyO3/pyo3/pull/4348) +- Add `as_super` and `into_super` methods for `Bound`. [#4351](https://github.com/PyO3/pyo3/pull/4351) +- Add FFI definitions `PyCFunctionFast` and `PyCFunctionFastWithKeywords` [#4415](https://github.com/PyO3/pyo3/pull/4415) +- Add FFI definitions for `PyMutex` on Python 3.13 and newer. [#4421](https://github.com/PyO3/pyo3/pull/4421) +- Add `PyDict::locked_for_each` to iterate efficiently on freethreaded Python. [#4439](https://github.com/PyO3/pyo3/pull/4439) +- Add FFI definitions `PyObject_GetOptionalAttr`, `PyObject_GetOptionalAttrString`, `PyObject_HasAttrWithError`, `PyObject_HasAttrStringWithError`, `Py_CONSTANT_*` constants, `Py_GetConstant`, `Py_GetConstantBorrowed`, and `PyType_GetModuleByDef` on Python 3.13 and newer. [#4447](https://github.com/PyO3/pyo3/pull/4447) +- Add FFI definitions for the Python critical section API available on Python 3.13 and newer. [#4477](https://github.com/PyO3/pyo3/pull/4477) +- Add derive macro for `IntoPyObject`. [#4495](https://github.com/PyO3/pyo3/pull/4495) +- Add `Borrowed::as_ptr`. [#4520](https://github.com/PyO3/pyo3/pull/4520) +- Add FFI definition for `PyImport_AddModuleRef`. [#4529](https://github.com/PyO3/pyo3/pull/4529) +- Add `PyAnyMethods::try_iter`. [#4553](https://github.com/PyO3/pyo3/pull/4553) +- Add `pyo3::sync::with_critical_section`, a wrapper around the Python Critical Section API added in Python 3.13. [#4587](https://github.com/PyO3/pyo3/pull/4587) +- Add `#[pymodule(gil_used = false)]` option to declare that a module supports the free-threaded build. [#4588](https://github.com/PyO3/pyo3/pull/4588) +- Add `PyModule::gil_used` method to declare that a module supports the free-threaded build. [#4588](https://github.com/PyO3/pyo3/pull/4588) +- Add FFI definition `PyDateTime_CAPSULE_NAME`. [#4634](https://github.com/PyO3/pyo3/pull/4634) +- Add `PyMappingProxy` type to represent the `mappingproxy` Python class. [#4644](https://github.com/PyO3/pyo3/pull/4644) +- Add FFI definitions `PyList_Extend` and `PyList_Clear`. [#4667](https://github.com/PyO3/pyo3/pull/4667) +- Add derive macro for `IntoPyObjectRef`. [#4674](https://github.com/PyO3/pyo3/pull/4674) +- Add `pyo3::sync::OnceExt` and `pyo3::sync::OnceLockExt` traits. [#4676](https://github.com/PyO3/pyo3/pull/4676) + +### Changed + +- Prefer `IntoPyObject` over `IntoPy>>` for `#[pyfunction]` and `#[pymethods]` return types. [#4060](https://github.com/PyO3/pyo3/pull/4060) +- Report multiple errors from `#[pyclass]` and `#[pyo3(..)]` attributes. [#4243](https://github.com/PyO3/pyo3/pull/4243) +- Nested declarative `#[pymodule]` are automatically treated as submodules (no `PyInit_` entrypoint is created). [#4308](https://github.com/PyO3/pyo3/pull/4308) +- Deprecate `PyAnyMethods::is_ellipsis` (`Py::is_ellipsis` was deprecated in PyO3 0.20). [#4322](https://github.com/PyO3/pyo3/pull/4322) +- Deprecate `PyLong` in favor of `PyInt`. [#4347](https://github.com/PyO3/pyo3/pull/4347) +- Rename `IntoPyDict::into_py_dict_bound` to `IntoPyDict::into_py_dict`. [#4388](https://github.com/PyO3/pyo3/pull/4388) +- `PyModule::from_code` now expects `&CStr` as arguments instead of `&str`. [#4404](https://github.com/PyO3/pyo3/pull/4404) +- Use "fastcall" Python calling convention for `#[pyfunction]`s when compiling on abi3 for Python 3.10 and up. [#4415](https://github.com/PyO3/pyo3/pull/4415) +- Remove `Copy` and `Clone` from `PyObject` struct FFI definition. [#4434](https://github.com/PyO3/pyo3/pull/4434) +- `Python::eval` and `Python::run` now take a `&CStr` instead of `&str`. [#4435](https://github.com/PyO3/pyo3/pull/4435) +- Deprecate `IPowModulo`, `PyClassAttributeDef`, `PyGetterDef`, `PyMethodDef`, `PyMethodDefType`, and `PySetterDef` from PyO3's public API. [#4441](https://github.com/PyO3/pyo3/pull/4441) +- `IntoPyObject` impls for `Vec`, `&[u8]`, `[u8; N]`, `Cow<[u8]>` and `SmallVec<[u8; N]>` now convert into Python `bytes` rather than a `list` of integers. [#4442](https://github.com/PyO3/pyo3/pull/4442) +- Emit a compile-time error when attempting to subclass a class that doesn't allow subclassing. [#4453](https://github.com/PyO3/pyo3/pull/4453) +- `IntoPyDict::into_py_dict` is now fallible due to `IntoPyObject` migration. [#4493](https://github.com/PyO3/pyo3/pull/4493) +- The `abi3` feature will now override config files provided via `PYO3_BUILD_CONFIG`. [#4497](https://github.com/PyO3/pyo3/pull/4497) +- Disable the `GILProtected` struct on free-threaded Python. [#4504](https://github.com/PyO3/pyo3/pull/4504) +- Updated FFI definitions for functions and struct fields that have been deprecated or removed from CPython. [#4534](https://github.com/PyO3/pyo3/pull/4534) +- Disable `PyListMethods::get_item_unchecked` on free-threaded Python. [#4539](https://github.com/PyO3/pyo3/pull/4539) +- Add `GILOnceCell::import`. [#4542](https://github.com/PyO3/pyo3/pull/4542) +- Deprecate `PyAnyMethods::iter` in favour of `PyAnyMethods::try_iter`. [#4553](https://github.com/PyO3/pyo3/pull/4553) +- The `#[pyclass]` macro now requires a types to be `Sync`. (Except for `#[pyclass(unsendable)]` types). [#4566](https://github.com/PyO3/pyo3/pull/4566) +- `PyList::new` and `PyTuple::new` are now fallible due to `IntoPyObject` migration. [#4580](https://github.com/PyO3/pyo3/pull/4580) +- `PyErr::matches` is now fallible due to `IntoPyObject` migration. [#4595](https://github.com/PyO3/pyo3/pull/4595) +- Deprecate `ToPyObject` in favour of `IntoPyObject` [#4595](https://github.com/PyO3/pyo3/pull/4595) +- Deprecate `PyWeakrefMethods::get_option`. [#4597](https://github.com/PyO3/pyo3/pull/4597) +- Seal `PyWeakrefMethods` trait. [#4598](https://github.com/PyO3/pyo3/pull/4598) +- Remove `PyNativeTypeInitializer` and `PyObjectInit` from the PyO3 public API. [#4611](https://github.com/PyO3/pyo3/pull/4611) +- Deprecate `IntoPy` in favor of `IntoPyObject` [#4618](https://github.com/PyO3/pyo3/pull/4618) +- Eagerly normalize exceptions in `PyErr::take()` and `PyErr::fetch()` on Python 3.11 and older. [#4655](https://github.com/PyO3/pyo3/pull/4655) +- Move `IntoPy::type_output` to `IntoPyObject::type_output`. [#4657](https://github.com/PyO3/pyo3/pull/4657) +- Change return type of `PyMapping::keys`, `PyMapping::values` and `PyMapping::items` to `Bound<'py, PyList>` instead of `Bound<'py, PySequence>`. [#4661](https://github.com/PyO3/pyo3/pull/4661) +- Complex enums now allow field types that either implement `IntoPyObject` by reference or by value together with `Clone`. This makes `Py` available as field type. [#4694](https://github.com/PyO3/pyo3/pull/4694) + + +### Removed + +- Remove all functionality deprecated in PyO3 0.20. [#4322](https://github.com/PyO3/pyo3/pull/4322) +- Remove all functionality deprecated in PyO3 0.21. [#4323](https://github.com/PyO3/pyo3/pull/4323) +- Deprecate `PyUnicode` in favour of `PyString`. [#4370](https://github.com/PyO3/pyo3/pull/4370) +- Remove deprecated `gil-refs` feature. [#4378](https://github.com/PyO3/pyo3/pull/4378) +- Remove private FFI definitions `_Py_IMMORTAL_REFCNT`, `_Py_IsImmortal`, `_Py_TPFLAGS_STATIC_BUILTIN`, `_Py_Dealloc`, `_Py_IncRef`, `_Py_DecRef`. [#4447](https://github.com/PyO3/pyo3/pull/4447) +- Remove private FFI definitions `_Py_c_sum`, `_Py_c_diff`, `_Py_c_neg`, `_Py_c_prod`, `_Py_c_quot`, `_Py_c_pow`, `_Py_c_abs`. [#4521](https://github.com/PyO3/pyo3/pull/4521) +- Remove `_borrowed` methods of `PyWeakRef` and `PyWeakRefProxy`. [#4528](https://github.com/PyO3/pyo3/pull/4528) +- Removed private FFI definition `_PyErr_ChainExceptions`. [#4534](https://github.com/PyO3/pyo3/pull/4534) + +### Fixed + +- Fix invalid library search path `lib_dir` when cross-compiling. [#4389](https://github.com/PyO3/pyo3/pull/4389) +- Fix FFI definition `Py_Is` for PyPy on 3.10 to call the function defined by PyPy. [#4447](https://github.com/PyO3/pyo3/pull/4447) +- Fix compile failure when using `#[cfg]` attributes for simple enum variants. [#4509](https://github.com/PyO3/pyo3/pull/4509) +- Fix compiler warning for `non_snake_case` method names inside `#[pymethods]` generated code. [#4567](https://github.com/PyO3/pyo3/pull/4567) +- Fix compile error with `#[derive(FromPyObject)]` generic struct with trait bounds. [#4645](https://github.com/PyO3/pyo3/pull/4645) +- Fix compile error for `#[classmethod]` and `#[staticmethod]` on magic methods. [#4654](https://github.com/PyO3/pyo3/pull/4654) +- Fix compile warning for `unsafe_op_in_unsafe_fn` in generated macro code. [#4674](https://github.com/PyO3/pyo3/pull/4674) +- Fix incorrect deprecation warning for `#[pyclass] enum`s with custom `__eq__` implementation. [#4692](https://github.com/PyO3/pyo3/pull/4692) +- Fix `non_upper_case_globals` lint firing for generated `__match_args__` on complex enums. [#4705](https://github.com/PyO3/pyo3/pull/4705) + ## [0.22.5] - 2024-10-15 ### Fixed @@ -1899,7 +1996,8 @@ Yanked - Initial release -[Unreleased]: https://github.com/pyo3/pyo3/compare/v0.22.5...HEAD +[Unreleased]: https://github.com/pyo3/pyo3/compare/v0.23.0...HEAD +[0.23.0]: https://github.com/pyo3/pyo3/compare/v0.22.5...v0.23.0 [0.22.5]: https://github.com/pyo3/pyo3/compare/v0.22.4...v0.22.5 [0.22.4]: https://github.com/pyo3/pyo3/compare/v0.22.3...v0.22.4 [0.22.3]: https://github.com/pyo3/pyo3/compare/v0.22.2...v0.22.3 diff --git a/Cargo.toml b/Cargo.toml index 9e931ed00b6..1dc198fecac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3" -version = "0.23.0-dev" +version = "0.23.0" description = "Bindings to Python interpreter" authors = ["PyO3 Project and Contributors "] readme = "README.md" @@ -21,10 +21,10 @@ memoffset = "0.9" once_cell = "1.13" # ffi bindings to the python interpreter, split into a separate crate so they can be used independently -pyo3-ffi = { path = "pyo3-ffi", version = "=0.23.0-dev" } +pyo3-ffi = { path = "pyo3-ffi", version = "=0.23.0" } # support crates for macros feature -pyo3-macros = { path = "pyo3-macros", version = "=0.23.0-dev", optional = true } +pyo3-macros = { path = "pyo3-macros", version = "=0.23.0", optional = true } indoc = { version = "2.0.1", optional = true } unindent = { version = "0.2.1", optional = true } @@ -66,7 +66,7 @@ static_assertions = "1.1.0" uuid = {version = "1.10.0", features = ["v4"] } [build-dependencies] -pyo3-build-config = { path = "pyo3-build-config", version = "=0.23.0-dev", features = ["resolve-config"] } +pyo3-build-config = { path = "pyo3-build-config", version = "=0.23.0", features = ["resolve-config"] } [features] default = ["macros"] diff --git a/newsfragments/4060.added.md b/newsfragments/4060.added.md deleted file mode 100644 index 2734df34bc9..00000000000 --- a/newsfragments/4060.added.md +++ /dev/null @@ -1 +0,0 @@ -New `IntoPyObject` (fallible) conversion trait to convert from Rust to Python values. \ No newline at end of file diff --git a/newsfragments/4060.changed.md b/newsfragments/4060.changed.md deleted file mode 100644 index 8d104a05cae..00000000000 --- a/newsfragments/4060.changed.md +++ /dev/null @@ -1 +0,0 @@ -`#[pyfunction]` and `#[pymethods]` return types will prefer `IntoPyObject` over `IntoPy` \ No newline at end of file diff --git a/newsfragments/4233.added.md b/newsfragments/4233.added.md deleted file mode 100644 index cd45d163951..00000000000 --- a/newsfragments/4233.added.md +++ /dev/null @@ -1 +0,0 @@ -Added `#[pyclass(str="")]` option to generate `__str__` based on a `Display` implementation or format string. \ No newline at end of file diff --git a/newsfragments/4243.changed.md b/newsfragments/4243.changed.md deleted file mode 100644 index d9464ab37aa..00000000000 --- a/newsfragments/4243.changed.md +++ /dev/null @@ -1 +0,0 @@ -Report multiple errors from `#[pyclass]` and `#[pyo3(..)]` attributes. diff --git a/newsfragments/4308.changed.md b/newsfragments/4308.changed.md deleted file mode 100644 index 6b5310cdd36..00000000000 --- a/newsfragments/4308.changed.md +++ /dev/null @@ -1 +0,0 @@ -Nested declarative `#[pymodule]` are automatically treated as submodules (no `PyInit_` entrypoint is created) diff --git a/newsfragments/4317.added.md b/newsfragments/4317.added.md deleted file mode 100644 index 99849236101..00000000000 --- a/newsfragments/4317.added.md +++ /dev/null @@ -1 +0,0 @@ -Implement `PartialEq` for `Bound<'py, PyInt>` with `u8`, `u16`, `u32`, `u64`, `u128`, `usize`, `i8`, `i16`, `i32`, `i64`, `i128` and `isize`. diff --git a/newsfragments/4322.changed.md b/newsfragments/4322.changed.md deleted file mode 100644 index dd15a89dcba..00000000000 --- a/newsfragments/4322.changed.md +++ /dev/null @@ -1 +0,0 @@ -Deprecate `PyAnyMethods::is_ellipsis` (`Py::is_ellpsis` was deprecated in PyO3 0.20). diff --git a/newsfragments/4322.removed.md b/newsfragments/4322.removed.md deleted file mode 100644 index 4d8f62e4aef..00000000000 --- a/newsfragments/4322.removed.md +++ /dev/null @@ -1 +0,0 @@ -Remove all functionality deprecated in PyO3 0.20. diff --git a/newsfragments/4323.removed.md b/newsfragments/4323.removed.md deleted file mode 100644 index c9d46f6a886..00000000000 --- a/newsfragments/4323.removed.md +++ /dev/null @@ -1 +0,0 @@ -Remove all functionality deprecated in PyO3 0.21. diff --git a/newsfragments/4347.changed.md b/newsfragments/4347.changed.md deleted file mode 100644 index e64ad2c6395..00000000000 --- a/newsfragments/4347.changed.md +++ /dev/null @@ -1 +0,0 @@ -Deprecate `PyLong` in favor of `PyInt`. diff --git a/newsfragments/4348.added.md b/newsfragments/4348.added.md deleted file mode 100644 index 57d3e415187..00000000000 --- a/newsfragments/4348.added.md +++ /dev/null @@ -1 +0,0 @@ -Implement `PartialEq` and `PartialEq` for `Bound<'py, PyFloat>`. \ No newline at end of file diff --git a/newsfragments/4351.added.md b/newsfragments/4351.added.md deleted file mode 100644 index f9276ae0d4f..00000000000 --- a/newsfragments/4351.added.md +++ /dev/null @@ -1 +0,0 @@ -Added `as_super` and `into_super` methods for `Bound`. \ No newline at end of file diff --git a/newsfragments/4370.removed.md b/newsfragments/4370.removed.md deleted file mode 100644 index d54c6a26601..00000000000 --- a/newsfragments/4370.removed.md +++ /dev/null @@ -1 +0,0 @@ -Deprecated PyUnicode in favour of PyString. diff --git a/newsfragments/4378.removed.md b/newsfragments/4378.removed.md deleted file mode 100644 index 3c43d46478d..00000000000 --- a/newsfragments/4378.removed.md +++ /dev/null @@ -1 +0,0 @@ -removed deprecated `gil-refs` feature diff --git a/newsfragments/4388.changed.md b/newsfragments/4388.changed.md deleted file mode 100644 index 6fa66449c9c..00000000000 --- a/newsfragments/4388.changed.md +++ /dev/null @@ -1 +0,0 @@ -Renamed `IntoPyDict::into_py_dict_bound` into `IntoPyDict::into_py_dict`. diff --git a/newsfragments/4389.fixed.md b/newsfragments/4389.fixed.md deleted file mode 100644 index 6702efd3a75..00000000000 --- a/newsfragments/4389.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Fix invalid library search path `lib_dir` when cross-compiling. diff --git a/newsfragments/4404.changed.md b/newsfragments/4404.changed.md deleted file mode 100644 index 728c45ae694..00000000000 --- a/newsfragments/4404.changed.md +++ /dev/null @@ -1 +0,0 @@ -`PyModule::from_code` now expects &CStr as arguments instead of `&str`. \ No newline at end of file diff --git a/newsfragments/4415.added.md b/newsfragments/4415.added.md deleted file mode 100644 index 51796b3c816..00000000000 --- a/newsfragments/4415.added.md +++ /dev/null @@ -1 +0,0 @@ -Add FFI definitions `PyCFunctionFast` and `PyCFunctionFastWithKeywords` diff --git a/newsfragments/4415.changed.md b/newsfragments/4415.changed.md deleted file mode 100644 index 47c5e8bfb6d..00000000000 --- a/newsfragments/4415.changed.md +++ /dev/null @@ -1 +0,0 @@ -Use "fastcall" Python calling convention for `#[pyfunction]`s when compiling on abi3 for Python 3.10 and up. diff --git a/newsfragments/4421.added.md b/newsfragments/4421.added.md deleted file mode 100644 index b0a85bea3ca..00000000000 --- a/newsfragments/4421.added.md +++ /dev/null @@ -1 +0,0 @@ -* Added bindings for PyMutex. diff --git a/newsfragments/4421.fixed.md b/newsfragments/4421.fixed.md deleted file mode 100644 index 075b1fa7b5a..00000000000 --- a/newsfragments/4421.fixed.md +++ /dev/null @@ -1 +0,0 @@ -* Updated FFI bindings for free-threaded CPython 3.13 ABI diff --git a/newsfragments/4434.changed.md b/newsfragments/4434.changed.md deleted file mode 100644 index f16513add1b..00000000000 --- a/newsfragments/4434.changed.md +++ /dev/null @@ -1,5 +0,0 @@ -* The `PyO3::ffi` bindings for the C `PyObject` struct no longer derive from - `Copy` and `Clone`. If you use the ffi directly you will need to remove `Copy` - and `Clone` from any derived types. Any cases where a PyObject struct was - copied or cloned directly likely indicates a bug, it is not safe to allocate - PyObject structs outside of the Python runtime. diff --git a/newsfragments/4435.changed.md b/newsfragments/4435.changed.md deleted file mode 100644 index 9de2b84df5e..00000000000 --- a/newsfragments/4435.changed.md +++ /dev/null @@ -1 +0,0 @@ -Reintroduced `Python::eval` and `Python::run` now take a `&CStr` instead of `&str`. \ No newline at end of file diff --git a/newsfragments/4439.changed.md b/newsfragments/4439.changed.md deleted file mode 100644 index 9cb01a4d2b8..00000000000 --- a/newsfragments/4439.changed.md +++ /dev/null @@ -1,3 +0,0 @@ -* Make `PyDict` iterator compatible with free-threaded build -* Added `PyDict::locked_for_each` method to iterate on free-threaded builds to prevent the dict being mutated during iteration -* Iterate over `dict.items()` when dict is subclassed from `PyDict` diff --git a/newsfragments/4441.changed.md b/newsfragments/4441.changed.md deleted file mode 100644 index 996b368807e..00000000000 --- a/newsfragments/4441.changed.md +++ /dev/null @@ -1 +0,0 @@ -Deprecate `IPowModulo`, `PyClassAttributeDef`, `PyGetterDef`, `PyMethodDef`, `PyMethodDefType`, and `PySetterDef` from PyO3's public API. diff --git a/newsfragments/4442.changed.md b/newsfragments/4442.changed.md deleted file mode 100644 index 44fbcbfe23c..00000000000 --- a/newsfragments/4442.changed.md +++ /dev/null @@ -1,2 +0,0 @@ -`IntoPyObject` impls for `Vec`, `&[u8]`, `[u8; N]`, `Cow<[u8]>` and `SmallVec<[u8; N]>` now -convert into `PyBytes` rather than `PyList`. \ No newline at end of file diff --git a/newsfragments/4447.added.md b/newsfragments/4447.added.md deleted file mode 100644 index a1e6b5eaa39..00000000000 --- a/newsfragments/4447.added.md +++ /dev/null @@ -1 +0,0 @@ -Add Python 3.13 FFI definitions `PyObject_GetOptionalAttr`, `PyObject_GetOptionalAttrString`, `PyObject_HasAttrWithError`, `PyObject_HasAttrStringWithError`, `Py_CONSTANT_*` constants, `Py_GetConstant`, `Py_GetConstantBorrowed`, and `PyType_GetModuleByDef`. diff --git a/newsfragments/4447.fixed.md b/newsfragments/4447.fixed.md deleted file mode 100644 index bfc92ae1d26..00000000000 --- a/newsfragments/4447.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Fix FFI definition `Py_Is` for PyPy on 3.10 to call the function defined by PyPy. diff --git a/newsfragments/4447.removed.md b/newsfragments/4447.removed.md deleted file mode 100644 index c7451866d83..00000000000 --- a/newsfragments/4447.removed.md +++ /dev/null @@ -1 +0,0 @@ -Remove private FFI definitions `_Py_IMMORTAL_REFCNT`, `_Py_IsImmortal`, `_Py_TPFLAGS_STATIC_BUILTIN`, `_Py_Dealloc`, `_Py_IncRef`, `_Py_DecRef`. diff --git a/newsfragments/4453.changed.md b/newsfragments/4453.changed.md deleted file mode 100644 index 58a1e0ffcbd..00000000000 --- a/newsfragments/4453.changed.md +++ /dev/null @@ -1 +0,0 @@ -Make subclassing a class that doesn't allow that a compile-time error instead of runtime \ No newline at end of file diff --git a/newsfragments/4477.added.md b/newsfragments/4477.added.md deleted file mode 100644 index d0f43f909d5..00000000000 --- a/newsfragments/4477.added.md +++ /dev/null @@ -1 +0,0 @@ -* Added bindings for the Python critical section API available on Python 3.13 and newer. diff --git a/newsfragments/4493.changed.md b/newsfragments/4493.changed.md deleted file mode 100644 index efff3b6fa6e..00000000000 --- a/newsfragments/4493.changed.md +++ /dev/null @@ -1 +0,0 @@ -`IntoPyDict::into_py_dict` is now fallible due to `IntoPyObject` migration. \ No newline at end of file diff --git a/newsfragments/4495.added.md b/newsfragments/4495.added.md deleted file mode 100644 index 2cbe2a85bbf..00000000000 --- a/newsfragments/4495.added.md +++ /dev/null @@ -1 +0,0 @@ -Added `IntoPyObject` derive macro \ No newline at end of file diff --git a/newsfragments/4497.changed.md b/newsfragments/4497.changed.md deleted file mode 100644 index 594b6d373e5..00000000000 --- a/newsfragments/4497.changed.md +++ /dev/null @@ -1 +0,0 @@ -The `abi3` feature will now override config files provided via `PYO3_BUILD_CONFIG`. diff --git a/newsfragments/4504.changed.md b/newsfragments/4504.changed.md deleted file mode 100644 index 94d056dcef9..00000000000 --- a/newsfragments/4504.changed.md +++ /dev/null @@ -1,2 +0,0 @@ -* The `GILProtected` struct is not available on the free-threaded build of - Python 3.13. diff --git a/newsfragments/4509.fixed.md b/newsfragments/4509.fixed.md deleted file mode 100644 index 3684b3e617d..00000000000 --- a/newsfragments/4509.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Fix compile failure when using `#[cfg]` attributes for simple enum variants. diff --git a/newsfragments/4512.changed.md b/newsfragments/4512.changed.md deleted file mode 100644 index 1e86689c5ae..00000000000 --- a/newsfragments/4512.changed.md +++ /dev/null @@ -1 +0,0 @@ -`GILOnceCell` is now thread-safe for the Python 3.13 freethreaded builds. diff --git a/newsfragments/4520.added.md b/newsfragments/4520.added.md deleted file mode 100644 index d9952934de5..00000000000 --- a/newsfragments/4520.added.md +++ /dev/null @@ -1 +0,0 @@ -Add `Borrowed::as_ptr`. diff --git a/newsfragments/4521.removed.md b/newsfragments/4521.removed.md deleted file mode 100644 index 3ef52c5515d..00000000000 --- a/newsfragments/4521.removed.md +++ /dev/null @@ -1 +0,0 @@ -Remove private FFI definitions `_Py_c_sum`, `_Py_c_diff`, `_Py_c_neg`, `_Py_c_prod`, `_Py_c_quot`, `_Py_c_pow`, `_Py_c_abs`. diff --git a/newsfragments/4528.removed.md b/newsfragments/4528.removed.md deleted file mode 100644 index 79c66b98818..00000000000 --- a/newsfragments/4528.removed.md +++ /dev/null @@ -1,7 +0,0 @@ -* Removed the `get_object_borrowed`, `upgrade_borrowed`, `upgrade_borrowed_as`, -`upgrade_borrowed_as_unchecked`, `upgrade_borrowed_as_exact` methods of -`PyWeakref` and `PyWeakrefProxy`. These returned borrowed references to weakly -referenced data, and in principle if the GIL is released the last strong -reference could be released, allowing a possible use-after-free error. If you -are using these functions, you should change to the equivalent function that -returns a `Bound<'py, T>` reference. diff --git a/newsfragments/4529.added.md b/newsfragments/4529.added.md deleted file mode 100644 index 8a82a942eb6..00000000000 --- a/newsfragments/4529.added.md +++ /dev/null @@ -1 +0,0 @@ -* Added FFI bindings for `PyImport_AddModuleRef`. diff --git a/newsfragments/4534.changed.md b/newsfragments/4534.changed.md deleted file mode 100644 index c6ff877976d..00000000000 --- a/newsfragments/4534.changed.md +++ /dev/null @@ -1,3 +0,0 @@ -* Updated the FFI bindings for functions and struct fields that have been - deprecated or removed. You may see new deprecation warnings if you are using - functions or fields exposed by the C API that are deprecated. diff --git a/newsfragments/4534.removed.md b/newsfragments/4534.removed.md deleted file mode 100644 index 3ecd27f5eb9..00000000000 --- a/newsfragments/4534.removed.md +++ /dev/null @@ -1 +0,0 @@ -* Removed the bindings for the private function `_PyErr_ChainExceptions`. diff --git a/newsfragments/4539.removed.md b/newsfragments/4539.removed.md deleted file mode 100644 index dd1da0169b6..00000000000 --- a/newsfragments/4539.removed.md +++ /dev/null @@ -1,3 +0,0 @@ -* `PyListMethods::get_item_unchecked` is disabled on the free-threaded build. - It relies on accessing list internals without any locking and is not - thread-safe without the GIL to synchronize access. diff --git a/newsfragments/4542.changed.md b/newsfragments/4542.changed.md deleted file mode 100644 index 1f983a5e344..00000000000 --- a/newsfragments/4542.changed.md +++ /dev/null @@ -1 +0,0 @@ -Change `GILOnceCell::get_or_try_init_type_ref` to `GILOnceCell::import` and make it public API. diff --git a/newsfragments/4544.changed.md b/newsfragments/4544.changed.md deleted file mode 100644 index c94758a770d..00000000000 --- a/newsfragments/4544.changed.md +++ /dev/null @@ -1,2 +0,0 @@ -* Refactored runtime borrow checking for mutable pyclass instances - to be thread-safe when the GIL is disabled. diff --git a/newsfragments/4553.fixed.md b/newsfragments/4553.fixed.md deleted file mode 100644 index c6714bfce95..00000000000 --- a/newsfragments/4553.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Deprecate `PyAnyMethods.iter` and replace it with `PyAnyMethods.try_iter` diff --git a/newsfragments/4566.changed.md b/newsfragments/4566.changed.md deleted file mode 100644 index 2e9db108df1..00000000000 --- a/newsfragments/4566.changed.md +++ /dev/null @@ -1,5 +0,0 @@ -* The `pyclass` macro now creates a rust type that is `Sync` by default. If you - would like to opt out of this, annotate your class with - `pyclass(unsendable)`. See the migraiton guide entry (INSERT GUIDE LINK HERE) - for more information on updating to accommadate this change. - diff --git a/newsfragments/4567.fixed.md b/newsfragments/4567.fixed.md deleted file mode 100644 index 24507b81b41..00000000000 --- a/newsfragments/4567.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Fix compiler warning about non snake case method names inside `#[pymethods]` generated code. diff --git a/newsfragments/4577.added.md b/newsfragments/4577.added.md deleted file mode 100644 index 71858564fe5..00000000000 --- a/newsfragments/4577.added.md +++ /dev/null @@ -1 +0,0 @@ -* Added a guide page for free-threaded Python. diff --git a/newsfragments/4580.changed.md b/newsfragments/4580.changed.md deleted file mode 100644 index 72b838d4055..00000000000 --- a/newsfragments/4580.changed.md +++ /dev/null @@ -1 +0,0 @@ -`PyList::new` and `PyTuple::new` are now fallible due to `IntoPyObject` migration. \ No newline at end of file diff --git a/newsfragments/4582.packaging.md b/newsfragments/4582.packaging.md deleted file mode 100644 index 524ee02e017..00000000000 --- a/newsfragments/4582.packaging.md +++ /dev/null @@ -1 +0,0 @@ -Drop support for PyPy 3.7 and 3.8. diff --git a/newsfragments/4587.added.md b/newsfragments/4587.added.md deleted file mode 100644 index 4ccd4cd1c5e..00000000000 --- a/newsfragments/4587.added.md +++ /dev/null @@ -1,2 +0,0 @@ -* Added `with_critical_section`, a safe wrapper around the Python Critical - Section API added in Python 3.13 for the free-threaded build. diff --git a/newsfragments/4588.added.md b/newsfragments/4588.added.md deleted file mode 100644 index 42b5b8e219a..00000000000 --- a/newsfragments/4588.added.md +++ /dev/null @@ -1,3 +0,0 @@ -* It is now possible to declare that a module supports the free-threaded build - by either calling `PyModule::gil_used` or passing - `gil_used = false` as a parameter to the `pymodule` proc macro. diff --git a/newsfragments/4595.changed.md b/newsfragments/4595.changed.md deleted file mode 100644 index e8b7eba5cbf..00000000000 --- a/newsfragments/4595.changed.md +++ /dev/null @@ -1,2 +0,0 @@ -- `PyErr::matches` is now fallible due to `IntoPyObject` migration. -- deprecate `ToPyObject` in favor of `IntoPyObject` \ No newline at end of file diff --git a/newsfragments/4597.changed.md b/newsfragments/4597.changed.md deleted file mode 100644 index 7ec760451bd..00000000000 --- a/newsfragments/4597.changed.md +++ /dev/null @@ -1 +0,0 @@ -Deprecate `PyWeakrefMethods::get_option`. diff --git a/newsfragments/4598.changed.md b/newsfragments/4598.changed.md deleted file mode 100644 index a8dfc040813..00000000000 --- a/newsfragments/4598.changed.md +++ /dev/null @@ -1 +0,0 @@ -Seal `PyWeakrefMethods` trait. diff --git a/newsfragments/4604.packaging.md b/newsfragments/4604.packaging.md deleted file mode 100644 index c6dd6a60cf9..00000000000 --- a/newsfragments/4604.packaging.md +++ /dev/null @@ -1 +0,0 @@ -Extend range of supported versions of `hashbrown` optional dependency to include version 0.15 diff --git a/newsfragments/4606.changed.md b/newsfragments/4606.changed.md deleted file mode 100644 index 173252992c1..00000000000 --- a/newsfragments/4606.changed.md +++ /dev/null @@ -1 +0,0 @@ -Seal `PyAddToModule` trait. diff --git a/newsfragments/4611.changed.md b/newsfragments/4611.changed.md deleted file mode 100644 index 950de4305ea..00000000000 --- a/newsfragments/4611.changed.md +++ /dev/null @@ -1 +0,0 @@ -`PyNativeTypeInitializer` and `PyObjectInit` are moved into `impl_`. `PyObjectInit` is now a Sealed trait diff --git a/newsfragments/4617.packaging.md b/newsfragments/4617.packaging.md deleted file mode 100644 index a9a0a41a8d7..00000000000 --- a/newsfragments/4617.packaging.md +++ /dev/null @@ -1,13 +0,0 @@ -deps: update dependencies - -- eyre: 0.4 => 0.6.8 -- hashbrown: 0.9 => 0.14.5 -- indexmap: 1.6 => 2.5.0 -- num-complex: 0.2 => 0.4.6 -- chrono-tz: 0.6 => 0.10 - -Eyre min-version is limited to 0.6.8 to be compatible with MSRV 1.63 -Hashbrown min-version is limited to 0.14.5: - https://github.com/rust-lang/hashbrown/issues/574 -Indexmap min-version is limited to 2.5.0 to be compatible with hashbrown 0.14.5 - diff --git a/newsfragments/4618.changed.md b/newsfragments/4618.changed.md deleted file mode 100644 index f3e9dee773d..00000000000 --- a/newsfragments/4618.changed.md +++ /dev/null @@ -1 +0,0 @@ -deprecate `IntoPy` in favor of `IntoPyObject` \ No newline at end of file diff --git a/newsfragments/4623.fixed.md b/newsfragments/4623.fixed.md deleted file mode 100644 index 18fd8460b44..00000000000 --- a/newsfragments/4623.fixed.md +++ /dev/null @@ -1 +0,0 @@ -* The FFI wrapper for the PyDateTime_IMPORT macro is now thread-safe. diff --git a/newsfragments/4634.added.md b/newsfragments/4634.added.md deleted file mode 100644 index 886e56911bd..00000000000 --- a/newsfragments/4634.added.md +++ /dev/null @@ -1 +0,0 @@ -Add FFI definition `PyDateTime_CAPSULE_NAME`. diff --git a/newsfragments/4644.added.md b/newsfragments/4644.added.md deleted file mode 100644 index 4b4a277abf8..00000000000 --- a/newsfragments/4644.added.md +++ /dev/null @@ -1 +0,0 @@ -New `PyMappingProxy` struct corresponing to the `mappingproxy` class in Python. diff --git a/newsfragments/4645.fixed.md b/newsfragments/4645.fixed.md deleted file mode 100644 index ec4352d6693..00000000000 --- a/newsfragments/4645.fixed.md +++ /dev/null @@ -1 +0,0 @@ -fix `#[derive(FromPyObject)]` expansion on generic with trait bounds \ No newline at end of file diff --git a/newsfragments/4654.fixed.md b/newsfragments/4654.fixed.md deleted file mode 100644 index 5e470178b55..00000000000 --- a/newsfragments/4654.fixed.md +++ /dev/null @@ -1 +0,0 @@ -fix compile error for `#[classmethod]`/`#[staticmethod]` on magic methods \ No newline at end of file diff --git a/newsfragments/4655.changed.md b/newsfragments/4655.changed.md deleted file mode 100644 index 544fac4973f..00000000000 --- a/newsfragments/4655.changed.md +++ /dev/null @@ -1 +0,0 @@ -Eagerly normalize exceptions in `PyErr::take()` and `PyErr::fetch()` on Python 3.11 and older. diff --git a/newsfragments/4657.changed.md b/newsfragments/4657.changed.md deleted file mode 100644 index 38018916001..00000000000 --- a/newsfragments/4657.changed.md +++ /dev/null @@ -1 +0,0 @@ -`IntoPy::type_output` moved to `IntoPyObject::type_output` \ No newline at end of file diff --git a/newsfragments/4661.changed.md b/newsfragments/4661.changed.md deleted file mode 100644 index 10b521fc605..00000000000 --- a/newsfragments/4661.changed.md +++ /dev/null @@ -1 +0,0 @@ -`PyMapping`'s `keys`, `values` and `items` methods return `PyList` instead of `PySequence`. \ No newline at end of file diff --git a/newsfragments/4665.changed.md b/newsfragments/4665.changed.md deleted file mode 100644 index 2ebbf0c86b4..00000000000 --- a/newsfragments/4665.changed.md +++ /dev/null @@ -1,2 +0,0 @@ -* The `sequential` and `string-sum` examples have moved into a new `examples` - directory in the `pyo3-ffi` crate. diff --git a/newsfragments/4667.added.md b/newsfragments/4667.added.md deleted file mode 100644 index fc2a914607e..00000000000 --- a/newsfragments/4667.added.md +++ /dev/null @@ -1 +0,0 @@ -Add `PyList_Extend` & `PyList_Clear` to pyo3-ffi diff --git a/newsfragments/4671.fixed.md b/newsfragments/4671.fixed.md deleted file mode 100644 index 9b0cd9d8f0c..00000000000 --- a/newsfragments/4671.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Make `PyErr` internals thread-safe. diff --git a/newsfragments/4674.fixed.md b/newsfragments/4674.fixed.md deleted file mode 100644 index 6245a6f734a..00000000000 --- a/newsfragments/4674.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Fixes unintentional `unsafe_op_in_unsafe_fn` trigger by adjusting macro hygiene. \ No newline at end of file diff --git a/newsfragments/4676.added.md b/newsfragments/4676.added.md deleted file mode 100644 index 730b2297d91..00000000000 --- a/newsfragments/4676.added.md +++ /dev/null @@ -1 +0,0 @@ -Add `pyo3::sync::OnceExt` and `pyo3::sync::OnceLockExt` traits. diff --git a/newsfragments/4692.fixed.md b/newsfragments/4692.fixed.md deleted file mode 100644 index a5dc6d098cf..00000000000 --- a/newsfragments/4692.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Fix incorrect deprecation warning for `#[pyclass] enum`s with custom `__eq__` implementation. diff --git a/newsfragments/4694.fixed.md b/newsfragments/4694.fixed.md deleted file mode 100644 index 80df8802cd4..00000000000 --- a/newsfragments/4694.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Complex enums now allow field types that either implement `IntoPyObject` by reference or by value together with `Clone`. This makes `Py` available as field type. \ No newline at end of file diff --git a/newsfragments/4705.fixed.md b/newsfragments/4705.fixed.md deleted file mode 100644 index b7f49d40b0d..00000000000 --- a/newsfragments/4705.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Fix `non_upper_case_globals` lint firing for generated `__match_args__` on complex enums. diff --git a/pyo3-build-config/Cargo.toml b/pyo3-build-config/Cargo.toml index a5549df3ecb..f760152a087 100644 --- a/pyo3-build-config/Cargo.toml +++ b/pyo3-build-config/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3-build-config" -version = "0.23.0-dev" +version = "0.23.0" 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 0e58197bbfd..74f23aa5429 100644 --- a/pyo3-ffi/Cargo.toml +++ b/pyo3-ffi/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3-ffi" -version = "0.23.0-dev" +version = "0.23.0" description = "Python-API bindings for the PyO3 ecosystem" authors = ["PyO3 Project and Contributors "] keywords = ["pyo3", "python", "cpython", "ffi"] @@ -41,7 +41,7 @@ generate-import-lib = ["pyo3-build-config/python3-dll-a"] paste = "1" [build-dependencies] -pyo3-build-config = { path = "../pyo3-build-config", version = "=0.23.0-dev", features = ["resolve-config"] } +pyo3-build-config = { path = "../pyo3-build-config", version = "=0.23.0", features = ["resolve-config"] } [lints] workspace = true diff --git a/pyo3-macros-backend/Cargo.toml b/pyo3-macros-backend/Cargo.toml index 337d19f5653..ec74aa07640 100644 --- a/pyo3-macros-backend/Cargo.toml +++ b/pyo3-macros-backend/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3-macros-backend" -version = "0.23.0-dev" +version = "0.23.0" description = "Code generation for PyO3 package" authors = ["PyO3 Project and Contributors "] keywords = ["pyo3", "python", "cpython", "ffi"] @@ -16,7 +16,7 @@ edition = "2021" [dependencies] heck = "0.5" proc-macro2 = { version = "1.0.60", default-features = false } -pyo3-build-config = { path = "../pyo3-build-config", version = "=0.23.0-dev", features = ["resolve-config"] } +pyo3-build-config = { path = "../pyo3-build-config", version = "=0.23.0", features = ["resolve-config"] } quote = { version = "1", default-features = false } [dependencies.syn] @@ -25,7 +25,7 @@ default-features = false features = ["derive", "parsing", "printing", "clone-impls", "full", "extra-traits"] [build-dependencies] -pyo3-build-config = { path = "../pyo3-build-config", version = "=0.23.0-dev" } +pyo3-build-config = { path = "../pyo3-build-config", version = "=0.23.0" } [lints] workspace = true diff --git a/pyo3-macros/Cargo.toml b/pyo3-macros/Cargo.toml index c86afd8e2d2..4fb52ea1e69 100644 --- a/pyo3-macros/Cargo.toml +++ b/pyo3-macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3-macros" -version = "0.23.0-dev" +version = "0.23.0" description = "Proc macros for PyO3 package" authors = ["PyO3 Project and Contributors "] keywords = ["pyo3", "python", "cpython", "ffi"] @@ -21,7 +21,7 @@ experimental-async = ["pyo3-macros-backend/experimental-async"] proc-macro2 = { version = "1.0.60", default-features = false } quote = "1" syn = { version = "2", features = ["full", "extra-traits"] } -pyo3-macros-backend = { path = "../pyo3-macros-backend", version = "=0.23.0-dev" } +pyo3-macros-backend = { path = "../pyo3-macros-backend", version = "=0.23.0" } [lints] workspace = true diff --git a/pyproject.toml b/pyproject.toml index 0b58bb9a71b..45d538a669a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ [tool.towncrier] filename = "CHANGELOG.md" -version = "0.23.0-dev" +version = "0.23.0" start_string = "\n" template = ".towncrier.template.md" title_format = "## [{version}] - {project_date}" diff --git a/tests/ui/reject_generics.stderr b/tests/ui/reject_generics.stderr index 78c3d00e624..1718c8adeae 100644 --- a/tests/ui/reject_generics.stderr +++ b/tests/ui/reject_generics.stderr @@ -1,10 +1,10 @@ -error: #[pyclass] cannot have generic parameters. For an explanation, see https://pyo3.rs/v0.23.0-dev/class.html#no-generic-parameters +error: #[pyclass] cannot have generic parameters. For an explanation, see https://pyo3.rs/v0.23.0/class.html#no-generic-parameters --> tests/ui/reject_generics.rs:4:25 | 4 | struct ClassWithGenerics { | ^ -error: #[pyclass] cannot have lifetime parameters. For an explanation, see https://pyo3.rs/v0.23.0-dev/class.html#no-lifetime-parameters +error: #[pyclass] cannot have lifetime parameters. For an explanation, see https://pyo3.rs/v0.23.0/class.html#no-lifetime-parameters --> tests/ui/reject_generics.rs:9:27 | 9 | struct ClassWithLifetimes<'a> { From c0f756c056f56d0796cebef4eac48a05db97f956 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Fri, 15 Nov 2024 17:07:10 +0000 Subject: [PATCH 376/495] fixup docs for packaging (#4710) --- README.md | 4 +- 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 +- pyo3-ffi/README.md | 128 ++++++++++++----- pyo3-ffi/src/lib.rs | 134 +++++++++++++++++- 8 files changed, 234 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index a131a823f0e..b086e82cae5 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,7 @@ name = "string_sum" crate-type = ["cdylib"] [dependencies] -pyo3 = { version = "0.22.5", features = ["extension-module"] } +pyo3 = { version = "0.23.0", features = ["extension-module"] } ``` **`src/lib.rs`** @@ -140,7 +140,7 @@ Start a new project with `cargo new` and add `pyo3` to the `Cargo.toml` like th ```toml [dependencies.pyo3] -version = "0.22.5" +version = "0.23.0" features = ["auto-initialize"] ``` diff --git a/examples/decorator/.template/pre-script.rhai b/examples/decorator/.template/pre-script.rhai index 3f9199c0f2f..d06e0e70994 100644 --- a/examples/decorator/.template/pre-script.rhai +++ b/examples/decorator/.template/pre-script.rhai @@ -1,4 +1,4 @@ -variable::set("PYO3_VERSION", "0.22.5"); +variable::set("PYO3_VERSION", "0.23.0"); 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 3f9199c0f2f..d06e0e70994 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.22.5"); +variable::set("PYO3_VERSION", "0.23.0"); 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 1d2e4657033..19694cc1f8c 100644 --- a/examples/plugin/.template/pre-script.rhai +++ b/examples/plugin/.template/pre-script.rhai @@ -1,4 +1,4 @@ -variable::set("PYO3_VERSION", "0.22.5"); +variable::set("PYO3_VERSION", "0.23.0"); 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 9a92b25c613..d3cf83d3a4d 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.22.5"); +variable::set("PYO3_VERSION", "0.23.0"); 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 3f9199c0f2f..d06e0e70994 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.22.5"); +variable::set("PYO3_VERSION", "0.23.0"); file::rename(".template/Cargo.toml", "Cargo.toml"); file::rename(".template/pyproject.toml", "pyproject.toml"); file::delete(".template"); diff --git a/pyo3-ffi/README.md b/pyo3-ffi/README.md index c5acc96ed3b..d9931b3cf48 100644 --- a/pyo3-ffi/README.md +++ b/pyo3-ffi/README.md @@ -41,13 +41,32 @@ name = "string_sum" crate-type = ["cdylib"] [dependencies.pyo3-ffi] -version = "*" +version = "0.23.0" features = ["extension-module"] + +[build-dependencies] +# This is only necessary if you need to configure your build based on +# the Python version or the compile-time configuration for the interpreter. +pyo3_build_config = "0.23.0" +``` + +If you need to use conditional compilation based on Python version or how +Python was compiled, you need to add `pyo3-build-config` as a +`build-dependency` in your `Cargo.toml` as in the example above and either +create a new `build.rs` file or modify an existing one so that +`pyo3_build_config::use_pyo3_cfgs()` gets called at build time: + +**`build.rs`** + +```rust,ignore +fn main() { + pyo3_build_config::use_pyo3_cfgs() +} ``` **`src/lib.rs`** ```rust -use std::os::raw::c_char; +use std::os::raw::{c_char, c_long}; use std::ptr; use pyo3_ffi::*; @@ -57,14 +76,14 @@ static mut MODULE_DEF: PyModuleDef = PyModuleDef { m_name: c_str!("string_sum").as_ptr(), m_doc: c_str!("A Python module written in Rust.").as_ptr(), m_size: 0, - m_methods: unsafe { METHODS.as_mut_ptr().cast() }, + m_methods: unsafe { METHODS as *const [PyMethodDef] as *mut PyMethodDef }, m_slots: std::ptr::null_mut(), m_traverse: None, m_clear: None, m_free: None, }; -static mut METHODS: [PyMethodDef; 2] = [ +static mut METHODS: &[PyMethodDef] = &[ PyMethodDef { ml_name: c_str!("sum_as_string").as_ptr(), ml_meth: PyMethodDefPointer { @@ -74,58 +93,99 @@ static mut METHODS: [PyMethodDef; 2] = [ ml_doc: c_str!("returns the sum of two integers as a string").as_ptr(), }, // A zeroed PyMethodDef to mark the end of the array. - PyMethodDef::zeroed() + PyMethodDef::zeroed(), ]; // The module initialization function, which must be named `PyInit_`. #[allow(non_snake_case)] #[no_mangle] pub unsafe extern "C" fn PyInit_string_sum() -> *mut PyObject { - PyModule_Create(ptr::addr_of_mut!(MODULE_DEF)) + let module = PyModule_Create(ptr::addr_of_mut!(MODULE_DEF)); + if module.is_null() { + return module; + } + #[cfg(Py_GIL_DISABLED)] + { + if PyUnstable_Module_SetGIL(module, Py_MOD_GIL_NOT_USED) < 0 { + Py_DECREF(module); + return std::ptr::null_mut(); + } + } + module } -pub unsafe extern "C" fn sum_as_string( - _self: *mut PyObject, - args: *mut *mut PyObject, - nargs: Py_ssize_t, -) -> *mut PyObject { - if nargs != 2 { - PyErr_SetString( - PyExc_TypeError, - c_str!("sum_as_string() expected 2 positional arguments").as_ptr(), +/// A helper to parse function arguments +/// If we used PyO3's proc macros they'd handle all of this boilerplate for us :) +unsafe fn parse_arg_as_i32(obj: *mut PyObject, n_arg: usize) -> Option { + if PyLong_Check(obj) == 0 { + let msg = format!( + "sum_as_string expected an int for positional argument {}\0", + n_arg ); - return std::ptr::null_mut(); + PyErr_SetString(PyExc_TypeError, msg.as_ptr().cast::()); + return None; } - let arg1 = *args; - if PyLong_Check(arg1) == 0 { - PyErr_SetString( - PyExc_TypeError, - c_str!("sum_as_string() expected an int for positional argument 1").as_ptr(), - ); - return std::ptr::null_mut(); + // Let's keep the behaviour consistent on platforms where `c_long` is bigger than 32 bits. + // In particular, it is an i32 on Windows but i64 on most Linux systems + let mut overflow = 0; + let i_long: c_long = PyLong_AsLongAndOverflow(obj, &mut overflow); + + #[allow(irrefutable_let_patterns)] // some platforms have c_long equal to i32 + if overflow != 0 { + raise_overflowerror(obj); + None + } else if let Ok(i) = i_long.try_into() { + Some(i) + } else { + raise_overflowerror(obj); + None } +} - let arg1 = PyLong_AsLong(arg1); - if !PyErr_Occurred().is_null() { - return ptr::null_mut(); +unsafe fn raise_overflowerror(obj: *mut PyObject) { + let obj_repr = PyObject_Str(obj); + if !obj_repr.is_null() { + let mut size = 0; + let p = PyUnicode_AsUTF8AndSize(obj_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 msg = format!("cannot fit {} in 32 bits\0", s); + + PyErr_SetString(PyExc_OverflowError, msg.as_ptr().cast::()); + } + Py_DECREF(obj_repr); } +} - let arg2 = *args.add(1); - if PyLong_Check(arg2) == 0 { +pub unsafe extern "C" fn sum_as_string( + _self: *mut PyObject, + args: *mut *mut PyObject, + nargs: Py_ssize_t, +) -> *mut PyObject { + if nargs != 2 { PyErr_SetString( PyExc_TypeError, - c_str!("sum_as_string() expected an int for positional argument 2").as_ptr(), + c_str!("sum_as_string expected 2 positional arguments").as_ptr(), ); return std::ptr::null_mut(); } - let arg2 = PyLong_AsLong(arg2); - if !PyErr_Occurred().is_null() { - return ptr::null_mut(); - } + let (first, second) = (*args, *args.add(1)); + + let first = match parse_arg_as_i32(first, 1) { + Some(x) => x, + None => return std::ptr::null_mut(), + }; + let second = match parse_arg_as_i32(second, 2) { + Some(x) => x, + None => return std::ptr::null_mut(), + }; - match arg1.checked_add(arg2) { + match first.checked_add(second) { Some(sum) => { let string = sum.to_string(); PyUnicode_FromStringAndSize(string.as_ptr().cast::(), string.len() as isize) diff --git a/pyo3-ffi/src/lib.rs b/pyo3-ffi/src/lib.rs index 23a5e0000b1..7bdba1173d6 100644 --- a/pyo3-ffi/src/lib.rs +++ b/pyo3-ffi/src/lib.rs @@ -130,7 +130,139 @@ //! //! **`src/lib.rs`** //! ```rust -#![doc = include_str!("../examples/string-sum/src/lib.rs")] +//! use std::os::raw::{c_char, c_long}; +//! use std::ptr; +//! +//! use pyo3_ffi::*; +//! +//! static mut MODULE_DEF: PyModuleDef = PyModuleDef { +//! m_base: PyModuleDef_HEAD_INIT, +//! m_name: c_str!("string_sum").as_ptr(), +//! m_doc: c_str!("A Python module written in Rust.").as_ptr(), +//! m_size: 0, +//! m_methods: unsafe { METHODS as *const [PyMethodDef] as *mut PyMethodDef }, +//! m_slots: std::ptr::null_mut(), +//! m_traverse: None, +//! m_clear: None, +//! m_free: None, +//! }; +//! +//! static mut METHODS: &[PyMethodDef] = &[ +//! PyMethodDef { +//! ml_name: c_str!("sum_as_string").as_ptr(), +//! ml_meth: PyMethodDefPointer { +//! PyCFunctionFast: sum_as_string, +//! }, +//! ml_flags: METH_FASTCALL, +//! ml_doc: c_str!("returns the sum of two integers as a string").as_ptr(), +//! }, +//! // A zeroed PyMethodDef to mark the end of the array. +//! PyMethodDef::zeroed(), +//! ]; +//! +//! // The module initialization function, which must be named `PyInit_`. +//! #[allow(non_snake_case)] +//! #[no_mangle] +//! pub unsafe extern "C" fn PyInit_string_sum() -> *mut PyObject { +//! let module = PyModule_Create(ptr::addr_of_mut!(MODULE_DEF)); +//! if module.is_null() { +//! return module; +//! } +//! #[cfg(Py_GIL_DISABLED)] +//! { +//! if PyUnstable_Module_SetGIL(module, Py_MOD_GIL_NOT_USED) < 0 { +//! Py_DECREF(module); +//! return std::ptr::null_mut(); +//! } +//! } +//! module +//! } +//! +//! /// A helper to parse function arguments +//! /// If we used PyO3's proc macros they'd handle all of this boilerplate for us :) +//! unsafe fn parse_arg_as_i32(obj: *mut PyObject, n_arg: usize) -> Option { +//! if PyLong_Check(obj) == 0 { +//! let msg = format!( +//! "sum_as_string expected an int for positional argument {}\0", +//! n_arg +//! ); +//! PyErr_SetString(PyExc_TypeError, msg.as_ptr().cast::()); +//! return None; +//! } +//! +//! // Let's keep the behaviour consistent on platforms where `c_long` is bigger than 32 bits. +//! // In particular, it is an i32 on Windows but i64 on most Linux systems +//! let mut overflow = 0; +//! let i_long: c_long = PyLong_AsLongAndOverflow(obj, &mut overflow); +//! +//! #[allow(irrefutable_let_patterns)] // some platforms have c_long equal to i32 +//! if overflow != 0 { +//! raise_overflowerror(obj); +//! None +//! } else if let Ok(i) = i_long.try_into() { +//! Some(i) +//! } else { +//! raise_overflowerror(obj); +//! None +//! } +//! } +//! +//! unsafe fn raise_overflowerror(obj: *mut PyObject) { +//! let obj_repr = PyObject_Str(obj); +//! if !obj_repr.is_null() { +//! let mut size = 0; +//! let p = PyUnicode_AsUTF8AndSize(obj_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 msg = format!("cannot fit {} in 32 bits\0", s); +//! +//! PyErr_SetString(PyExc_OverflowError, msg.as_ptr().cast::()); +//! } +//! Py_DECREF(obj_repr); +//! } +//! } +//! +//! pub unsafe extern "C" fn sum_as_string( +//! _self: *mut PyObject, +//! args: *mut *mut PyObject, +//! nargs: Py_ssize_t, +//! ) -> *mut PyObject { +//! if nargs != 2 { +//! PyErr_SetString( +//! PyExc_TypeError, +//! c_str!("sum_as_string expected 2 positional arguments").as_ptr(), +//! ); +//! return std::ptr::null_mut(); +//! } +//! +//! let (first, second) = (*args, *args.add(1)); +//! +//! let first = match parse_arg_as_i32(first, 1) { +//! Some(x) => x, +//! None => return std::ptr::null_mut(), +//! }; +//! let second = match parse_arg_as_i32(second, 2) { +//! Some(x) => x, +//! None => return std::ptr::null_mut(), +//! }; +//! +//! match first.checked_add(second) { +//! Some(sum) => { +//! let string = sum.to_string(); +//! PyUnicode_FromStringAndSize(string.as_ptr().cast::(), string.len() as isize) +//! } +//! None => { +//! PyErr_SetString( +//! PyExc_OverflowError, +//! c_str!("arguments too large to add").as_ptr(), +//! ); +//! std::ptr::null_mut() +//! } +//! } +//! } //! ``` //! //! With those two files in place, now `maturin` needs to be installed. This can be done using From a7fbe369194055a8b54e328677435bf12c942a02 Mon Sep 17 00:00:00 2001 From: Yotam Ofek Date: Fri, 15 Nov 2024 23:34:15 +0200 Subject: [PATCH 377/495] Fix migration guide URL (#4711) Original link was wrong --- src/conversion.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/conversion.rs b/src/conversion.rs index 46ff4af3a62..5986aefd6e3 100644 --- a/src/conversion.rs +++ b/src/conversion.rs @@ -69,7 +69,7 @@ pub unsafe trait AsPyPointer { /// Conversion trait that allows various objects to be converted into `PyObject`. #[deprecated( since = "0.23.0", - note = "`ToPyObject` is going to be replaced by `IntoPyObject`. See the migration guide (https://pyo3.rs/v0.23/migration) for more information." + note = "`ToPyObject` is going to be replaced by `IntoPyObject`. See the migration guide (https://pyo3.rs/v0.23.0/migration) for more information." )] pub trait ToPyObject { /// Converts self into a Python object. @@ -169,7 +169,7 @@ pub trait ToPyObject { )] #[deprecated( since = "0.23.0", - note = "`IntoPy` is going to be replaced by `IntoPyObject`. See the migration guide (https://pyo3.rs/v0.23/migration) for more information." + note = "`IntoPy` is going to be replaced by `IntoPyObject`. See the migration guide (https://pyo3.rs/v0.23.0/migration) for more information." )] pub trait IntoPy: Sized { /// Performs the conversion. From 46db18e046ef8eeb153f5a50459f04cfa852b3f4 Mon Sep 17 00:00:00 2001 From: Yotam Ofek Date: Sat, 16 Nov 2024 00:04:11 +0200 Subject: [PATCH 378/495] docs: Don't try to build `gil-refs` feature (#4712) Build for v0.23.0 docs is currently failing (https://docs.rs/crate/pyo3/0.23.0/builds/1542136) --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 1dc198fecac..92579f2ec58 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -147,7 +147,7 @@ members = [ [package.metadata.docs.rs] no-default-features = true -features = ["full", "gil-refs"] +features = ["full"] rustdoc-args = ["--cfg", "docsrs"] [workspace.lints.clippy] From df614e0a6aa28c3b61261d90c8e6639b6fc0eab3 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Sun, 17 Nov 2024 04:06:08 +0000 Subject: [PATCH 379/495] release: 0.23.1 (#4715) --- CHANGELOG.md | 7 ++++++- Cargo.toml | 8 ++++---- README.md | 4 ++-- examples/decorator/.template/pre-script.rhai | 2 +- examples/maturin-starter/.template/pre-script.rhai | 2 +- examples/plugin/.template/pre-script.rhai | 2 +- .../setuptools-rust-starter/.template/pre-script.rhai | 2 +- examples/word-count/.template/pre-script.rhai | 2 +- pyo3-build-config/Cargo.toml | 2 +- pyo3-ffi/Cargo.toml | 4 ++-- pyo3-ffi/README.md | 4 ++-- pyo3-macros-backend/Cargo.toml | 6 +++--- pyo3-macros/Cargo.toml | 4 ++-- pyproject.toml | 2 +- tests/ui/reject_generics.stderr | 4 ++-- 15 files changed, 30 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eef2151e07c..4bfce433293 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,10 @@ To see unreleased changes, please see the [CHANGELOG on the main branch guide](h +## [0.23.1] - 2024-11-16 + +Re-release of 0.23.0 with fixes to docs.rs build. + ## [0.23.0] - 2024-11-15 ### Packaging @@ -1996,7 +2000,8 @@ Yanked - Initial release -[Unreleased]: https://github.com/pyo3/pyo3/compare/v0.23.0...HEAD +[Unreleased]: https://github.com/pyo3/pyo3/compare/v0.23.1...HEAD +[0.23.1]: https://github.com/pyo3/pyo3/compare/v0.23.0...v0.23.1 [0.23.0]: https://github.com/pyo3/pyo3/compare/v0.22.5...v0.23.0 [0.22.5]: https://github.com/pyo3/pyo3/compare/v0.22.4...v0.22.5 [0.22.4]: https://github.com/pyo3/pyo3/compare/v0.22.3...v0.22.4 diff --git a/Cargo.toml b/Cargo.toml index 92579f2ec58..3eca038e054 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3" -version = "0.23.0" +version = "0.23.1" description = "Bindings to Python interpreter" authors = ["PyO3 Project and Contributors "] readme = "README.md" @@ -21,10 +21,10 @@ memoffset = "0.9" once_cell = "1.13" # ffi bindings to the python interpreter, split into a separate crate so they can be used independently -pyo3-ffi = { path = "pyo3-ffi", version = "=0.23.0" } +pyo3-ffi = { path = "pyo3-ffi", version = "=0.23.1" } # support crates for macros feature -pyo3-macros = { path = "pyo3-macros", version = "=0.23.0", optional = true } +pyo3-macros = { path = "pyo3-macros", version = "=0.23.1", optional = true } indoc = { version = "2.0.1", optional = true } unindent = { version = "0.2.1", optional = true } @@ -66,7 +66,7 @@ static_assertions = "1.1.0" uuid = {version = "1.10.0", features = ["v4"] } [build-dependencies] -pyo3-build-config = { path = "pyo3-build-config", version = "=0.23.0", features = ["resolve-config"] } +pyo3-build-config = { path = "pyo3-build-config", version = "=0.23.1", features = ["resolve-config"] } [features] default = ["macros"] diff --git a/README.md b/README.md index b086e82cae5..c0a46af44d6 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,7 @@ name = "string_sum" crate-type = ["cdylib"] [dependencies] -pyo3 = { version = "0.23.0", features = ["extension-module"] } +pyo3 = { version = "0.23.1", features = ["extension-module"] } ``` **`src/lib.rs`** @@ -140,7 +140,7 @@ Start a new project with `cargo new` and add `pyo3` to the `Cargo.toml` like th ```toml [dependencies.pyo3] -version = "0.23.0" +version = "0.23.1" features = ["auto-initialize"] ``` diff --git a/examples/decorator/.template/pre-script.rhai b/examples/decorator/.template/pre-script.rhai index d06e0e70994..db0f2e8b002 100644 --- a/examples/decorator/.template/pre-script.rhai +++ b/examples/decorator/.template/pre-script.rhai @@ -1,4 +1,4 @@ -variable::set("PYO3_VERSION", "0.23.0"); +variable::set("PYO3_VERSION", "0.23.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 d06e0e70994..db0f2e8b002 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.23.0"); +variable::set("PYO3_VERSION", "0.23.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 19694cc1f8c..90eb43e817c 100644 --- a/examples/plugin/.template/pre-script.rhai +++ b/examples/plugin/.template/pre-script.rhai @@ -1,4 +1,4 @@ -variable::set("PYO3_VERSION", "0.23.0"); +variable::set("PYO3_VERSION", "0.23.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 d3cf83d3a4d..eeb279d1497 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.23.0"); +variable::set("PYO3_VERSION", "0.23.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 d06e0e70994..db0f2e8b002 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.23.0"); +variable::set("PYO3_VERSION", "0.23.1"); file::rename(".template/Cargo.toml", "Cargo.toml"); file::rename(".template/pyproject.toml", "pyproject.toml"); file::delete(".template"); diff --git a/pyo3-build-config/Cargo.toml b/pyo3-build-config/Cargo.toml index f760152a087..29cecb0cf95 100644 --- a/pyo3-build-config/Cargo.toml +++ b/pyo3-build-config/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3-build-config" -version = "0.23.0" +version = "0.23.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 74f23aa5429..3b7a63fe3d9 100644 --- a/pyo3-ffi/Cargo.toml +++ b/pyo3-ffi/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3-ffi" -version = "0.23.0" +version = "0.23.1" description = "Python-API bindings for the PyO3 ecosystem" authors = ["PyO3 Project and Contributors "] keywords = ["pyo3", "python", "cpython", "ffi"] @@ -41,7 +41,7 @@ generate-import-lib = ["pyo3-build-config/python3-dll-a"] paste = "1" [build-dependencies] -pyo3-build-config = { path = "../pyo3-build-config", version = "=0.23.0", features = ["resolve-config"] } +pyo3-build-config = { path = "../pyo3-build-config", version = "=0.23.1", features = ["resolve-config"] } [lints] workspace = true diff --git a/pyo3-ffi/README.md b/pyo3-ffi/README.md index d9931b3cf48..097822709bd 100644 --- a/pyo3-ffi/README.md +++ b/pyo3-ffi/README.md @@ -41,13 +41,13 @@ name = "string_sum" crate-type = ["cdylib"] [dependencies.pyo3-ffi] -version = "0.23.0" +version = "0.23.1" features = ["extension-module"] [build-dependencies] # This is only necessary if you need to configure your build based on # the Python version or the compile-time configuration for the interpreter. -pyo3_build_config = "0.23.0" +pyo3_build_config = "0.23.1" ``` If you need to use conditional compilation based on Python version or how diff --git a/pyo3-macros-backend/Cargo.toml b/pyo3-macros-backend/Cargo.toml index ec74aa07640..864d0b3712a 100644 --- a/pyo3-macros-backend/Cargo.toml +++ b/pyo3-macros-backend/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3-macros-backend" -version = "0.23.0" +version = "0.23.1" description = "Code generation for PyO3 package" authors = ["PyO3 Project and Contributors "] keywords = ["pyo3", "python", "cpython", "ffi"] @@ -16,7 +16,7 @@ edition = "2021" [dependencies] heck = "0.5" proc-macro2 = { version = "1.0.60", default-features = false } -pyo3-build-config = { path = "../pyo3-build-config", version = "=0.23.0", features = ["resolve-config"] } +pyo3-build-config = { path = "../pyo3-build-config", version = "=0.23.1", features = ["resolve-config"] } quote = { version = "1", default-features = false } [dependencies.syn] @@ -25,7 +25,7 @@ default-features = false features = ["derive", "parsing", "printing", "clone-impls", "full", "extra-traits"] [build-dependencies] -pyo3-build-config = { path = "../pyo3-build-config", version = "=0.23.0" } +pyo3-build-config = { path = "../pyo3-build-config", version = "=0.23.1" } [lints] workspace = true diff --git a/pyo3-macros/Cargo.toml b/pyo3-macros/Cargo.toml index 4fb52ea1e69..18b79161f25 100644 --- a/pyo3-macros/Cargo.toml +++ b/pyo3-macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3-macros" -version = "0.23.0" +version = "0.23.1" description = "Proc macros for PyO3 package" authors = ["PyO3 Project and Contributors "] keywords = ["pyo3", "python", "cpython", "ffi"] @@ -21,7 +21,7 @@ experimental-async = ["pyo3-macros-backend/experimental-async"] proc-macro2 = { version = "1.0.60", default-features = false } quote = "1" syn = { version = "2", features = ["full", "extra-traits"] } -pyo3-macros-backend = { path = "../pyo3-macros-backend", version = "=0.23.0" } +pyo3-macros-backend = { path = "../pyo3-macros-backend", version = "=0.23.1" } [lints] workspace = true diff --git a/pyproject.toml b/pyproject.toml index 45d538a669a..211a374db59 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ [tool.towncrier] filename = "CHANGELOG.md" -version = "0.23.0" +version = "0.23.1" start_string = "\n" template = ".towncrier.template.md" title_format = "## [{version}] - {project_date}" diff --git a/tests/ui/reject_generics.stderr b/tests/ui/reject_generics.stderr index 1718c8adeae..5adb8eaca9e 100644 --- a/tests/ui/reject_generics.stderr +++ b/tests/ui/reject_generics.stderr @@ -1,10 +1,10 @@ -error: #[pyclass] cannot have generic parameters. For an explanation, see https://pyo3.rs/v0.23.0/class.html#no-generic-parameters +error: #[pyclass] cannot have generic parameters. For an explanation, see https://pyo3.rs/v0.23.1/class.html#no-generic-parameters --> tests/ui/reject_generics.rs:4:25 | 4 | struct ClassWithGenerics { | ^ -error: #[pyclass] cannot have lifetime parameters. For an explanation, see https://pyo3.rs/v0.23.0/class.html#no-lifetime-parameters +error: #[pyclass] cannot have lifetime parameters. For an explanation, see https://pyo3.rs/v0.23.1/class.html#no-lifetime-parameters --> tests/ui/reject_generics.rs:9:27 | 9 | struct ClassWithLifetimes<'a> { From e37e985a6d8de181160d188c06385ddf6ac2db39 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Nov 2024 22:30:22 +0000 Subject: [PATCH 380/495] Bump codecov/codecov-action from 4 to 5 (#4718) * Bump codecov/codecov-action from 4 to 5 Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 4 to 5. - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/v4...v5) --- updated-dependencies: - dependency-name: codecov/codecov-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] * format ci yml * fix "file" -> "files" deprecation --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: David Hewitt --- .github/workflows/ci.yml | 164 +++++++++++++++++++-------------------- 1 file changed, 81 insertions(+), 83 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index af1217b31fa..b0bf3ea4bab 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,7 +23,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: - python-version: '3.12' + python-version: "3.12" - run: python -m pip install --upgrade pip && pip install nox - uses: dtolnay/rust-toolchain@stable with: @@ -41,11 +41,10 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: - python-version: '3.12' + python-version: "3.12" - name: resolve MSRV id: resolve-msrv - run: - echo MSRV=`python -c 'import tomllib; print(tomllib.load(open("Cargo.toml", "rb"))["package"]["rust-version"])'` >> $GITHUB_OUTPUT + run: echo MSRV=`python -c 'import tomllib; print(tomllib.load(open("Cargo.toml", "rb"))["package"]["rust-version"])'` >> $GITHUB_OUTPUT semver-checks: if: github.ref != 'refs/heads/main' @@ -55,7 +54,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: - python-version: '3.12' + python-version: "3.12" - uses: obi1kenobi/cargo-semver-checks-action@v2 check-msrv: @@ -69,7 +68,7 @@ jobs: components: rust-src - uses: actions/setup-python@v5 with: - python-version: '3.12' + python-version: "3.12" - uses: Swatinem/rust-cache@v2 with: save-if: ${{ github.event_name != 'merge_group' }} @@ -91,43 +90,44 @@ jobs: fail-fast: ${{ !contains(github.event.pull_request.labels.*.name, 'CI-no-fail-fast') }} matrix: rust: [stable] - platform: [ - { - os: "macos-latest", - python-architecture: "arm64", - rust-target: "aarch64-apple-darwin", - }, - { - os: "ubuntu-latest", - python-architecture: "x64", - rust-target: "x86_64-unknown-linux-gnu", - }, - { - os: "ubuntu-latest", - python-architecture: "x64", - rust-target: "powerpc64le-unknown-linux-gnu", - }, - { - os: "ubuntu-latest", - python-architecture: "x64", - rust-target: "s390x-unknown-linux-gnu", - }, - { - os: "ubuntu-latest", - python-architecture: "x64", - rust-target: "wasm32-wasi", - }, - { - os: "windows-latest", - python-architecture: "x64", - rust-target: "x86_64-pc-windows-msvc", - }, - { - os: "windows-latest", - python-architecture: "x86", - rust-target: "i686-pc-windows-msvc", - }, - ] + platform: + [ + { + os: "macos-latest", + python-architecture: "arm64", + rust-target: "aarch64-apple-darwin", + }, + { + os: "ubuntu-latest", + python-architecture: "x64", + rust-target: "x86_64-unknown-linux-gnu", + }, + { + os: "ubuntu-latest", + python-architecture: "x64", + rust-target: "powerpc64le-unknown-linux-gnu", + }, + { + os: "ubuntu-latest", + python-architecture: "x64", + rust-target: "s390x-unknown-linux-gnu", + }, + { + os: "ubuntu-latest", + python-architecture: "x64", + rust-target: "wasm32-wasi", + }, + { + os: "windows-latest", + python-architecture: "x64", + rust-target: "x86_64-pc-windows-msvc", + }, + { + os: "windows-latest", + python-architecture: "x86", + 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 @@ -148,7 +148,7 @@ jobs: components: clippy,rust-src - uses: actions/setup-python@v5 with: - python-version: '3.12' + python-version: "3.12" architecture: ${{ matrix.platform.python-architecture }} - uses: Swatinem/rust-cache@v2 with: @@ -177,15 +177,14 @@ jobs: matrix: rust: [stable] python-version: ["3.12"] - platform: - [ + platform: [ { - os: "macos-latest", # first available arm macos runner + os: "macos-latest", # first available arm macos runner python-architecture: "arm64", rust-target: "aarch64-apple-darwin", }, { - os: "macos-13", # last available x86_64 macos runner + os: "macos-13", # last available x86_64 macos runner python-architecture: "x64", rust-target: "x86_64-apple-darwin", }, @@ -234,18 +233,19 @@ jobs: fail-fast: ${{ !contains(github.event.pull_request.labels.*.name, 'CI-no-fail-fast') }} matrix: rust: [stable] - python-version: [ - "3.7", - "3.8", - "3.9", - "3.10", - "3.11", - "3.12", - "3.13", - "pypy3.9", - "pypy3.10", - "graalpy24.0", - ] + python-version: + [ + "3.7", + "3.8", + "3.9", + "3.10", + "3.11", + "3.12", + "3.13", + "pypy3.9", + "pypy3.10", + "graalpy24.0", + ] platform: [ { @@ -389,7 +389,7 @@ jobs: with: # FIXME valgrind detects an issue with Python 3.12.5, needs investigation # whether it's a PyO3 issue or upstream CPython. - python-version: '3.12.4' + python-version: "3.12.4" - uses: Swatinem/rust-cache@v2 with: save-if: ${{ github.event_name != 'merge_group' }} @@ -410,7 +410,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: - python-version: '3.12' + python-version: "3.12" - uses: Swatinem/rust-cache@v2 with: save-if: ${{ github.event_name != 'merge_group' }} @@ -432,7 +432,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: - python-version: '3.12' + python-version: "3.12" - uses: Swatinem/rust-cache@v2 with: save-if: ${{ github.event_name != 'merge_group' }} @@ -442,7 +442,7 @@ jobs: - run: cargo rustdoc --lib --no-default-features --features full -Zunstable-options --config "build.rustdocflags=[\"--cfg\", \"docsrs\"]" coverage: - if : ${{ github.event_name != 'merge_group' }} + if: ${{ github.event_name != 'merge_group' }} needs: [fmt] name: coverage ${{ matrix.os }} strategy: @@ -453,7 +453,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: - python-version: '3.12' + python-version: "3.12" - uses: Swatinem/rust-cache@v2 with: save-if: ${{ github.event_name != 'merge_group' }} @@ -464,9 +464,9 @@ jobs: uses: taiki-e/install-action@cargo-llvm-cov - run: python -m pip install --upgrade pip && pip install nox - run: nox -s coverage - - uses: codecov/codecov-action@v4 + - uses: codecov/codecov-action@v5 with: - file: coverage.json + files: coverage.json name: ${{ matrix.os }} token: ${{ secrets.CODECOV_TOKEN }} @@ -552,11 +552,11 @@ jobs: test-free-threaded: needs: [fmt] - name: Free threaded tests - ${{ matrix.runner }} - runs-on: ${{ matrix.runner }} + name: Free threaded tests - ${{ matrix.os }} + runs-on: ${{ matrix.os }} strategy: matrix: - runner: ["ubuntu-latest", "macos-latest", "windows-latest"] + os: ["ubuntu-latest", "macos-latest", "windows-latest"] steps: - uses: actions/checkout@v4 - uses: Swatinem/rust-cache@v2 @@ -568,7 +568,7 @@ jobs: # TODO: replace with actions/setup-python when there is support - uses: quansight-labs/setup-python@v5.3.1 with: - python-version: '3.13t' + python-version: "3.13t" - name: Install cargo-llvm-cov uses: taiki-e/install-action@cargo-llvm-cov - run: python3 -m sysconfig @@ -582,10 +582,10 @@ jobs: - name: Generate coverage report run: nox -s generate-coverage-report - name: Upload coverage report - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v5 with: - file: coverage.json - name: test-free-threaded + files: coverage.json + name: ${{ matrix.os }}-test-free-threaded token: ${{ secrets.CODECOV_TOKEN }} test-version-limits: @@ -596,7 +596,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: - python-version: '3.12' + python-version: "3.12" - uses: Swatinem/rust-cache@v2 with: save-if: ${{ github.event_name != 'merge_group' }} @@ -620,7 +620,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: - python-version: '3.12' + python-version: "3.12" - uses: Swatinem/rust-cache@v2 with: save-if: ${{ github.event_name != 'merge_group' }} @@ -659,7 +659,7 @@ jobs: flags: "-i python3.12 --features abi3 --features generate-import-lib" manylinux: off # macos x86_64 -> aarch64 - - os: "macos-13" # last x86_64 macos runners + - os: "macos-13" # last x86_64 macos runners target: "aarch64-apple-darwin" # macos aarch64 -> x86_64 - os: "macos-latest" @@ -668,11 +668,10 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: - python-version: '3.12' + python-version: "3.12" - uses: Swatinem/rust-cache@v2 with: - workspaces: - examples/maturin-starter + workspaces: examples/maturin-starter save-if: ${{ github.event_name != 'merge_group' }} key: ${{ matrix.target }} - name: Setup cross-compiler @@ -692,11 +691,10 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: - python-version: '3.12' + python-version: "3.12" - uses: Swatinem/rust-cache@v2 with: - workspaces: - examples/maturin-starter + workspaces: examples/maturin-starter save-if: ${{ github.event_name != 'merge_group' }} - uses: actions/cache/restore@v4 with: From 4be47599ba8d6242a062483133c5abd8176192dc Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Tue, 19 Nov 2024 22:21:17 +0000 Subject: [PATCH 381/495] add `IntoPyObjectExt` trait (#4708) * add `IntoPyObjectExt` trait * adjust method names, more docs & usage internally * more uses of `IntoPyObjectExt` * guide docs * newsfragment * fixup doctest * Update guide/src/conversions/traits.md Co-authored-by: Icxolu <10486322+Icxolu@users.noreply.github.com> --------- Co-authored-by: Icxolu <10486322+Icxolu@users.noreply.github.com> --- guide/src/conversions/traits.md | 82 ++++++++++++++------------ newsfragments/4708.added.md | 1 + pyo3-macros-backend/src/pyclass.rs | 35 +++--------- pyo3-macros-backend/src/pyimpl.rs | 5 +- src/conversion.rs | 77 +++++++++++++++++++++---- src/conversions/either.rs | 28 ++------- src/coroutine.rs | 7 +-- src/impl_/pyclass.rs | 15 ++--- src/impl_/wrap.rs | 11 +--- src/instance.rs | 4 +- src/lib.rs | 2 +- src/types/any.rs | 92 ++++++------------------------ src/types/dict.rs | 38 ++++-------- src/types/frozenset.rs | 10 +--- src/types/list.rs | 30 +++------- src/types/module.rs | 16 +++--- src/types/sequence.rs | 28 +++------ src/types/set.rs | 20 ++----- src/types/tuple.rs | 28 +++++---- src/types/weakref/proxy.rs | 11 ++-- src/types/weakref/reference.rs | 11 ++-- 21 files changed, 218 insertions(+), 333 deletions(-) create mode 100644 newsfragments/4708.added.md diff --git a/guide/src/conversions/traits.md b/guide/src/conversions/traits.md index 5e3da6ea1b0..c4e8f14866c 100644 --- a/guide/src/conversions/traits.md +++ b/guide/src/conversions/traits.md @@ -490,9 +490,11 @@ If the input is neither a string nor an integer, the error message will be: - the function signature must be `fn(&Bound) -> PyResult` where `T` is the Rust type of the argument. ### `IntoPyObject` -This trait defines the to-python conversion for a Rust type. All types in PyO3 implement this trait, +The ['IntoPyObject'] trait defines the to-python conversion for a Rust type. All types in PyO3 implement this trait, as does a `#[pyclass]` which doesn't use `extends`. +This trait defines a single method, `into_pyobject()`, which returns a [`Result`] with `Ok` and `Err` types depending on the input value. For convenience, there is a companion [`IntoPyObjectExt`] trait which adds methods such as `into_py_any()` which converts the `Ok` and `Err` types to commonly used types (in the case of `into_py_any()`, `Py` and `PyErr` respectively). + Occasionally you may choose to implement this for custom types which are mapped to Python types _without_ having a unique python type. @@ -510,7 +512,7 @@ into `PyTuple` with the fields in declaration order. // structs convert into `PyDict` with field names as keys #[derive(IntoPyObject)] -struct Struct { +struct Struct { count: usize, obj: Py, } @@ -532,11 +534,11 @@ forward the implementation to the inner type. // newtype tuple structs are implicitly `transparent` #[derive(IntoPyObject)] -struct TransparentTuple(PyObject); +struct TransparentTuple(PyObject); #[derive(IntoPyObject)] #[pyo3(transparent)] -struct TransparentStruct<'py> { +struct TransparentStruct<'py> { inner: Bound<'py, PyAny>, // `'py` lifetime will be used as the Python lifetime } ``` @@ -582,7 +584,7 @@ impl<'py> IntoPyObject<'py> for MyPyObjectWrapper { } } -// equivalent to former `ToPyObject` implementations +// equivalent to former `ToPyObject` implementations impl<'a, 'py> IntoPyObject<'py> for &'a MyPyObjectWrapper { type Target = PyAny; type Output = Borrowed<'a, 'py, Self::Target>; // `Borrowed` can be used to optimized reference counting @@ -594,38 +596,6 @@ impl<'a, 'py> IntoPyObject<'py> for &'a MyPyObjectWrapper { } ``` -### `IntoPy` - -
- -⚠️ Warning: API update in progress 🛠️ - -PyO3 0.23 has introduced `IntoPyObject` as the new trait for to-python conversions. While `#[pymethods]` and `#[pyfunction]` contain a compatibility layer to allow `IntoPy` as a return type, all Python API have been migrated to use `IntoPyObject`. To migrate implement `IntoPyObject` for your type. -
- - -This trait defines the to-python conversion for a Rust type. It is usually implemented as -`IntoPy`, which is the trait needed for returning a value from `#[pyfunction]` and -`#[pymethods]`. - -All types in PyO3 implement this trait, as does a `#[pyclass]` which doesn't use `extends`. - -Occasionally you may choose to implement this for custom types which are mapped to Python types -_without_ having a unique python type. - -```rust -use pyo3::prelude::*; -# #[allow(dead_code)] -struct MyPyObjectWrapper(PyObject); - -#[allow(deprecated)] -impl IntoPy for MyPyObjectWrapper { - fn into_py(self, py: Python<'_>) -> PyObject { - self.0 - } -} -``` - #### `BoundObject` for conversions that may be `Bound` or `Borrowed` `IntoPyObject::into_py_object` returns either `Bound` or `Borrowed` depending on the implementation for a concrete type. For example, the `IntoPyObject` implementation for `u32` produces a `Bound<'py, PyInt>` and the `bool` implementation produces a `Borrowed<'py, 'py, PyBool>`: @@ -672,6 +642,8 @@ where the_vec.iter() .map(|x| { Ok( + // Note: the below is equivalent to `x.into_py_any()` + // from the `IntoPyObjectExt` trait x.into_pyobject(py) .map_err(Into::into)? .into_any() @@ -693,6 +665,38 @@ let vec_of_pyobjs: Vec> = Python::with_gil(|py| { In the example above we used `BoundObject::into_any` and `BoundObject::unbind` to manipulate the python types and smart pointers into the result type we wanted to produce from the function. +### `IntoPy` + +
+ +⚠️ Warning: API update in progress 🛠️ + +PyO3 0.23 has introduced `IntoPyObject` as the new trait for to-python conversions. While `#[pymethods]` and `#[pyfunction]` contain a compatibility layer to allow `IntoPy` as a return type, all Python API have been migrated to use `IntoPyObject`. To migrate implement `IntoPyObject` for your type. +
+ + +This trait defines the to-python conversion for a Rust type. It is usually implemented as +`IntoPy`, which is the trait needed for returning a value from `#[pyfunction]` and +`#[pymethods]`. + +All types in PyO3 implement this trait, as does a `#[pyclass]` which doesn't use `extends`. + +Occasionally you may choose to implement this for custom types which are mapped to Python types +_without_ having a unique python type. + +```rust +use pyo3::prelude::*; +# #[allow(dead_code)] +struct MyPyObjectWrapper(PyObject); + +#[allow(deprecated)] +impl IntoPy for MyPyObjectWrapper { + fn into_py(self, py: Python<'_>) -> PyObject { + self.0 + } +} +``` + ### The `ToPyObject` trait
@@ -710,8 +714,12 @@ same purpose, except that it consumes `self`. [`IntoPy`]: {{#PYO3_DOCS_URL}}/pyo3/conversion/trait.IntoPy.html [`FromPyObject`]: {{#PYO3_DOCS_URL}}/pyo3/conversion/trait.FromPyObject.html [`ToPyObject`]: {{#PYO3_DOCS_URL}}/pyo3/conversion/trait.ToPyObject.html +[`IntoPyObject`]: {{#PYO3_DOCS_URL}}/pyo3/conversion/trait.IntoPyObject.html +[`IntoPyObjectExt`]: {{#PYO3_DOCS_URL}}/pyo3/conversion/trait.IntoPyObjectExt.html [`PyObject`]: {{#PYO3_DOCS_URL}}/pyo3/type.PyObject.html [`PyRef`]: {{#PYO3_DOCS_URL}}/pyo3/pycell/struct.PyRef.html [`PyRefMut`]: {{#PYO3_DOCS_URL}}/pyo3/pycell/struct.PyRefMut.html [`BoundObject`]: {{#PYO3_DOCS_URL}}/pyo3/instance/trait.BoundObject.html + +[`Result`]: https://doc.rust-lang.org/stable/std/result/enum.Result.html diff --git a/newsfragments/4708.added.md b/newsfragments/4708.added.md new file mode 100644 index 00000000000..c8c91d16221 --- /dev/null +++ b/newsfragments/4708.added.md @@ -0,0 +1 @@ +Add `IntoPyObjectExt` trait. diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index a83ba880271..87d02c6f878 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -1367,10 +1367,7 @@ fn impl_complex_enum_tuple_variant_getitem( .map(|i| { let field_access = format_ident!("_{}", i); quote! { #i => - #pyo3_path::IntoPyObject::into_pyobject(#variant_cls::#field_access(slf)?, py) - .map(#pyo3_path::BoundObject::into_any) - .map(#pyo3_path::BoundObject::unbind) - .map_err(::std::convert::Into::into) + #pyo3_path::IntoPyObjectExt::into_py_any(#variant_cls::#field_access(slf)?, py) } }) .collect(); @@ -1852,16 +1849,10 @@ fn pyclass_richcmp_arms( .map(|span| { quote_spanned! { span => #pyo3_path::pyclass::CompareOp::Eq => { - #pyo3_path::IntoPyObject::into_pyobject(self_val == other, py) - .map(#pyo3_path::BoundObject::into_any) - .map(#pyo3_path::BoundObject::unbind) - .map_err(::std::convert::Into::into) + #pyo3_path::IntoPyObjectExt::into_py_any(self_val == other, py) }, #pyo3_path::pyclass::CompareOp::Ne => { - #pyo3_path::IntoPyObject::into_pyobject(self_val != other, py) - .map(#pyo3_path::BoundObject::into_any) - .map(#pyo3_path::BoundObject::unbind) - .map_err(::std::convert::Into::into) + #pyo3_path::IntoPyObjectExt::into_py_any(self_val != other, py) }, } }) @@ -1876,28 +1867,16 @@ fn pyclass_richcmp_arms( .map(|ord| { quote_spanned! { ord.span() => #pyo3_path::pyclass::CompareOp::Gt => { - #pyo3_path::IntoPyObject::into_pyobject(self_val > other, py) - .map(#pyo3_path::BoundObject::into_any) - .map(#pyo3_path::BoundObject::unbind) - .map_err(::std::convert::Into::into) + #pyo3_path::IntoPyObjectExt::into_py_any(self_val > other, py) }, #pyo3_path::pyclass::CompareOp::Lt => { - #pyo3_path::IntoPyObject::into_pyobject(self_val < other, py) - .map(#pyo3_path::BoundObject::into_any) - .map(#pyo3_path::BoundObject::unbind) - .map_err(::std::convert::Into::into) + #pyo3_path::IntoPyObjectExt::into_py_any(self_val < other, py) }, #pyo3_path::pyclass::CompareOp::Le => { - #pyo3_path::IntoPyObject::into_pyobject(self_val <= other, py) - .map(#pyo3_path::BoundObject::into_any) - .map(#pyo3_path::BoundObject::unbind) - .map_err(::std::convert::Into::into) + #pyo3_path::IntoPyObjectExt::into_py_any(self_val <= other, py) }, #pyo3_path::pyclass::CompareOp::Ge => { - #pyo3_path::IntoPyObject::into_pyobject(self_val >= other, py) - .map(#pyo3_path::BoundObject::into_any) - .map(#pyo3_path::BoundObject::unbind) - .map_err(::std::convert::Into::into) + #pyo3_path::IntoPyObjectExt::into_py_any(self_val >= other, py) }, } }) diff --git a/pyo3-macros-backend/src/pyimpl.rs b/pyo3-macros-backend/src/pyimpl.rs index 614db3c5459..72f06721ec4 100644 --- a/pyo3-macros-backend/src/pyimpl.rs +++ b/pyo3-macros-backend/src/pyimpl.rs @@ -213,10 +213,7 @@ pub fn gen_py_const(cls: &syn::Type, spec: &ConstSpec, ctx: &Ctx) -> MethodAndMe let associated_method = quote! { fn #wrapper_ident(py: #pyo3_path::Python<'_>) -> #pyo3_path::PyResult<#pyo3_path::PyObject> { - #pyo3_path::IntoPyObject::into_pyobject(#cls::#member, py) - .map(#pyo3_path::BoundObject::into_any) - .map(#pyo3_path::BoundObject::unbind) - .map_err(::std::convert::Into::into) + #pyo3_path::IntoPyObjectExt::into_py_any(#cls::#member, py) } }; diff --git a/src/conversion.rs b/src/conversion.rs index 5986aefd6e3..82ad4d84977 100644 --- a/src/conversion.rs +++ b/src/conversion.rs @@ -178,8 +178,23 @@ pub trait IntoPy: Sized { /// Defines a conversion from a Rust type to a Python object, which may fail. /// +/// This trait has `#[derive(IntoPyObject)]` to automatically implement it for simple types and +/// `#[derive(IntoPyObjectRef)]` to implement the same for references. +/// /// It functions similarly to std's [`TryInto`] trait, but requires a [GIL token](Python) /// as an argument. +/// +/// The [`into_pyobject`][IntoPyObject::into_pyobject] method is designed for maximum flexibility and efficiency; it +/// - allows for a concrete Python type to be returned (the [`Target`][IntoPyObject::Target] associated type) +/// - allows for the smart pointer containing the Python object to be either `Bound<'py, Self::Target>` or `Borrowed<'a, 'py, Self::Target>` +/// to avoid unnecessary reference counting overhead +/// - allows for a custom error type to be returned in the event of a conversion error to avoid +/// unnecessarily creating a Python exception +/// +/// # See also +/// +/// - The [`IntoPyObjectExt`] trait, which provides convenience methods for common usages of +/// `IntoPyObject` which erase type information and convert errors to `PyErr`. #[cfg_attr( diagnostic_namespace, diagnostic::on_unimplemented( @@ -227,12 +242,7 @@ pub trait IntoPyObject<'py>: Sized { I: IntoIterator + AsRef<[Self]>, I::IntoIter: ExactSizeIterator, { - let mut iter = iter.into_iter().map(|e| { - e.into_pyobject(py) - .map(BoundObject::into_any) - .map(BoundObject::into_bound) - .map_err(Into::into) - }); + let mut iter = iter.into_iter().map(|e| e.into_bound_py_any(py)); let list = crate::types::list::try_new_from_iter(py, &mut iter); list.map(Bound::into_any) } @@ -250,12 +260,7 @@ pub trait IntoPyObject<'py>: Sized { I: IntoIterator + AsRef<[::BaseType]>, I::IntoIter: ExactSizeIterator, { - let mut iter = iter.into_iter().map(|e| { - e.into_pyobject(py) - .map(BoundObject::into_any) - .map(BoundObject::into_bound) - .map_err(Into::into) - }); + let mut iter = iter.into_iter().map(|e| e.into_bound_py_any(py)); let list = crate::types::list::try_new_from_iter(py, &mut iter); list.map(Bound::into_any) } @@ -347,6 +352,54 @@ where } } +mod into_pyobject_ext { + pub trait Sealed {} + impl<'py, T> Sealed for T where T: super::IntoPyObject<'py> {} +} + +/// Convenience methods for common usages of [`IntoPyObject`]. Every type that implements +/// [`IntoPyObject`] also implements this trait. +/// +/// These methods: +/// - Drop type information from the output, returning a `PyAny` object. +/// - Always convert the `Error` type to `PyErr`, which may incur a performance penalty but it +/// more convenient in contexts where the `?` operator would produce a `PyErr` anyway. +pub trait IntoPyObjectExt<'py>: IntoPyObject<'py> + into_pyobject_ext::Sealed { + /// Converts `self` into an owned Python object, dropping type information. + #[inline] + fn into_bound_py_any(self, py: Python<'py>) -> PyResult> { + match self.into_pyobject(py) { + Ok(obj) => Ok(obj.into_any().into_bound()), + Err(err) => Err(err.into()), + } + } + + /// Converts `self` into an owned Python object, dropping type information and unbinding it + /// from the `'py` lifetime. + #[inline] + fn into_py_any(self, py: Python<'py>) -> PyResult> { + match self.into_pyobject(py) { + Ok(obj) => Ok(obj.into_any().unbind()), + Err(err) => Err(err.into()), + } + } + + /// Converts `self` into a Python object. + /// + /// This is equivalent to calling [`into_pyobject`][IntoPyObject::into_pyobject] followed + /// with `.map_err(Into::into)` to convert the error type to [`PyErr`]. This is helpful + /// for generic code which wants to make use of the `?` operator. + #[inline] + fn into_pyobject_or_pyerr(self, py: Python<'py>) -> PyResult { + match self.into_pyobject(py) { + Ok(obj) => Ok(obj), + Err(err) => Err(err.into()), + } + } +} + +impl<'py, T> IntoPyObjectExt<'py> for T where T: IntoPyObject<'py> {} + /// Extract a type from a Python object. /// /// diff --git a/src/conversions/either.rs b/src/conversions/either.rs index 1fdcd22d7e2..a514b1fde8d 100644 --- a/src/conversions/either.rs +++ b/src/conversions/either.rs @@ -47,8 +47,8 @@ #[cfg(feature = "experimental-inspect")] use crate::inspect::types::TypeInfo; use crate::{ - conversion::IntoPyObject, exceptions::PyTypeError, types::any::PyAnyMethods, Bound, - BoundObject, FromPyObject, PyAny, PyErr, PyObject, PyResult, Python, + exceptions::PyTypeError, types::any::PyAnyMethods, Bound, FromPyObject, IntoPyObject, + IntoPyObjectExt, PyAny, PyErr, PyObject, PyResult, Python, }; #[allow(deprecated)] use crate::{IntoPy, ToPyObject}; @@ -82,16 +82,8 @@ where fn into_pyobject(self, py: Python<'py>) -> Result { match self { - Either::Left(l) => l - .into_pyobject(py) - .map(BoundObject::into_any) - .map(BoundObject::into_bound) - .map_err(Into::into), - Either::Right(r) => r - .into_pyobject(py) - .map(BoundObject::into_any) - .map(BoundObject::into_bound) - .map_err(Into::into), + Either::Left(l) => l.into_bound_py_any(py), + Either::Right(r) => r.into_bound_py_any(py), } } } @@ -108,16 +100,8 @@ where fn into_pyobject(self, py: Python<'py>) -> Result { match self { - Either::Left(l) => l - .into_pyobject(py) - .map(BoundObject::into_any) - .map(BoundObject::into_bound) - .map_err(Into::into), - Either::Right(r) => r - .into_pyobject(py) - .map(BoundObject::into_any) - .map(BoundObject::into_bound) - .map_err(Into::into), + Either::Left(l) => l.into_bound_py_any(py), + Either::Right(r) => r.into_bound_py_any(py), } } } diff --git a/src/coroutine.rs b/src/coroutine.rs index aa4335e9d0b..671defb1770 100644 --- a/src/coroutine.rs +++ b/src/coroutine.rs @@ -15,7 +15,7 @@ use crate::{ exceptions::{PyAttributeError, PyRuntimeError, PyStopIteration}, panic::PanicException, types::{string::PyStringMethods, PyIterator, PyString}, - Bound, BoundObject, IntoPyObject, Py, PyAny, PyErr, PyObject, PyResult, Python, + Bound, IntoPyObject, IntoPyObjectExt, Py, PyAny, PyErr, PyObject, PyResult, Python, }; pub(crate) mod cancel; @@ -60,10 +60,7 @@ impl Coroutine { let wrap = async move { let obj = future.await.map_err(Into::into)?; // SAFETY: GIL is acquired when future is polled (see `Coroutine::poll`) - obj.into_pyobject(unsafe { Python::assume_gil_acquired() }) - .map(BoundObject::into_any) - .map(BoundObject::unbind) - .map_err(Into::into) + obj.into_py_any(unsafe { Python::assume_gil_acquired() }) }; Self { name: name.map(Bound::unbind), diff --git a/src/impl_/pyclass.rs b/src/impl_/pyclass.rs index 8e7e8cf844f..7bb61442ec5 100644 --- a/src/impl_/pyclass.rs +++ b/src/impl_/pyclass.rs @@ -1,5 +1,4 @@ use crate::{ - conversion::IntoPyObject, exceptions::{PyAttributeError, PyNotImplementedError, PyRuntimeError, PyValueError}, ffi, impl_::{ @@ -10,7 +9,8 @@ use crate::{ }, pycell::PyBorrowError, types::{any::PyAnyMethods, PyBool}, - Borrowed, BoundObject, Py, PyAny, PyClass, PyErr, PyRef, PyResult, PyTypeInfo, Python, + Borrowed, BoundObject, IntoPyObject, IntoPyObjectExt, Py, PyAny, PyClass, PyErr, PyRef, + PyResult, PyTypeInfo, Python, }; #[allow(deprecated)] use crate::{IntoPy, ToPyObject}; @@ -1532,10 +1532,7 @@ impl ConvertField, { - obj.into_pyobject(py) - .map(BoundObject::into_any) - .map(BoundObject::unbind) - .map_err(Into::into) + obj.into_py_any(py) } } @@ -1545,11 +1542,7 @@ impl ConvertField, { - obj.clone() - .into_pyobject(py) - .map(BoundObject::into_any) - .map(BoundObject::unbind) - .map_err(Into::into) + obj.clone().into_py_any(py) } } diff --git a/src/impl_/wrap.rs b/src/impl_/wrap.rs index 9381828245a..7b214219408 100644 --- a/src/impl_/wrap.rs +++ b/src/impl_/wrap.rs @@ -2,9 +2,7 @@ use std::{convert::Infallible, marker::PhantomData, ops::Deref}; #[allow(deprecated)] use crate::IntoPy; -use crate::{ - conversion::IntoPyObject, ffi, types::PyNone, Bound, BoundObject, PyObject, PyResult, Python, -}; +use crate::{ffi, types::PyNone, Bound, IntoPyObject, IntoPyObjectExt, PyObject, PyResult, Python}; /// Used to wrap values in `Option` for default arguments. pub trait SomeWrap { @@ -97,9 +95,7 @@ impl<'py, T: IntoPyObject<'py>, E> IntoPyObjectConverter> { where T: IntoPyObject<'py>, { - obj.and_then(|obj| obj.into_pyobject(py).map_err(Into::into)) - .map(BoundObject::into_any) - .map(BoundObject::unbind) + obj.and_then(|obj| obj.into_py_any(py)) } #[inline] @@ -107,8 +103,7 @@ impl<'py, T: IntoPyObject<'py>, E> IntoPyObjectConverter> { where T: IntoPyObject<'py>, { - obj.and_then(|obj| obj.into_pyobject(py).map_err(Into::into)) - .map(BoundObject::into_bound) + obj.and_then(|obj| obj.into_bound_py_any(py)) .map(Bound::into_ptr) } } diff --git a/src/instance.rs b/src/instance.rs index 14d2d11b5d7..840416116f3 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -1466,7 +1466,7 @@ impl Py { /// # Example: `intern!`ing the attribute name /// /// ``` - /// # use pyo3::{intern, pyfunction, types::PyModule, IntoPyObject, PyObject, Python, PyResult}; + /// # use pyo3::{intern, pyfunction, types::PyModule, IntoPyObjectExt, PyObject, Python, PyResult}; /// # /// #[pyfunction] /// fn set_answer(ob: PyObject, py: Python<'_>) -> PyResult<()> { @@ -1474,7 +1474,7 @@ impl Py { /// } /// # /// # Python::with_gil(|py| { - /// # let ob = PyModule::new(py, "empty").unwrap().into_pyobject(py).unwrap().into_any().unbind(); + /// # let ob = PyModule::new(py, "empty").unwrap().into_py_any(py).unwrap(); /// # set_answer(ob, py).unwrap(); /// # }); /// ``` diff --git a/src/lib.rs b/src/lib.rs index 265824adab1..46a2fd53d32 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -334,7 +334,7 @@ #![doc = concat!("[Features chapter of the guide]: https://pyo3.rs/v", env!("CARGO_PKG_VERSION"), "/features.html#features-reference \"Features Reference - PyO3 user guide\"")] //! [`Ungil`]: crate::marker::Ungil pub use crate::class::*; -pub use crate::conversion::{AsPyPointer, FromPyObject, IntoPyObject}; +pub use crate::conversion::{AsPyPointer, FromPyObject, IntoPyObject, IntoPyObjectExt}; #[allow(deprecated)] pub use crate::conversion::{IntoPy, ToPyObject}; pub use crate::err::{DowncastError, DowncastIntoError, PyErr, PyErrArguments, PyResult, ToPyErr}; diff --git a/src/types/any.rs b/src/types/any.rs index e620cf6d137..d060c187631 100644 --- a/src/types/any.rs +++ b/src/types/any.rs @@ -11,7 +11,7 @@ use crate::type_object::{PyTypeCheck, PyTypeInfo}; #[cfg(not(any(PyPy, GraalPy)))] use crate::types::PySuper; use crate::types::{PyDict, PyIterator, PyList, PyString, PyTuple, PyType}; -use crate::{err, ffi, Borrowed, BoundObject, Python}; +use crate::{err, ffi, Borrowed, BoundObject, IntoPyObjectExt, Python}; use std::cell::UnsafeCell; use std::cmp::Ordering; use std::os::raw::c_int; @@ -922,11 +922,7 @@ macro_rules! implement_binop { let py = self.py(); inner( self, - other - .into_pyobject(py) - .map_err(Into::into)? - .into_any() - .as_borrowed(), + other.into_pyobject_or_pyerr(py)?.into_any().as_borrowed(), ) } }; @@ -996,15 +992,8 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { let py = self.py(); inner( self, - attr_name - .into_pyobject(py) - .map_err(Into::into)? - .as_borrowed(), - value - .into_pyobject(py) - .map_err(Into::into)? - .into_any() - .as_borrowed(), + attr_name.into_pyobject_or_pyerr(py)?.as_borrowed(), + value.into_pyobject_or_pyerr(py)?.into_any().as_borrowed(), ) } @@ -1019,13 +1008,7 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { } let py = self.py(); - inner( - self, - attr_name - .into_pyobject(py) - .map_err(Into::into)? - .as_borrowed(), - ) + inner(self, attr_name.into_pyobject_or_pyerr(py)?.as_borrowed()) } fn compare(&self, other: O) -> PyResult @@ -1057,11 +1040,7 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { let py = self.py(); inner( self, - other - .into_pyobject(py) - .map_err(Into::into)? - .into_any() - .as_borrowed(), + other.into_pyobject_or_pyerr(py)?.into_any().as_borrowed(), ) } @@ -1083,11 +1062,7 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { let py = self.py(); inner( self, - other - .into_pyobject(py) - .map_err(Into::into)? - .into_any() - .as_borrowed(), + other.into_pyobject_or_pyerr(py)?.into_any().as_borrowed(), compare_op, ) } @@ -1198,11 +1173,7 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { let py = self.py(); inner( self, - other - .into_pyobject(py) - .map_err(Into::into)? - .into_any() - .as_borrowed(), + other.into_pyobject_or_pyerr(py)?.into_any().as_borrowed(), ) } @@ -1227,16 +1198,8 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { let py = self.py(); inner( self, - other - .into_pyobject(py) - .map_err(Into::into)? - .into_any() - .as_borrowed(), - modulus - .into_pyobject(py) - .map_err(Into::into)? - .into_any() - .as_borrowed(), + other.into_pyobject_or_pyerr(py)?.into_any().as_borrowed(), + modulus.into_pyobject_or_pyerr(py)?.into_any().as_borrowed(), ) } @@ -1264,11 +1227,7 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { } let py = self.py(); - inner( - self, - args.into_pyobject(py).map_err(Into::into)?.as_borrowed(), - kwargs, - ) + inner(self, args.into_pyobject_or_pyerr(py)?.as_borrowed(), kwargs) } #[inline] @@ -1304,7 +1263,7 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { N: IntoPyObject<'py, Target = PyString>, { let py = self.py(); - let name = name.into_pyobject(py).map_err(Into::into)?.into_bound(); + let name = name.into_pyobject_or_pyerr(py)?.into_bound(); unsafe { ffi::compat::PyObject_CallMethodNoArgs(self.as_ptr(), name.as_ptr()) .assume_owned_or_err(py) @@ -1354,10 +1313,7 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { let py = self.py(); inner( self, - key.into_pyobject(py) - .map_err(Into::into)? - .into_any() - .as_borrowed(), + key.into_pyobject_or_pyerr(py)?.into_any().as_borrowed(), ) } @@ -1379,15 +1335,8 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { let py = self.py(); inner( self, - key.into_pyobject(py) - .map_err(Into::into)? - .into_any() - .as_borrowed(), - value - .into_pyobject(py) - .map_err(Into::into)? - .into_any() - .as_borrowed(), + key.into_pyobject_or_pyerr(py)?.into_any().as_borrowed(), + value.into_pyobject_or_pyerr(py)?.into_any().as_borrowed(), ) } @@ -1404,10 +1353,7 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { let py = self.py(); inner( self, - key.into_pyobject(py) - .map_err(Into::into)? - .into_any() - .as_borrowed(), + key.into_pyobject_or_pyerr(py)?.into_any().as_borrowed(), ) } @@ -1574,11 +1520,7 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { let py = self.py(); inner( self, - value - .into_pyobject(py) - .map_err(Into::into)? - .into_any() - .as_borrowed(), + value.into_pyobject_or_pyerr(py)?.into_any().as_borrowed(), ) } diff --git a/src/types/dict.rs b/src/types/dict.rs index 129f32dc9e1..b3c8e37962b 100644 --- a/src/types/dict.rs +++ b/src/types/dict.rs @@ -4,7 +4,7 @@ use crate::ffi_ptr_ext::FfiPtrExt; use crate::instance::{Borrowed, Bound}; use crate::py_result_ext::PyResultExt; use crate::types::{PyAny, PyAnyMethods, PyList, PyMapping}; -use crate::{ffi, BoundObject, IntoPyObject, Python}; +use crate::{ffi, BoundObject, IntoPyObject, IntoPyObjectExt, Python}; /// Represents a Python `dict`. /// @@ -239,7 +239,7 @@ impl<'py> PyDictMethods<'py> for Bound<'py, PyDict> { where K: IntoPyObject<'py>, { - fn inner(dict: &Bound<'_, PyDict>, key: &Bound<'_, PyAny>) -> PyResult { + fn inner(dict: &Bound<'_, PyDict>, key: Borrowed<'_, '_, PyAny>) -> PyResult { match unsafe { ffi::PyDict_Contains(dict.as_ptr(), key.as_ptr()) } { 1 => Ok(true), 0 => Ok(false), @@ -250,10 +250,7 @@ impl<'py> PyDictMethods<'py> for Bound<'py, PyDict> { let py = self.py(); inner( self, - &key.into_pyobject(py) - .map_err(Into::into)? - .into_any() - .as_borrowed(), + key.into_pyobject_or_pyerr(py)?.into_any().as_borrowed(), ) } @@ -263,7 +260,7 @@ impl<'py> PyDictMethods<'py> for Bound<'py, PyDict> { { fn inner<'py>( dict: &Bound<'py, PyDict>, - key: &Bound<'_, PyAny>, + key: Borrowed<'_, '_, PyAny>, ) -> PyResult>> { let py = dict.py(); let mut result: *mut ffi::PyObject = std::ptr::null_mut(); @@ -283,10 +280,7 @@ impl<'py> PyDictMethods<'py> for Bound<'py, PyDict> { let py = self.py(); inner( self, - &key.into_pyobject(py) - .map_err(Into::into)? - .into_any() - .as_borrowed(), + key.into_pyobject_or_pyerr(py)?.into_any().as_borrowed(), ) } @@ -297,8 +291,8 @@ impl<'py> PyDictMethods<'py> for Bound<'py, PyDict> { { fn inner( dict: &Bound<'_, PyDict>, - key: &Bound<'_, PyAny>, - value: &Bound<'_, PyAny>, + key: Borrowed<'_, '_, PyAny>, + value: Borrowed<'_, '_, PyAny>, ) -> PyResult<()> { err::error_on_minusone(dict.py(), unsafe { ffi::PyDict_SetItem(dict.as_ptr(), key.as_ptr(), value.as_ptr()) @@ -308,15 +302,8 @@ impl<'py> PyDictMethods<'py> for Bound<'py, PyDict> { let py = self.py(); inner( self, - &key.into_pyobject(py) - .map_err(Into::into)? - .into_any() - .as_borrowed(), - &value - .into_pyobject(py) - .map_err(Into::into)? - .into_any() - .as_borrowed(), + key.into_pyobject_or_pyerr(py)?.into_any().as_borrowed(), + value.into_pyobject_or_pyerr(py)?.into_any().as_borrowed(), ) } @@ -324,7 +311,7 @@ impl<'py> PyDictMethods<'py> for Bound<'py, PyDict> { where K: IntoPyObject<'py>, { - fn inner(dict: &Bound<'_, PyDict>, key: &Bound<'_, PyAny>) -> PyResult<()> { + fn inner(dict: &Bound<'_, PyDict>, key: Borrowed<'_, '_, PyAny>) -> PyResult<()> { err::error_on_minusone(dict.py(), unsafe { ffi::PyDict_DelItem(dict.as_ptr(), key.as_ptr()) }) @@ -333,10 +320,7 @@ impl<'py> PyDictMethods<'py> for Bound<'py, PyDict> { let py = self.py(); inner( self, - &key.into_pyobject(py) - .map_err(Into::into)? - .into_any() - .as_borrowed(), + key.into_pyobject_or_pyerr(py)?.into_any().as_borrowed(), ) } diff --git a/src/types/frozenset.rs b/src/types/frozenset.rs index 3c5a62a01d8..954c49b5902 100644 --- a/src/types/frozenset.rs +++ b/src/types/frozenset.rs @@ -1,4 +1,3 @@ -use crate::conversion::IntoPyObject; use crate::types::PyIterator; use crate::{ err::{self, PyErr, PyResult}, @@ -8,7 +7,7 @@ use crate::{ types::any::PyAnyMethods, Bound, PyAny, Python, }; -use crate::{Borrowed, BoundObject}; +use crate::{Borrowed, BoundObject, IntoPyObject, IntoPyObjectExt}; use std::ptr; /// Allows building a Python `frozenset` one item at a time @@ -181,10 +180,7 @@ impl<'py> PyFrozenSetMethods<'py> for Bound<'py, PyFrozenSet> { let py = self.py(); inner( self, - key.into_pyobject(py) - .map_err(Into::into)? - .into_any() - .as_borrowed(), + key.into_pyobject_or_pyerr(py)?.into_any().as_borrowed(), ) } @@ -266,7 +262,7 @@ where let ptr = set.as_ptr(); for e in elements { - let obj = e.into_pyobject(py).map_err(Into::into)?; + let obj = e.into_pyobject_or_pyerr(py)?; err::error_on_minusone(py, unsafe { ffi::PySet_Add(ptr, obj.as_ptr()) })?; } diff --git a/src/types/list.rs b/src/types/list.rs index f00c194739f..af2b557cba9 100644 --- a/src/types/list.rs +++ b/src/types/list.rs @@ -5,7 +5,9 @@ use crate::ffi::{self, Py_ssize_t}; use crate::ffi_ptr_ext::FfiPtrExt; use crate::internal_tricks::get_ssize_index; use crate::types::{PySequence, PyTuple}; -use crate::{Borrowed, Bound, BoundObject, IntoPyObject, PyAny, PyErr, PyObject, Python}; +use crate::{ + Borrowed, Bound, BoundObject, IntoPyObject, IntoPyObjectExt, PyAny, PyErr, PyObject, Python, +}; use crate::types::any::PyAnyMethods; use crate::types::sequence::PySequenceMethods; @@ -104,12 +106,7 @@ impl PyList { T: IntoPyObject<'py>, U: ExactSizeIterator, { - let iter = elements.into_iter().map(|e| { - e.into_pyobject(py) - .map(BoundObject::into_any) - .map(BoundObject::into_bound) - .map_err(Into::into) - }); + let iter = elements.into_iter().map(|e| e.into_bound_py_any(py)); try_new_from_iter(py, iter) } @@ -339,14 +336,7 @@ impl<'py> PyListMethods<'py> for Bound<'py, PyList> { } let py = self.py(); - inner( - self, - index, - item.into_pyobject(py) - .map_err(Into::into)? - .into_any() - .into_bound(), - ) + inner(self, index, item.into_bound_py_any(py)?) } /// Deletes the `index`th element of self. @@ -394,10 +384,7 @@ impl<'py> PyListMethods<'py> for Bound<'py, PyList> { let py = self.py(); inner( self, - item.into_pyobject(py) - .map_err(Into::into)? - .into_any() - .as_borrowed(), + item.into_pyobject_or_pyerr(py)?.into_any().as_borrowed(), ) } @@ -422,10 +409,7 @@ impl<'py> PyListMethods<'py> for Bound<'py, PyList> { inner( self, index, - item.into_pyobject(py) - .map_err(Into::into)? - .into_any() - .as_borrowed(), + item.into_pyobject_or_pyerr(py)?.into_any().as_borrowed(), ) } diff --git a/src/types/module.rs b/src/types/module.rs index d3e59c85198..fd7299cb084 100644 --- a/src/types/module.rs +++ b/src/types/module.rs @@ -1,4 +1,3 @@ -use crate::conversion::IntoPyObject; use crate::err::{PyErr, PyResult}; use crate::ffi_ptr_ext::FfiPtrExt; use crate::impl_::callback::IntoPyCallbackOutput; @@ -7,7 +6,10 @@ use crate::pyclass::PyClass; use crate::types::{ any::PyAnyMethods, list::PyListMethods, PyAny, PyCFunction, PyDict, PyList, PyString, }; -use crate::{exceptions, ffi, Borrowed, Bound, BoundObject, Py, PyObject, Python}; +use crate::{ + exceptions, ffi, Borrowed, Bound, BoundObject, IntoPyObject, IntoPyObjectExt, Py, PyObject, + Python, +}; use std::ffi::{CStr, CString}; #[cfg(all(not(Py_LIMITED_API), Py_GIL_DISABLED))] use std::os::raw::c_int; @@ -89,7 +91,7 @@ impl PyModule { where N: IntoPyObject<'py, Target = PyString>, { - let name = name.into_pyobject(py).map_err(Into::into)?; + let name = name.into_pyobject_or_pyerr(py)?; unsafe { ffi::PyImport_Import(name.as_ptr()) .assume_owned_or_err(py) @@ -508,12 +510,8 @@ impl<'py> PyModuleMethods<'py> for Bound<'py, PyModule> { let py = self.py(); inner( self, - name.into_pyobject(py).map_err(Into::into)?.as_borrowed(), - value - .into_pyobject(py) - .map_err(Into::into)? - .into_any() - .as_borrowed(), + name.into_pyobject_or_pyerr(py)?.as_borrowed(), + value.into_pyobject_or_pyerr(py)?.into_any().as_borrowed(), ) } diff --git a/src/types/sequence.rs b/src/types/sequence.rs index 0801704f700..bc2643dcf8e 100644 --- a/src/types/sequence.rs +++ b/src/types/sequence.rs @@ -9,7 +9,10 @@ use crate::py_result_ext::PyResultExt; use crate::sync::GILOnceCell; use crate::type_object::PyTypeInfo; use crate::types::{any::PyAnyMethods, PyAny, PyList, PyString, PyTuple, PyType}; -use crate::{ffi, Borrowed, BoundObject, FromPyObject, IntoPyObject, Py, PyTypeCheck, Python}; +use crate::{ + ffi, Borrowed, BoundObject, FromPyObject, IntoPyObject, IntoPyObjectExt, Py, PyTypeCheck, + Python, +}; /// Represents a reference to a Python object supporting the sequence protocol. /// @@ -221,10 +224,7 @@ impl<'py> PySequenceMethods<'py> for Bound<'py, PySequence> { inner( self, i, - item.into_pyobject(py) - .map_err(Into::into)? - .into_any() - .as_borrowed(), + item.into_pyobject_or_pyerr(py)?.into_any().as_borrowed(), ) } @@ -269,11 +269,7 @@ impl<'py> PySequenceMethods<'py> for Bound<'py, PySequence> { let py = self.py(); inner( self, - value - .into_pyobject(py) - .map_err(Into::into)? - .into_any() - .as_borrowed(), + value.into_pyobject_or_pyerr(py)?.into_any().as_borrowed(), ) } @@ -294,11 +290,7 @@ impl<'py> PySequenceMethods<'py> for Bound<'py, PySequence> { let py = self.py(); inner( self, - value - .into_pyobject(py) - .map_err(Into::into)? - .into_any() - .as_borrowed(), + value.into_pyobject_or_pyerr(py)?.into_any().as_borrowed(), ) } @@ -316,11 +308,7 @@ impl<'py> PySequenceMethods<'py> for Bound<'py, PySequence> { let py = self.py(); inner( self, - value - .into_pyobject(py) - .map_err(Into::into)? - .into_any() - .as_borrowed(), + value.into_pyobject_or_pyerr(py)?.into_any().as_borrowed(), ) } diff --git a/src/types/set.rs b/src/types/set.rs index e7c24f5b1ea..d5e39ebc83d 100644 --- a/src/types/set.rs +++ b/src/types/set.rs @@ -1,4 +1,3 @@ -use crate::conversion::IntoPyObject; use crate::types::PyIterator; #[allow(deprecated)] use crate::ToPyObject; @@ -9,7 +8,7 @@ use crate::{ py_result_ext::PyResultExt, types::any::PyAnyMethods, }; -use crate::{ffi, Borrowed, BoundObject, PyAny, Python}; +use crate::{ffi, Borrowed, BoundObject, IntoPyObject, IntoPyObjectExt, PyAny, Python}; use std::ptr; /// Represents a Python `set`. @@ -161,10 +160,7 @@ impl<'py> PySetMethods<'py> for Bound<'py, PySet> { let py = self.py(); inner( self, - key.into_pyobject(py) - .map_err(Into::into)? - .into_any() - .as_borrowed(), + key.into_pyobject_or_pyerr(py)?.into_any().as_borrowed(), ) } @@ -183,10 +179,7 @@ impl<'py> PySetMethods<'py> for Bound<'py, PySet> { let py = self.py(); inner( self, - key.into_pyobject(py) - .map_err(Into::into)? - .into_any() - .as_borrowed(), + key.into_pyobject_or_pyerr(py)?.into_any().as_borrowed(), ) } @@ -203,10 +196,7 @@ impl<'py> PySetMethods<'py> for Bound<'py, PySet> { let py = self.py(); inner( self, - key.into_pyobject(py) - .map_err(Into::into)? - .into_any() - .as_borrowed(), + key.into_pyobject_or_pyerr(py)?.into_any().as_borrowed(), ) } @@ -316,7 +306,7 @@ where let ptr = set.as_ptr(); elements.into_iter().try_for_each(|element| { - let obj = element.into_pyobject(py).map_err(Into::into)?; + let obj = element.into_pyobject_or_pyerr(py)?; err::error_on_minusone(py, unsafe { ffi::PySet_Add(ptr, obj.as_ptr()) }) })?; diff --git a/src/types/tuple.rs b/src/types/tuple.rs index 3a1f92815c2..216a376d833 100644 --- a/src/types/tuple.rs +++ b/src/types/tuple.rs @@ -1,6 +1,5 @@ use std::iter::FusedIterator; -use crate::conversion::IntoPyObject; use crate::ffi::{self, Py_ssize_t}; use crate::ffi_ptr_ext::FfiPtrExt; #[cfg(feature = "experimental-inspect")] @@ -9,7 +8,8 @@ use crate::instance::Borrowed; use crate::internal_tricks::get_ssize_index; use crate::types::{any::PyAnyMethods, sequence::PySequenceMethods, PyList, PySequence}; use crate::{ - exceptions, Bound, BoundObject, FromPyObject, Py, PyAny, PyErr, PyObject, PyResult, Python, + exceptions, Bound, FromPyObject, IntoPyObject, IntoPyObjectExt, Py, PyAny, PyErr, PyObject, + PyResult, Python, }; #[allow(deprecated)] use crate::{IntoPy, ToPyObject}; @@ -99,12 +99,7 @@ impl PyTuple { T: IntoPyObject<'py>, U: ExactSizeIterator, { - let elements = elements.into_iter().map(|e| { - e.into_pyobject(py) - .map(BoundObject::into_any) - .map(BoundObject::into_bound) - .map_err(Into::into) - }); + let elements = elements.into_iter().map(|e| e.into_bound_py_any(py)); try_new_from_iter(py, elements) } @@ -523,14 +518,14 @@ macro_rules! tuple_conversion ({$length:expr,$(($refN:ident, $n:tt, $T:ident)),+ #[allow(deprecated)] impl <$($T: ToPyObject),+> ToPyObject for ($($T,)+) { fn to_object(&self, py: Python<'_>) -> PyObject { - array_into_tuple(py, [$(self.$n.to_object(py)),+]).into() + array_into_tuple(py, [$(self.$n.to_object(py).into_bound(py)),+]).into() } } #[allow(deprecated)] impl <$($T: IntoPy),+> IntoPy for ($($T,)+) { fn into_py(self, py: Python<'_>) -> PyObject { - array_into_tuple(py, [$(self.$n.into_py(py)),+]).into() + array_into_tuple(py, [$(self.$n.into_py(py).into_bound(py)),+]).into() } } @@ -543,7 +538,7 @@ macro_rules! tuple_conversion ({$length:expr,$(($refN:ident, $n:tt, $T:ident)),+ type Error = PyErr; fn into_pyobject(self, py: Python<'py>) -> Result { - Ok(array_into_tuple(py, [$(self.$n.into_pyobject(py).map_err(Into::into)?.into_any().unbind()),+]).into_bound(py)) + Ok(array_into_tuple(py, [$(self.$n.into_bound_py_any(py)?),+])) } #[cfg(feature = "experimental-inspect")] @@ -562,7 +557,7 @@ macro_rules! tuple_conversion ({$length:expr,$(($refN:ident, $n:tt, $T:ident)),+ type Error = PyErr; fn into_pyobject(self, py: Python<'py>) -> Result { - Ok(array_into_tuple(py, [$(self.$n.into_pyobject(py).map_err(Into::into)?.into_any().unbind()),+]).into_bound(py)) + Ok(array_into_tuple(py, [$(self.$n.into_bound_py_any(py)?),+])) } #[cfg(feature = "experimental-inspect")] @@ -574,7 +569,7 @@ macro_rules! tuple_conversion ({$length:expr,$(($refN:ident, $n:tt, $T:ident)),+ #[allow(deprecated)] impl <$($T: IntoPy),+> IntoPy> for ($($T,)+) { fn into_py(self, py: Python<'_>) -> Py { - array_into_tuple(py, [$(self.$n.into_py(py)),+]) + array_into_tuple(py, [$(self.$n.into_py(py).into_bound(py)),+]).unbind() } } @@ -600,10 +595,13 @@ macro_rules! tuple_conversion ({$length:expr,$(($refN:ident, $n:tt, $T:ident)),+ } }); -fn array_into_tuple(py: Python<'_>, array: [PyObject; N]) -> Py { +fn array_into_tuple<'py, const N: usize>( + py: Python<'py>, + array: [Bound<'py, PyAny>; N], +) -> Bound<'py, PyTuple> { unsafe { let ptr = ffi::PyTuple_New(N.try_into().expect("0 < N <= 12")); - let tup = Py::from_owned_ptr(py, ptr); + let tup = ptr.assume_owned(py).downcast_into_unchecked(); for (index, obj) in array.into_iter().enumerate() { #[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))] ffi::PyTuple_SET_ITEM(ptr, index as ffi::Py_ssize_t, obj.into_ptr()); diff --git a/src/types/weakref/proxy.rs b/src/types/weakref/proxy.rs index 6b20a29b8c2..5334f0341f1 100644 --- a/src/types/weakref/proxy.rs +++ b/src/types/weakref/proxy.rs @@ -3,7 +3,7 @@ use crate::ffi_ptr_ext::FfiPtrExt; use crate::py_result_ext::PyResultExt; use crate::type_object::PyTypeCheck; use crate::types::any::PyAny; -use crate::{ffi, Bound, BoundObject, IntoPyObject}; +use crate::{ffi, Borrowed, Bound, BoundObject, IntoPyObject, IntoPyObjectExt}; use super::PyWeakrefMethods; @@ -152,7 +152,7 @@ impl PyWeakrefProxy { { fn inner<'py>( object: &Bound<'py, PyAny>, - callback: Bound<'py, PyAny>, + callback: Borrowed<'_, 'py, PyAny>, ) -> PyResult> { unsafe { Bound::from_owned_ptr_or_err( @@ -167,10 +167,9 @@ impl PyWeakrefProxy { inner( object, callback - .into_pyobject(py) - .map(BoundObject::into_any) - .map(BoundObject::into_bound) - .map_err(Into::into)?, + .into_pyobject_or_pyerr(py)? + .into_any() + .as_borrowed(), ) } diff --git a/src/types/weakref/reference.rs b/src/types/weakref/reference.rs index dc7ea4a272a..edabb6da935 100644 --- a/src/types/weakref/reference.rs +++ b/src/types/weakref/reference.rs @@ -2,7 +2,7 @@ use crate::err::PyResult; use crate::ffi_ptr_ext::FfiPtrExt; use crate::py_result_ext::PyResultExt; use crate::types::any::PyAny; -use crate::{ffi, Bound, BoundObject, IntoPyObject}; +use crate::{ffi, Borrowed, Bound, BoundObject, IntoPyObject, IntoPyObjectExt}; #[cfg(any(PyPy, GraalPy, Py_LIMITED_API))] use crate::type_object::PyTypeCheck; @@ -161,7 +161,7 @@ impl PyWeakrefReference { { fn inner<'py>( object: &Bound<'py, PyAny>, - callback: Bound<'py, PyAny>, + callback: Borrowed<'_, 'py, PyAny>, ) -> PyResult> { unsafe { Bound::from_owned_ptr_or_err( @@ -176,10 +176,9 @@ impl PyWeakrefReference { inner( object, callback - .into_pyobject(py) - .map(BoundObject::into_any) - .map(BoundObject::into_bound) - .map_err(Into::into)?, + .into_pyobject_or_pyerr(py)? + .into_any() + .as_borrowed(), ) } From 06d2affe0897c893498a262242b77900dc366cd6 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Wed, 20 Nov 2024 08:28:25 +0000 Subject: [PATCH 382/495] docs: remove outdated `#[pyclass(text_signature = "...")]` option (#4721) --- guide/pyclass-parameters.md | 1 - 1 file changed, 1 deletion(-) diff --git a/guide/pyclass-parameters.md b/guide/pyclass-parameters.md index 863f447080e..7ebca2ec821 100644 --- a/guide/pyclass-parameters.md +++ b/guide/pyclass-parameters.md @@ -21,7 +21,6 @@ | `set_all` | Generates setters for all fields of the pyclass. | | `str` | Implements `__str__` using the `Display` implementation of the underlying Rust datatype or by passing an optional format string `str=""`. *Note: The optional format string is only allowed for structs. `name` and `rename_all` are incompatible with the optional format string. Additional details can be found in the discussion on this [PR](https://github.com/PyO3/pyo3/pull/4233).* | | `subclass` | Allows other Python classes and `#[pyclass]` to inherit from this class. Enums cannot be subclassed. | -| `text_signature = "(arg1, arg2, ...)"` | Sets the text signature for the Python class' `__new__` method. | | `unsendable` | Required if your struct is not [`Send`][params-3]. Rather than using `unsendable`, consider implementing your struct in a thread-safe way by e.g. substituting [`Rc`][params-4] with [`Arc`][params-5]. By using `unsendable`, your class will panic when accessed by another thread. Also note the Python's GC is multi-threaded and while unsendable classes will not be traversed on foreign threads to avoid UB, this can lead to memory leaks. | | `weakref` | Allows this class to be [weakly referenceable][params-6]. | From 9681b54356f1baa4f3b3fee4fbcec5e324365616 Mon Sep 17 00:00:00 2001 From: Lily Foote Date: Sat, 23 Nov 2024 21:54:57 +0000 Subject: [PATCH 383/495] Fix ambiguous associated item error (#4725) * Add test for ambiguous associated item The `#[pyclass]` macro implements `IntoPyObject` for the annotated enum. When the enum has any of `Error`, `Output` or `Target` as members, this clashes with the associated types of `IntoPyObject`. This also happens when deriving `IntoPyObject` directly. * Fix #4723: ambiguous associated item in #[pyclass] This uses the fix described in https://github.com/rust-lang/rust/issues/57644 Also apply fix to `#[derive(IntoPyObject)]`. --- newsfragments/4725.fixed.md | 1 + pyo3-macros-backend/src/intopyobject.rs | 7 +++++-- pyo3-macros-backend/src/pyclass.rs | 14 ++++++++++---- tests/test_compile_error.rs | 1 + tests/ui/ambiguous_associated_items.rs | 25 +++++++++++++++++++++++++ 5 files changed, 42 insertions(+), 6 deletions(-) create mode 100644 newsfragments/4725.fixed.md create mode 100644 tests/ui/ambiguous_associated_items.rs diff --git a/newsfragments/4725.fixed.md b/newsfragments/4725.fixed.md new file mode 100644 index 00000000000..9ee069c5eb4 --- /dev/null +++ b/newsfragments/4725.fixed.md @@ -0,0 +1 @@ +Fix `ambiguous_associated_items` lint error in `#[pyclass]` and `#[derive(IntoPyObject)]` macros. diff --git a/pyo3-macros-backend/src/intopyobject.rs b/pyo3-macros-backend/src/intopyobject.rs index 4a46c07418f..a60a5486cb8 100644 --- a/pyo3-macros-backend/src/intopyobject.rs +++ b/pyo3-macros-backend/src/intopyobject.rs @@ -512,7 +512,7 @@ impl<'a> Enum<'a> { IntoPyObjectImpl { types: IntoPyObjectTypes::Opaque { target: quote!(#pyo3_path::types::PyAny), - output: quote!(#pyo3_path::Bound<'py, Self::Target>), + output: quote!(#pyo3_path::Bound<'py, >::Target>), error: quote!(#pyo3_path::PyErr), }, body: quote! { @@ -617,7 +617,10 @@ pub fn build_derive_into_pyobject(tokens: &DeriveInput) -> Resu type Output = #output; type Error = #error; - fn into_pyobject(self, py: #pyo3_path::Python<#lt_param>) -> ::std::result::Result { + fn into_pyobject(self, py: #pyo3_path::Python<#lt_param>) -> ::std::result::Result< + ::Output, + ::Error, + > { #body } } diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index 87d02c6f878..93596611f18 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -1072,10 +1072,13 @@ fn impl_complex_enum( quote! { impl<'py> #pyo3_path::conversion::IntoPyObject<'py> for #cls { type Target = Self; - type Output = #pyo3_path::Bound<'py, Self::Target>; + type Output = #pyo3_path::Bound<'py, >::Target>; type Error = #pyo3_path::PyErr; - fn into_pyobject(self, py: #pyo3_path::Python<'py>) -> ::std::result::Result { + fn into_pyobject(self, py: #pyo3_path::Python<'py>) -> ::std::result::Result< + ::Output, + ::Error, + > { match self { #(#match_arms)* } @@ -2161,10 +2164,13 @@ impl<'a> PyClassImplsBuilder<'a> { impl<'py> #pyo3_path::conversion::IntoPyObject<'py> for #cls { type Target = Self; - type Output = #pyo3_path::Bound<'py, Self::Target>; + type Output = #pyo3_path::Bound<'py, >::Target>; type Error = #pyo3_path::PyErr; - fn into_pyobject(self, py: #pyo3_path::Python<'py>) -> ::std::result::Result { + fn into_pyobject(self, py: #pyo3_path::Python<'py>) -> ::std::result::Result< + ::Output, + ::Error, + > { #pyo3_path::Bound::new(py, self) } } diff --git a/tests/test_compile_error.rs b/tests/test_compile_error.rs index b6cf5065371..e4e80e90263 100644 --- a/tests/test_compile_error.rs +++ b/tests/test_compile_error.rs @@ -67,4 +67,5 @@ fn test_compile_errors() { t.compile_fail("tests/ui/duplicate_pymodule_submodule.rs"); #[cfg(all(not(Py_LIMITED_API), Py_3_11))] t.compile_fail("tests/ui/invalid_base_class.rs"); + t.pass("tests/ui/ambiguous_associated_items.rs"); } diff --git a/tests/ui/ambiguous_associated_items.rs b/tests/ui/ambiguous_associated_items.rs new file mode 100644 index 00000000000..f553ba1f33f --- /dev/null +++ b/tests/ui/ambiguous_associated_items.rs @@ -0,0 +1,25 @@ +use pyo3::prelude::*; + +#[pyclass(eq)] +#[derive(PartialEq)] +pub enum SimpleItems { + Error, + Output, + Target, +} + +#[pyclass] +pub enum ComplexItems { + Error(PyObject), + Output(PyObject), + Target(PyObject), +} + +#[derive(IntoPyObject)] +enum DeriveItems { + Error(PyObject), + Output(PyObject), + Target(PyObject), +} + +fn main() {} From 0fb3623d1e83467f2553b3e83d411a2849e443a5 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Sat, 23 Nov 2024 16:30:07 -0700 Subject: [PATCH 384/495] Free-threaded build config fixes (#4719) * update build config logic and library name generation * fix free-threaded windows clippy with --features=abi3 * use constant * add release note * apply my self-review comments * use ensure and error handling instead of panicking * skip abi3 fixup on free-threaded build * don't support PYO3_USE_ABI3_FORWARD_COMPATIBILITY on free-threaded build * don't panic in pyo3-ffi in abi3 check * document lack of limited API support * add is_free_threaded() method to InterpreterConfig * implement David's code review suggestions * remove unused imports --- guide/src/free-threading.md | 28 +++- newsfragments/4719.fixed.md | 2 + pyo3-build-config/src/impl_.rs | 259 +++++++++++++++++++++++++-------- pyo3-ffi/build.rs | 35 +++-- 4 files changed, 245 insertions(+), 79 deletions(-) create mode 100644 newsfragments/4719.fixed.md diff --git a/guide/src/free-threading.md b/guide/src/free-threading.md index d867a707795..f212cb0b9a9 100644 --- a/guide/src/free-threading.md +++ b/guide/src/free-threading.md @@ -108,6 +108,15 @@ using single-phase initialization and the [`sequential`](https://github.com/PyO3/pyo3/tree/main/pyo3-ffi/examples/sequential) example for modules using multi-phase initialization. +If you would like to use conditional compilation to trigger different code paths +under the free-threaded build, you can use the `Py_GIL_DISABLED` attribute once +you have configured your crate to generate the necessary build configuration +data. See [the guide +section](./building-and-distribution/multiple-python-versions.md) for more +details about supporting multiple different Python versions, including the +free-threaded build. + + ## Special considerations for the free-threaded build The free-threaded interpreter does not have a GIL, and this can make interacting @@ -234,7 +243,24 @@ needed. For now you should explicitly add locking, possibly using conditional compilation or using the critical section API to avoid creating deadlocks with the GIL. -## Thread-safe single initialization +### Cannot build extensions using the limited API + +The free-threaded build uses a completely new ABI and there is not yet an +equivalent to the limited API for the free-threaded ABI. That means if your +crate depends on PyO3 using the `abi3` feature or an an `abi3-pyxx` feature, +PyO3 will print a warning and ignore that setting when building extensions using +the free-threaded interpreter. + +This means that if your package makes use of the ABI forward compatibility +provided by the limited API to uploads only one wheel for each release of your +package, you will need to update and tooling or instructions to also upload a +version-specific free-threaded wheel. + +See [the guide section](./building-and-distribution/multiple-python-versions.md) +for more details about supporting multiple different Python versions, including +the free-threaded build. + +### Thread-safe single initialization Until version 0.23, PyO3 provided only [`GILOnceCell`] to enable deadlock-free single initialization of data in contexts that might execute arbitrary Python diff --git a/newsfragments/4719.fixed.md b/newsfragments/4719.fixed.md new file mode 100644 index 00000000000..08cb1c8268e --- /dev/null +++ b/newsfragments/4719.fixed.md @@ -0,0 +1,2 @@ +* Fixed an issue that prevented building free-threaded extensions for crates + that request a specific minimum limited API version. diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index 6d2326429d2..4e5d3c10656 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -176,7 +176,7 @@ impl InterpreterConfig { } // If Py_GIL_DISABLED is set, do not build with limited API support - if self.abi3 && !self.build_flags.0.contains(&BuildFlag::Py_GIL_DISABLED) { + if self.abi3 && !self.is_free_threaded() { out.push("cargo:rustc-cfg=Py_LIMITED_API".to_owned()); } @@ -309,14 +309,14 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) // `_d.cp312-win_amd64.pyd` for 3.12 debug build map["ext_suffix"].starts_with("_d."), gil_disabled, - ) + )? } else { default_lib_name_unix( version, implementation, map.get("ld_version").map(String::as_str), gil_disabled, - ) + )? }; let lib_dir = if cfg!(windows) { @@ -394,7 +394,7 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) implementation, sysconfigdata.get_value("LDVERSION"), gil_disabled, - )); + )?); let pointer_width = parse_key!(sysconfigdata, "SIZEOF_VOID_P") .map(|bytes_width: u32| bytes_width * 8) .ok(); @@ -660,10 +660,18 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) ) } - /// Lowers the configured version to the abi3 version, if set. + pub fn is_free_threaded(&self) -> bool { + self.build_flags.0.contains(&BuildFlag::Py_GIL_DISABLED) + } + + /// Updates configured ABI to build for to the requested abi3 version + /// This is a no-op for platforms where abi3 is not supported fn fixup_for_abi3_version(&mut self, abi3_version: Option) -> Result<()> { - // PyPy doesn't support abi3; don't adjust the version - if self.implementation.is_pypy() || self.implementation.is_graalpy() { + // PyPy, GraalPy, and the free-threaded build don't support abi3; don't adjust the version + if self.implementation.is_pypy() + || self.implementation.is_graalpy() + || self.is_free_threaded() + { return Ok(()); } @@ -691,6 +699,14 @@ pub struct PythonVersion { } impl PythonVersion { + pub const PY313: Self = PythonVersion { + major: 3, + minor: 13, + }; + const PY310: Self = PythonVersion { + major: 3, + minor: 10, + }; const PY37: Self = PythonVersion { major: 3, minor: 7 }; } @@ -1536,7 +1552,7 @@ fn default_cross_compile(cross_compile_config: &CrossCompileConfig) -> Result InterpreterConfig { +fn default_abi3_config(host: &Triple, version: PythonVersion) -> Result { // FIXME: PyPy & GraalPy do not support the Stable ABI. let implementation = PythonImplementation::CPython; let abi3 = true; @@ -1549,12 +1565,12 @@ fn default_abi3_config(host: &Triple, version: PythonVersion) -> InterpreterConf false, false, false, - )) + )?) } else { None }; - InterpreterConfig { + Ok(InterpreterConfig { implementation, version, shared: true, @@ -1566,7 +1582,7 @@ fn default_abi3_config(host: &Triple, version: PythonVersion) -> InterpreterConf build_flags: BuildFlags::default(), suppress_build_script_link_lines: false, extra_build_script_lines: vec![], - } + }) } /// Detects the cross compilation target interpreter configuration from all @@ -1606,11 +1622,9 @@ fn load_cross_compile_config( Ok(config) } -// Link against python3.lib for the stable ABI on Windows. -// See https://www.python.org/dev/peps/pep-0384/#linkage -// -// This contains only the limited ABI symbols. +// These contains only the limited ABI symbols. const WINDOWS_ABI3_LIB_NAME: &str = "python3"; +const WINDOWS_ABI3_DEBUG_LIB_NAME: &str = "python3_d"; fn default_lib_name_for_target( version: PythonVersion, @@ -1619,16 +1633,9 @@ fn default_lib_name_for_target( target: &Triple, ) -> Option { if target.operating_system == OperatingSystem::Windows { - Some(default_lib_name_windows( - version, - implementation, - abi3, - false, - false, - false, - )) + Some(default_lib_name_windows(version, implementation, abi3, false, false, false).unwrap()) } else if is_linking_libpython_for_target(target) { - Some(default_lib_name_unix(version, implementation, None, false)) + Some(default_lib_name_unix(version, implementation, None, false).unwrap()) } else { None } @@ -1641,27 +1648,35 @@ fn default_lib_name_windows( mingw: bool, debug: bool, gil_disabled: bool, -) -> String { - if debug { +) -> Result { + if debug && version < PythonVersion::PY310 { // CPython bug: linking against python3_d.dll raises error // https://github.com/python/cpython/issues/101614 - if gil_disabled { - format!("python{}{}t_d", version.major, version.minor) + Ok(format!("python{}{}_d", version.major, version.minor)) + } else if abi3 && !(implementation.is_pypy() || implementation.is_graalpy()) { + if debug { + Ok(WINDOWS_ABI3_DEBUG_LIB_NAME.to_owned()) } else { - format!("python{}{}_d", version.major, version.minor) + Ok(WINDOWS_ABI3_LIB_NAME.to_owned()) } - } else if abi3 && !(implementation.is_pypy() || implementation.is_graalpy()) { - WINDOWS_ABI3_LIB_NAME.to_owned() } else if mingw { - if gil_disabled { - panic!("MinGW free-threaded builds are not currently tested or supported") - } + ensure!( + !gil_disabled, + "MinGW free-threaded builds are not currently tested or supported" + ); // https://packages.msys2.org/base/mingw-w64-python - format!("python{}.{}", version.major, version.minor) + Ok(format!("python{}.{}", version.major, version.minor)) } else if gil_disabled { - format!("python{}{}t", version.major, version.minor) + ensure!(version >= PythonVersion::PY313, "Cannot compile C extensions for the free-threaded build on Python versions earlier than 3.13, found {}.{}", version.major, version.minor); + if debug { + Ok(format!("python{}{}t_d", version.major, version.minor)) + } else { + Ok(format!("python{}{}t", version.major, version.minor)) + } + } else if debug { + Ok(format!("python{}{}_d", version.major, version.minor)) } else { - format!("python{}{}", version.major, version.minor) + Ok(format!("python{}{}", version.major, version.minor)) } } @@ -1670,30 +1685,31 @@ fn default_lib_name_unix( implementation: PythonImplementation, ld_version: Option<&str>, gil_disabled: bool, -) -> String { +) -> Result { match implementation { PythonImplementation::CPython => match ld_version { - Some(ld_version) => format!("python{}", ld_version), + Some(ld_version) => Ok(format!("python{}", ld_version)), None => { if version > PythonVersion::PY37 { // PEP 3149 ABI version tags are finally gone if gil_disabled { - format!("python{}.{}t", version.major, version.minor) + ensure!(version >= PythonVersion::PY313, "Cannot compile C extensions for the free-threaded build on Python versions earlier than 3.13, found {}.{}", version.major, version.minor); + Ok(format!("python{}.{}t", version.major, version.minor)) } else { - format!("python{}.{}", version.major, version.minor) + Ok(format!("python{}.{}", version.major, version.minor)) } } else { // Work around https://bugs.python.org/issue36707 - format!("python{}.{}m", version.major, version.minor) + Ok(format!("python{}.{}m", version.major, version.minor)) } } }, PythonImplementation::PyPy => match ld_version { - Some(ld_version) => format!("pypy{}-c", ld_version), - None => format!("pypy{}.{}-c", version.major, version.minor), + Some(ld_version) => Ok(format!("pypy{}-c", ld_version)), + None => Ok(format!("pypy{}.{}-c", version.major, version.minor)), }, - PythonImplementation::GraalPy => "python-native".to_string(), + PythonImplementation::GraalPy => Ok("python-native".to_string()), } } @@ -1863,7 +1879,7 @@ pub fn make_interpreter_config() -> Result { ); }; - let mut interpreter_config = default_abi3_config(&host, abi3_version.unwrap()); + let mut interpreter_config = default_abi3_config(&host, abi3_version.unwrap())?; // Auto generate python3.dll import libraries for Windows targets. #[cfg(feature = "python3-dll-a")] @@ -2200,7 +2216,7 @@ mod tests { let min_version = "3.7".parse().unwrap(); assert_eq!( - default_abi3_config(&host, min_version), + default_abi3_config(&host, min_version).unwrap(), InterpreterConfig { implementation: PythonImplementation::CPython, version: PythonVersion { major: 3, minor: 7 }, @@ -2223,7 +2239,7 @@ mod tests { let min_version = "3.9".parse().unwrap(); assert_eq!( - default_abi3_config(&host, min_version), + default_abi3_config(&host, min_version).unwrap(), InterpreterConfig { implementation: PythonImplementation::CPython, version: PythonVersion { major: 3, minor: 9 }, @@ -2389,9 +2405,19 @@ mod tests { false, false, false, - ), + ) + .unwrap(), "python39", ); + assert!(super::default_lib_name_windows( + PythonVersion { major: 3, minor: 9 }, + CPython, + false, + false, + false, + true, + ) + .is_err()); assert_eq!( super::default_lib_name_windows( PythonVersion { major: 3, minor: 9 }, @@ -2400,7 +2426,8 @@ mod tests { false, false, false, - ), + ) + .unwrap(), "python3", ); assert_eq!( @@ -2411,7 +2438,8 @@ mod tests { true, false, false, - ), + ) + .unwrap(), "python3.9", ); assert_eq!( @@ -2422,7 +2450,8 @@ mod tests { true, false, false, - ), + ) + .unwrap(), "python3", ); assert_eq!( @@ -2433,7 +2462,8 @@ mod tests { false, false, false, - ), + ) + .unwrap(), "python39", ); assert_eq!( @@ -2444,10 +2474,11 @@ mod tests { false, true, false, - ), + ) + .unwrap(), "python39_d", ); - // abi3 debug builds on windows use version-specific lib + // abi3 debug builds on windows use version-specific lib on 3.9 and older // to workaround https://github.com/python/cpython/issues/101614 assert_eq!( super::default_lib_name_windows( @@ -2457,9 +2488,81 @@ mod tests { false, true, false, - ), + ) + .unwrap(), "python39_d", ); + assert_eq!( + super::default_lib_name_windows( + PythonVersion { + major: 3, + minor: 10 + }, + CPython, + true, + false, + true, + false, + ) + .unwrap(), + "python3_d", + ); + // Python versions older than 3.13 don't support gil_disabled + assert!(super::default_lib_name_windows( + PythonVersion { + major: 3, + minor: 12, + }, + CPython, + false, + false, + false, + true, + ) + .is_err()); + // mingw and free-threading are incompatible (until someone adds support) + assert!(super::default_lib_name_windows( + PythonVersion { + major: 3, + minor: 12, + }, + CPython, + false, + true, + false, + true, + ) + .is_err()); + assert_eq!( + super::default_lib_name_windows( + PythonVersion { + major: 3, + minor: 13 + }, + CPython, + false, + false, + false, + true, + ) + .unwrap(), + "python313t", + ); + assert_eq!( + super::default_lib_name_windows( + PythonVersion { + major: 3, + minor: 13 + }, + CPython, + false, + false, + true, + true, + ) + .unwrap(), + "python313t_d", + ); } #[test] @@ -2472,7 +2575,8 @@ mod tests { CPython, None, false - ), + ) + .unwrap(), "python3.7m", ); // Defaults to pythonX.Y for CPython 3.8+ @@ -2482,7 +2586,8 @@ mod tests { CPython, None, false - ), + ) + .unwrap(), "python3.8", ); assert_eq!( @@ -2491,7 +2596,8 @@ mod tests { CPython, None, false - ), + ) + .unwrap(), "python3.9", ); // Can use ldversion to override for CPython @@ -2501,13 +2607,15 @@ mod tests { CPython, Some("3.7md"), false - ), + ) + .unwrap(), "python3.7md", ); // PyPy 3.9 includes ldversion assert_eq!( - super::default_lib_name_unix(PythonVersion { major: 3, minor: 9 }, PyPy, None, false), + super::default_lib_name_unix(PythonVersion { major: 3, minor: 9 }, PyPy, None, false) + .unwrap(), "pypy3.9-c", ); @@ -2517,9 +2625,36 @@ mod tests { PyPy, Some("3.9d"), false - ), + ) + .unwrap(), "pypy3.9d-c", ); + + // free-threading adds a t suffix + assert_eq!( + super::default_lib_name_unix( + PythonVersion { + major: 3, + minor: 13 + }, + CPython, + None, + true + ) + .unwrap(), + "python3.13t", + ); + // 3.12 and older are incompatible with gil_disabled + assert!(super::default_lib_name_unix( + PythonVersion { + major: 3, + minor: 12, + }, + CPython, + None, + true, + ) + .is_err()); } #[test] diff --git a/pyo3-ffi/build.rs b/pyo3-ffi/build.rs index 931838b5e5d..ea023de75fa 100644 --- a/pyo3-ffi/build.rs +++ b/pyo3-ffi/build.rs @@ -4,7 +4,7 @@ use pyo3_build_config::{ cargo_env_var, env_var, errors::Result, is_linking_libpython, resolve_interpreter_config, InterpreterConfig, PythonVersion, }, - warn, BuildFlag, PythonImplementation, + warn, PythonImplementation, }; /// Minimum Python version PyO3 supports. @@ -56,15 +56,22 @@ fn ensure_python_version(interpreter_config: &InterpreterConfig) -> Result<()> { interpreter_config.version, versions.min, ); - ensure!( - interpreter_config.version <= versions.max || env_var("PYO3_USE_ABI3_FORWARD_COMPATIBILITY").map_or(false, |os_str| os_str == "1"), - "the configured Python interpreter version ({}) is newer than PyO3's maximum supported version ({})\n\ - = help: please check if an updated version of PyO3 is available. Current version: {}\n\ - = help: set PYO3_USE_ABI3_FORWARD_COMPATIBILITY=1 to suppress this check and build anyway using the stable ABI", - interpreter_config.version, - versions.max, - std::env::var("CARGO_PKG_VERSION").unwrap(), - ); + if interpreter_config.version > versions.max { + ensure!(!interpreter_config.is_free_threaded(), + "The configured Python interpreter version ({}) is newer than PyO3's maximum supported version ({})\n\ + = help: please check if an updated version of PyO3 is available. Current version: {}\n\ + = help: The free-threaded build of CPython does not support the limited API so this check cannot be suppressed.", + interpreter_config.version, versions.max, std::env::var("CARGO_PKG_VERSION").unwrap() + ); + ensure!(env_var("PYO3_USE_ABI3_FORWARD_COMPATIBILITY").map_or(false, |os_str| os_str == "1"), + "the configured Python interpreter version ({}) is newer than PyO3's maximum supported version ({})\n\ + = help: please check if an updated version of PyO3 is available. Current version: {}\n\ + = help: set PYO3_USE_ABI3_FORWARD_COMPATIBILITY=1 to suppress this check and build anyway using the stable ABI", + interpreter_config.version, + versions.max, + std::env::var("CARGO_PKG_VERSION").unwrap(), + ); + } } PythonImplementation::PyPy => { let versions = SUPPORTED_VERSIONS_PYPY; @@ -107,14 +114,10 @@ fn ensure_python_version(interpreter_config: &InterpreterConfig) -> Result<()> { if interpreter_config.abi3 { match interpreter_config.implementation { PythonImplementation::CPython => { - if interpreter_config - .build_flags - .0 - .contains(&BuildFlag::Py_GIL_DISABLED) - { + if interpreter_config.is_free_threaded() { warn!( "The free-threaded build of CPython does not yet support abi3 so the build artifacts will be version-specific." - ) + ) } } PythonImplementation::PyPy => warn!( From 74ab0c094e004eaead18dedcb4c2db6157bc222f Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Mon, 25 Nov 2024 22:56:16 +0000 Subject: [PATCH 385/495] release: 0.23.2 (#4731) --- CHANGELOG.md | 15 ++++++++++++++- Cargo.toml | 8 ++++---- README.md | 4 ++-- 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/4708.added.md | 1 - newsfragments/4719.fixed.md | 2 -- newsfragments/4725.fixed.md | 1 - pyo3-build-config/Cargo.toml | 2 +- pyo3-ffi/Cargo.toml | 4 ++-- pyo3-ffi/README.md | 4 ++-- pyo3-macros-backend/Cargo.toml | 6 +++--- pyo3-macros/Cargo.toml | 4 ++-- pyproject.toml | 2 +- tests/ui/reject_generics.stderr | 4 ++-- 18 files changed, 38 insertions(+), 29 deletions(-) delete mode 100644 newsfragments/4708.added.md delete mode 100644 newsfragments/4719.fixed.md delete mode 100644 newsfragments/4725.fixed.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 4bfce433293..1ab62499596 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,18 @@ To see unreleased changes, please see the [CHANGELOG on the main branch guide](h +## [0.23.2] - 2024-11-25 + +### Added + +- Add `IntoPyObjectExt` trait. [#4708](https://github.com/PyO3/pyo3/pull/4708) + +### Fixed + +- Fix compile failures when building for free-threaded Python when the `abi3` or `abi3-pyxx` features are enabled. [#4719](https://github.com/PyO3/pyo3/pull/4719) +- Fix `ambiguous_associated_items` lint error in `#[pyclass]` and `#[derive(IntoPyObject)]` macros. [#4725](https://github.com/PyO3/pyo3/pull/4725) + + ## [0.23.1] - 2024-11-16 Re-release of 0.23.0 with fixes to docs.rs build. @@ -2000,7 +2012,8 @@ Yanked - Initial release -[Unreleased]: https://github.com/pyo3/pyo3/compare/v0.23.1...HEAD +[Unreleased]: https://github.com/pyo3/pyo3/compare/v0.23.2...HEAD +[0.23.2]: https://github.com/pyo3/pyo3/compare/v0.23.1...v0.23.2 [0.23.1]: https://github.com/pyo3/pyo3/compare/v0.23.0...v0.23.1 [0.23.0]: https://github.com/pyo3/pyo3/compare/v0.22.5...v0.23.0 [0.22.5]: https://github.com/pyo3/pyo3/compare/v0.22.4...v0.22.5 diff --git a/Cargo.toml b/Cargo.toml index 3eca038e054..778a8f7df2e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3" -version = "0.23.1" +version = "0.23.2" description = "Bindings to Python interpreter" authors = ["PyO3 Project and Contributors "] readme = "README.md" @@ -21,10 +21,10 @@ memoffset = "0.9" once_cell = "1.13" # ffi bindings to the python interpreter, split into a separate crate so they can be used independently -pyo3-ffi = { path = "pyo3-ffi", version = "=0.23.1" } +pyo3-ffi = { path = "pyo3-ffi", version = "=0.23.2" } # support crates for macros feature -pyo3-macros = { path = "pyo3-macros", version = "=0.23.1", optional = true } +pyo3-macros = { path = "pyo3-macros", version = "=0.23.2", optional = true } indoc = { version = "2.0.1", optional = true } unindent = { version = "0.2.1", optional = true } @@ -66,7 +66,7 @@ static_assertions = "1.1.0" uuid = {version = "1.10.0", features = ["v4"] } [build-dependencies] -pyo3-build-config = { path = "pyo3-build-config", version = "=0.23.1", features = ["resolve-config"] } +pyo3-build-config = { path = "pyo3-build-config", version = "=0.23.2", features = ["resolve-config"] } [features] default = ["macros"] diff --git a/README.md b/README.md index c0a46af44d6..58c6ce17c9d 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,7 @@ name = "string_sum" crate-type = ["cdylib"] [dependencies] -pyo3 = { version = "0.23.1", features = ["extension-module"] } +pyo3 = { version = "0.23.2", features = ["extension-module"] } ``` **`src/lib.rs`** @@ -140,7 +140,7 @@ Start a new project with `cargo new` and add `pyo3` to the `Cargo.toml` like th ```toml [dependencies.pyo3] -version = "0.23.1" +version = "0.23.2" features = ["auto-initialize"] ``` diff --git a/examples/decorator/.template/pre-script.rhai b/examples/decorator/.template/pre-script.rhai index db0f2e8b002..56353bbc3e2 100644 --- a/examples/decorator/.template/pre-script.rhai +++ b/examples/decorator/.template/pre-script.rhai @@ -1,4 +1,4 @@ -variable::set("PYO3_VERSION", "0.23.1"); +variable::set("PYO3_VERSION", "0.23.2"); 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 db0f2e8b002..56353bbc3e2 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.23.1"); +variable::set("PYO3_VERSION", "0.23.2"); 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 90eb43e817c..d8bba8b5fa7 100644 --- a/examples/plugin/.template/pre-script.rhai +++ b/examples/plugin/.template/pre-script.rhai @@ -1,4 +1,4 @@ -variable::set("PYO3_VERSION", "0.23.1"); +variable::set("PYO3_VERSION", "0.23.2"); file::rename(".template/Cargo.toml", "Cargo.toml"); file::rename(".template/plugin_api/Cargo.toml", "plugin_api/Cargo.toml"); file::delete(".template"); diff --git a/examples/setuptools-rust-starter/.template/pre-script.rhai b/examples/setuptools-rust-starter/.template/pre-script.rhai index eeb279d1497..4d41dacca0c 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.23.1"); +variable::set("PYO3_VERSION", "0.23.2"); 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 db0f2e8b002..56353bbc3e2 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.23.1"); +variable::set("PYO3_VERSION", "0.23.2"); file::rename(".template/Cargo.toml", "Cargo.toml"); file::rename(".template/pyproject.toml", "pyproject.toml"); file::delete(".template"); diff --git a/newsfragments/4708.added.md b/newsfragments/4708.added.md deleted file mode 100644 index c8c91d16221..00000000000 --- a/newsfragments/4708.added.md +++ /dev/null @@ -1 +0,0 @@ -Add `IntoPyObjectExt` trait. diff --git a/newsfragments/4719.fixed.md b/newsfragments/4719.fixed.md deleted file mode 100644 index 08cb1c8268e..00000000000 --- a/newsfragments/4719.fixed.md +++ /dev/null @@ -1,2 +0,0 @@ -* Fixed an issue that prevented building free-threaded extensions for crates - that request a specific minimum limited API version. diff --git a/newsfragments/4725.fixed.md b/newsfragments/4725.fixed.md deleted file mode 100644 index 9ee069c5eb4..00000000000 --- a/newsfragments/4725.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Fix `ambiguous_associated_items` lint error in `#[pyclass]` and `#[derive(IntoPyObject)]` macros. diff --git a/pyo3-build-config/Cargo.toml b/pyo3-build-config/Cargo.toml index 29cecb0cf95..cb2c6c6eebd 100644 --- a/pyo3-build-config/Cargo.toml +++ b/pyo3-build-config/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3-build-config" -version = "0.23.1" +version = "0.23.2" 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 3b7a63fe3d9..76fa9e4b8e1 100644 --- a/pyo3-ffi/Cargo.toml +++ b/pyo3-ffi/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3-ffi" -version = "0.23.1" +version = "0.23.2" description = "Python-API bindings for the PyO3 ecosystem" authors = ["PyO3 Project and Contributors "] keywords = ["pyo3", "python", "cpython", "ffi"] @@ -41,7 +41,7 @@ generate-import-lib = ["pyo3-build-config/python3-dll-a"] paste = "1" [build-dependencies] -pyo3-build-config = { path = "../pyo3-build-config", version = "=0.23.1", features = ["resolve-config"] } +pyo3-build-config = { path = "../pyo3-build-config", version = "=0.23.2", features = ["resolve-config"] } [lints] workspace = true diff --git a/pyo3-ffi/README.md b/pyo3-ffi/README.md index 097822709bd..d80dad93b3d 100644 --- a/pyo3-ffi/README.md +++ b/pyo3-ffi/README.md @@ -41,13 +41,13 @@ name = "string_sum" crate-type = ["cdylib"] [dependencies.pyo3-ffi] -version = "0.23.1" +version = "0.23.2" features = ["extension-module"] [build-dependencies] # This is only necessary if you need to configure your build based on # the Python version or the compile-time configuration for the interpreter. -pyo3_build_config = "0.23.1" +pyo3_build_config = "0.23.2" ``` If you need to use conditional compilation based on Python version or how diff --git a/pyo3-macros-backend/Cargo.toml b/pyo3-macros-backend/Cargo.toml index 864d0b3712a..d6daa874361 100644 --- a/pyo3-macros-backend/Cargo.toml +++ b/pyo3-macros-backend/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3-macros-backend" -version = "0.23.1" +version = "0.23.2" description = "Code generation for PyO3 package" authors = ["PyO3 Project and Contributors "] keywords = ["pyo3", "python", "cpython", "ffi"] @@ -16,7 +16,7 @@ edition = "2021" [dependencies] heck = "0.5" proc-macro2 = { version = "1.0.60", default-features = false } -pyo3-build-config = { path = "../pyo3-build-config", version = "=0.23.1", features = ["resolve-config"] } +pyo3-build-config = { path = "../pyo3-build-config", version = "=0.23.2", features = ["resolve-config"] } quote = { version = "1", default-features = false } [dependencies.syn] @@ -25,7 +25,7 @@ default-features = false features = ["derive", "parsing", "printing", "clone-impls", "full", "extra-traits"] [build-dependencies] -pyo3-build-config = { path = "../pyo3-build-config", version = "=0.23.1" } +pyo3-build-config = { path = "../pyo3-build-config", version = "=0.23.2" } [lints] workspace = true diff --git a/pyo3-macros/Cargo.toml b/pyo3-macros/Cargo.toml index 18b79161f25..2db1c442d97 100644 --- a/pyo3-macros/Cargo.toml +++ b/pyo3-macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3-macros" -version = "0.23.1" +version = "0.23.2" description = "Proc macros for PyO3 package" authors = ["PyO3 Project and Contributors "] keywords = ["pyo3", "python", "cpython", "ffi"] @@ -21,7 +21,7 @@ experimental-async = ["pyo3-macros-backend/experimental-async"] proc-macro2 = { version = "1.0.60", default-features = false } quote = "1" syn = { version = "2", features = ["full", "extra-traits"] } -pyo3-macros-backend = { path = "../pyo3-macros-backend", version = "=0.23.1" } +pyo3-macros-backend = { path = "../pyo3-macros-backend", version = "=0.23.2" } [lints] workspace = true diff --git a/pyproject.toml b/pyproject.toml index 211a374db59..e3a55bb6d66 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ [tool.towncrier] filename = "CHANGELOG.md" -version = "0.23.1" +version = "0.23.2" start_string = "\n" template = ".towncrier.template.md" title_format = "## [{version}] - {project_date}" diff --git a/tests/ui/reject_generics.stderr b/tests/ui/reject_generics.stderr index 5adb8eaca9e..1a811ddafb2 100644 --- a/tests/ui/reject_generics.stderr +++ b/tests/ui/reject_generics.stderr @@ -1,10 +1,10 @@ -error: #[pyclass] cannot have generic parameters. For an explanation, see https://pyo3.rs/v0.23.1/class.html#no-generic-parameters +error: #[pyclass] cannot have generic parameters. For an explanation, see https://pyo3.rs/v0.23.2/class.html#no-generic-parameters --> tests/ui/reject_generics.rs:4:25 | 4 | struct ClassWithGenerics { | ^ -error: #[pyclass] cannot have lifetime parameters. For an explanation, see https://pyo3.rs/v0.23.1/class.html#no-lifetime-parameters +error: #[pyclass] cannot have lifetime parameters. For an explanation, see https://pyo3.rs/v0.23.2/class.html#no-lifetime-parameters --> tests/ui/reject_generics.rs:9:27 | 9 | struct ClassWithLifetimes<'a> { From 7bd8df8d9c16358de7f7b158c8a432e00bb24706 Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Wed, 27 Nov 2024 10:19:36 +0100 Subject: [PATCH 386/495] switch deprecated implicit eq for simple enums (#4730) --- newsfragments/4730.removed.md | 1 + pyo3-macros-backend/src/pyclass.rs | 17 +---- pyo3-macros-backend/src/pymethod.rs | 9 --- src/impl_/pyclass.rs | 46 ------------- tests/test_enum.rs | 85 ++++++++----------------- tests/ui/deprecations.rs | 6 -- tests/ui/deprecations.stderr | 8 --- tests/ui/invalid_proto_pymethods.stderr | 11 ---- tests/ui/invalid_pyclass_args.stderr | 11 ---- 9 files changed, 27 insertions(+), 167 deletions(-) create mode 100644 newsfragments/4730.removed.md diff --git a/newsfragments/4730.removed.md b/newsfragments/4730.removed.md new file mode 100644 index 00000000000..de8b64f9ba6 --- /dev/null +++ b/newsfragments/4730.removed.md @@ -0,0 +1 @@ +Removed the deprecated implicit eq fallback for simple enums. \ No newline at end of file diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index 93596611f18..16e3c58f6ad 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -1903,24 +1903,11 @@ fn pyclass_richcmp_simple_enum( ensure_spanned!(options.eq.is_some(), eq_int.span() => "The `eq_int` option requires the `eq` option."); } - let deprecation = (options.eq_int.is_none() && options.eq.is_none()) - .then(|| { - quote! { - let _ = #pyo3_path::impl_::pyclass::DeprecationTest::<#cls>::new().autogenerated_equality(); - } - }) - .unwrap_or_default(); - - let mut options = options.clone(); - if options.eq.is_none() { - options.eq_int = Some(parse_quote!(eq_int)); - } - if options.eq.is_none() && options.eq_int.is_none() { return Ok((None, None)); } - let arms = pyclass_richcmp_arms(&options, ctx)?; + let arms = pyclass_richcmp_arms(options, ctx)?; let eq = options.eq.map(|eq| { quote_spanned! { eq.span() => @@ -1954,8 +1941,6 @@ fn pyclass_richcmp_simple_enum( other: &#pyo3_path::Bound<'_, #pyo3_path::PyAny>, op: #pyo3_path::pyclass::CompareOp ) -> #pyo3_path::PyResult<#pyo3_path::PyObject> { - #deprecation - #eq #eq_int diff --git a/pyo3-macros-backend/src/pymethod.rs b/pyo3-macros-backend/src/pymethod.rs index 560c3c9dcc1..1254a8d510b 100644 --- a/pyo3-macros-backend/src/pymethod.rs +++ b/pyo3-macros-backend/src/pymethod.rs @@ -1348,14 +1348,6 @@ impl SlotDef { )?; let name = spec.name; let holders = holders.init_holders(ctx); - let dep = if method_name == "__richcmp__" { - quote! { - #[allow(unknown_lints, non_local_definitions)] - impl #pyo3_path::impl_::pyclass::HasCustomRichCmp for #cls {} - } - } else { - TokenStream::default() - }; let associated_method = quote! { #[allow(non_snake_case)] unsafe fn #wrapper_ident( @@ -1363,7 +1355,6 @@ impl SlotDef { _raw_slf: *mut #pyo3_path::ffi::PyObject, #(#arg_idents: #arg_types),* ) -> #pyo3_path::PyResult<#ret_ty> { - #dep let function = #cls::#name; // Shadow the method name to avoid #3017 let _slf = _raw_slf; #holders diff --git a/src/impl_/pyclass.rs b/src/impl_/pyclass.rs index 7bb61442ec5..ac5c6e3e3f0 100644 --- a/src/impl_/pyclass.rs +++ b/src/impl_/pyclass.rs @@ -878,8 +878,6 @@ macro_rules! generate_pyclass_richcompare_slot { other: *mut $crate::ffi::PyObject, op: ::std::os::raw::c_int, ) -> *mut $crate::ffi::PyObject { - impl $crate::impl_::pyclass::HasCustomRichCmp for $cls {} - $crate::impl_::trampoline::richcmpfunc(slf, other, op, |py, slf, other, op| { use $crate::class::basic::CompareOp; use $crate::impl_::pyclass::*; @@ -1546,50 +1544,6 @@ impl ConvertField(Deprecation, ::std::marker::PhantomData); -pub struct Deprecation; - -impl DeprecationTest { - #[inline] - #[allow(clippy::new_without_default)] - pub const fn new() -> Self { - DeprecationTest(Deprecation, ::std::marker::PhantomData) - } -} - -impl std::ops::Deref for DeprecationTest { - type Target = Deprecation; - #[inline] - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl DeprecationTest -where - T: HasCustomRichCmp, -{ - /// For `HasCustomRichCmp` types; no deprecation warning. - #[inline] - pub fn autogenerated_equality(&self) {} -} - -impl Deprecation { - #[deprecated( - since = "0.22.0", - note = "Implicit equality for simple enums is deprecated. Use `#[pyclass(eq, eq_int)]` to keep the current behavior." - )] - /// For types which don't implement `HasCustomRichCmp`; emits deprecation warning. - #[inline] - pub fn autogenerated_equality(&self) {} -} - #[cfg(test)] #[cfg(feature = "macros")] mod tests { diff --git a/tests/test_enum.rs b/tests/test_enum.rs index 40c5f4681a8..537f8281297 100644 --- a/tests/test_enum.rs +++ b/tests/test_enum.rs @@ -23,6 +23,27 @@ fn test_enum_class_attr() { }) } +#[test] +fn test_enum_eq_enum() { + Python::with_gil(|py| { + let var1 = Py::new(py, MyEnum::Variant).unwrap(); + let var2 = Py::new(py, MyEnum::Variant).unwrap(); + let other_var = Py::new(py, MyEnum::OtherVariant).unwrap(); + py_assert!(py, var1 var2, "var1 == var2"); + py_assert!(py, var1 other_var, "var1 != other_var"); + py_assert!(py, var1 var2, "(var1 != var2) == False"); + }) +} + +#[test] +fn test_enum_eq_incomparable() { + Python::with_gil(|py| { + let var1 = Py::new(py, MyEnum::Variant).unwrap(); + py_assert!(py, var1, "(var1 == 'foo') == False"); + py_assert!(py, var1, "(var1 != 'foo') == True"); + }) +} + #[pyfunction] fn return_enum() -> MyEnum { MyEnum::Variant @@ -70,7 +91,11 @@ fn test_custom_discriminant() { py_run!(py, CustomDiscriminant one two, r#" assert CustomDiscriminant.One == one assert CustomDiscriminant.Two == two + assert CustomDiscriminant.One == 1 + assert CustomDiscriminant.Two == 2 assert one != two + assert CustomDiscriminant.One != 2 + assert CustomDiscriminant.Two != 1 "#); }) } @@ -300,66 +325,6 @@ fn test_complex_enum_with_hash() { }); } -#[allow(deprecated)] -mod deprecated { - use crate::py_assert; - use pyo3::prelude::*; - use pyo3::py_run; - - #[pyclass] - #[derive(Debug, PartialEq, Eq, Clone)] - pub enum MyEnum { - Variant, - OtherVariant, - } - - #[test] - fn test_enum_eq_enum() { - Python::with_gil(|py| { - let var1 = Py::new(py, MyEnum::Variant).unwrap(); - let var2 = Py::new(py, MyEnum::Variant).unwrap(); - let other_var = Py::new(py, MyEnum::OtherVariant).unwrap(); - py_assert!(py, var1 var2, "var1 == var2"); - py_assert!(py, var1 other_var, "var1 != other_var"); - py_assert!(py, var1 var2, "(var1 != var2) == False"); - }) - } - - #[test] - fn test_enum_eq_incomparable() { - Python::with_gil(|py| { - let var1 = Py::new(py, MyEnum::Variant).unwrap(); - py_assert!(py, var1, "(var1 == 'foo') == False"); - py_assert!(py, var1, "(var1 != 'foo') == True"); - }) - } - - #[pyclass] - enum CustomDiscriminant { - One = 1, - Two = 2, - } - - #[test] - fn test_custom_discriminant() { - Python::with_gil(|py| { - #[allow(non_snake_case)] - let CustomDiscriminant = py.get_type::(); - let one = Py::new(py, CustomDiscriminant::One).unwrap(); - let two = Py::new(py, CustomDiscriminant::Two).unwrap(); - py_run!(py, CustomDiscriminant one two, r#" - assert CustomDiscriminant.One == one - assert CustomDiscriminant.Two == two - assert CustomDiscriminant.One == 1 - assert CustomDiscriminant.Two == 2 - assert one != two - assert CustomDiscriminant.One != 2 - assert CustomDiscriminant.Two != 1 - "#); - }) - } -} - #[test] fn custom_eq() { #[pyclass(frozen)] diff --git a/tests/ui/deprecations.rs b/tests/ui/deprecations.rs index da78a826cae..47b00d7eeee 100644 --- a/tests/ui/deprecations.rs +++ b/tests/ui/deprecations.rs @@ -25,10 +25,4 @@ fn pyfunction_option_4( ) { } -#[pyclass] -pub enum SimpleEnumWithoutEq { - VariamtA, - VariantB, -} - fn main() {} diff --git a/tests/ui/deprecations.stderr b/tests/ui/deprecations.stderr index 6236dc55631..0c65bd83417 100644 --- a/tests/ui/deprecations.stderr +++ b/tests/ui/deprecations.stderr @@ -27,11 +27,3 @@ error: use of deprecated constant `__pyfunction_pyfunction_option_4::SIGNATURE`: | 21 | fn pyfunction_option_4( | ^^^^^^^^^^^^^^^^^^^ - -error: use of deprecated method `pyo3::impl_::pyclass::Deprecation::autogenerated_equality`: Implicit equality for simple enums is deprecated. Use `#[pyclass(eq, eq_int)]` to keep the current behavior. - --> tests/ui/deprecations.rs:28:1 - | -28 | #[pyclass] - | ^^^^^^^^^^ - | - = note: this error originates in the attribute macro `pyclass` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/tests/ui/invalid_proto_pymethods.stderr b/tests/ui/invalid_proto_pymethods.stderr index 18c96113299..82c99c2ddc3 100644 --- a/tests/ui/invalid_proto_pymethods.stderr +++ b/tests/ui/invalid_proto_pymethods.stderr @@ -40,17 +40,6 @@ note: candidate #2 is defined in an impl for the type `EqAndRichcmp` | ^^^^^^^^^^^^ = note: this error originates in the macro `::pyo3::impl_::pyclass::generate_pyclass_richcompare_slot` which comes from the expansion of the attribute macro `pymethods` (in Nightly builds, run with -Z macro-backtrace for more info) -error[E0119]: conflicting implementations of trait `HasCustomRichCmp` for type `EqAndRichcmp` - --> tests/ui/invalid_proto_pymethods.rs:55:1 - | -55 | #[pymethods] - | ^^^^^^^^^^^^ - | | - | first implementation here - | conflicting implementation for `EqAndRichcmp` - | - = note: this error originates in the macro `::pyo3::impl_::pyclass::generate_pyclass_richcompare_slot` which comes from the expansion of the attribute macro `pymethods` (in Nightly builds, run with -Z macro-backtrace for more info) - error[E0592]: duplicate definitions with name `__pymethod___richcmp____` --> tests/ui/invalid_proto_pymethods.rs:55:1 | diff --git a/tests/ui/invalid_pyclass_args.stderr b/tests/ui/invalid_pyclass_args.stderr index d1335e0f1a1..15aa0387cc6 100644 --- a/tests/ui/invalid_pyclass_args.stderr +++ b/tests/ui/invalid_pyclass_args.stderr @@ -162,17 +162,6 @@ error: The format string syntax cannot be used with enums 171 | #[pyclass(eq, str = "Stuff...")] | ^^^^^^^^^^ -error[E0119]: conflicting implementations of trait `HasCustomRichCmp` for type `EqOptAndManualRichCmp` - --> tests/ui/invalid_pyclass_args.rs:41:1 - | -37 | #[pyclass(eq)] - | -------------- first implementation here -... -41 | #[pymethods] - | ^^^^^^^^^^^^ conflicting implementation for `EqOptAndManualRichCmp` - | - = 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___richcmp____` --> tests/ui/invalid_pyclass_args.rs:37:1 | From b308ffab55b384282fe06e9fc671af36c25d14a8 Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Wed, 27 Nov 2024 20:51:14 +0100 Subject: [PATCH 387/495] fix clippy beta (#4737) --- examples/Cargo.toml | 1 + pyo3-build-config/Cargo.toml | 1 + pyo3-build-config/src/impl_.rs | 6 +----- pyo3-ffi/Cargo.toml | 1 + pyo3-macros-backend/Cargo.toml | 1 + pyo3-macros/Cargo.toml | 1 + pytests/Cargo.toml | 1 + 7 files changed, 7 insertions(+), 5 deletions(-) diff --git a/examples/Cargo.toml b/examples/Cargo.toml index 81557e7f534..f6c77eb609c 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -3,6 +3,7 @@ name = "pyo3-examples" version = "0.0.0" publish = false edition = "2021" +rust-version = "1.63" [dev-dependencies] pyo3 = { path = "..", features = ["auto-initialize", "extension-module"] } diff --git a/pyo3-build-config/Cargo.toml b/pyo3-build-config/Cargo.toml index cb2c6c6eebd..bcf8b1de2a6 100644 --- a/pyo3-build-config/Cargo.toml +++ b/pyo3-build-config/Cargo.toml @@ -9,6 +9,7 @@ repository = "https://github.com/pyo3/pyo3" categories = ["api-bindings", "development-tools::ffi"] license = "MIT OR Apache-2.0" edition = "2021" +rust-version = "1.63" [dependencies] once_cell = "1" diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index 4e5d3c10656..3a0915b4c8e 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -1122,11 +1122,7 @@ impl BuildFlags { Self( BuildFlags::ALL .iter() - .filter(|flag| { - config_map - .get_value(flag.to_string()) - .map_or(false, |value| value == "1") - }) + .filter(|flag| config_map.get_value(flag.to_string()) == Some("1")) .cloned() .collect(), ) diff --git a/pyo3-ffi/Cargo.toml b/pyo3-ffi/Cargo.toml index 76fa9e4b8e1..1a8cd7f09d7 100644 --- a/pyo3-ffi/Cargo.toml +++ b/pyo3-ffi/Cargo.toml @@ -10,6 +10,7 @@ categories = ["api-bindings", "development-tools::ffi"] license = "MIT OR Apache-2.0" edition = "2021" links = "python" +rust-version = "1.63" [dependencies] libc = "0.2.62" diff --git a/pyo3-macros-backend/Cargo.toml b/pyo3-macros-backend/Cargo.toml index d6daa874361..03c2ed0b189 100644 --- a/pyo3-macros-backend/Cargo.toml +++ b/pyo3-macros-backend/Cargo.toml @@ -9,6 +9,7 @@ repository = "https://github.com/pyo3/pyo3" categories = ["api-bindings", "development-tools::ffi"] license = "MIT OR Apache-2.0" edition = "2021" +rust-version = "1.63" # Note: we use default-features = false for proc-macro related crates # not to depend on proc-macro itself. diff --git a/pyo3-macros/Cargo.toml b/pyo3-macros/Cargo.toml index 2db1c442d97..b85cc7e03c6 100644 --- a/pyo3-macros/Cargo.toml +++ b/pyo3-macros/Cargo.toml @@ -9,6 +9,7 @@ repository = "https://github.com/pyo3/pyo3" categories = ["api-bindings", "development-tools::ffi"] license = "MIT OR Apache-2.0" edition = "2021" +rust-version = "1.63" [lib] proc-macro = true diff --git a/pytests/Cargo.toml b/pytests/Cargo.toml index 255094a6c40..1fee3093275 100644 --- a/pytests/Cargo.toml +++ b/pytests/Cargo.toml @@ -5,6 +5,7 @@ version = "0.1.0" description = "Python-based tests for PyO3" edition = "2021" publish = false +rust-version = "1.63" [dependencies] pyo3 = { path = "../", features = ["extension-module"] } From 188f1d656c1b5f2f3e616ab2884ea4c3a80d8fe5 Mon Sep 17 00:00:00 2001 From: Keming Date: Thu, 28 Nov 2024 19:05:55 +0800 Subject: [PATCH 388/495] Update class.md (#4739) --- 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 5d2c8435416..7229330a361 100644 --- a/guide/src/class.md +++ b/guide/src/class.md @@ -122,8 +122,8 @@ create_interface!(FloatClass, String); #### Must be thread-safe Python objects are freely shared between threads by the Python interpreter. This means that: -- Python objects may be created and destroyed by different Python threads; therefore #[pyclass]` objects must be `Send`. -- Python objects may be accessed by multiple python threads simultaneously; therefore `#[pyclass]` objects must be `Sync`. +- Python objects may be created and destroyed by different Python threads; therefore `#[pyclass]` objects must be `Send`. +- Python objects may be accessed by multiple Python threads simultaneously; therefore `#[pyclass]` objects must be `Sync`. For now, don't worry about these requirements; simple classes will already be thread-safe. There is a [detailed discussion on thread-safety](./class/thread-safety.md) later in the guide. From 1183ea3eca9d02fd0467c86fb67b1406516b22dd Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Thu, 28 Nov 2024 21:40:47 +0100 Subject: [PATCH 389/495] ci: updates for Rust 1.83 (#4741) --- tests/ui/invalid_pyfunctions.stderr | 2 +- tests/ui/static_ref.stderr | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/ui/invalid_pyfunctions.stderr b/tests/ui/invalid_pyfunctions.stderr index ab35b086b94..9b35b8dd9fe 100644 --- a/tests/ui/invalid_pyfunctions.stderr +++ b/tests/ui/invalid_pyfunctions.stderr @@ -58,6 +58,6 @@ error[E0277]: the trait bound `&str: From` `String` implements `From<&str>` `String` implements `From>` - `String` implements `From>` + `String` implements `From>` `String` implements `From` = note: required for `BoundRef<'_, '_, pyo3::types::PyModule>` to implement `Into<&str>` diff --git a/tests/ui/static_ref.stderr b/tests/ui/static_ref.stderr index 77c3646745e..9fe37355980 100644 --- a/tests/ui/static_ref.stderr +++ b/tests/ui/static_ref.stderr @@ -5,7 +5,7 @@ error: lifetime may not live long enough | ^^^^^^^^^^^^^ | | | lifetime `'py` defined here - | cast requires that `'py` must outlive `'static` + | coercion requires that `'py` must outlive `'static` | = note: this error originates in the attribute macro `pyfunction` (in Nightly builds, run with -Z macro-backtrace for more info) @@ -42,6 +42,6 @@ error: lifetime may not live long enough | ^^^^^^^^^^^^^ | | | lifetime `'py` defined here - | cast requires that `'py` must outlive `'static` + | coercion requires that `'py` must outlive `'static` | = note: this error originates in the attribute macro `pyfunction` (in Nightly builds, run with -Z macro-backtrace for more info) From 6abb69d9fa890b61a8048dd5be5084a4640f7621 Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Thu, 28 Nov 2024 23:38:11 +0100 Subject: [PATCH 390/495] removes implicit default of trailing optional arguments (see #2935) (#4729) --- guide/src/function/signature.md | 78 +------------------ guide/src/migration.md | 4 +- newsfragments/4729.removed.md | 1 + pyo3-macros-backend/src/deprecations.rs | 54 ------------- pyo3-macros-backend/src/lib.rs | 1 - pyo3-macros-backend/src/method.rs | 9 +-- pyo3-macros-backend/src/params.rs | 52 ++++++------- pyo3-macros-backend/src/pyclass.rs | 6 +- pyo3-macros-backend/src/pyfunction.rs | 2 +- .../src/pyfunction/signature.rs | 20 ++--- pyo3-macros-backend/src/pymethod.rs | 3 - tests/test_compile_error.rs | 1 - tests/ui/deprecations.rs | 28 ------- tests/ui/deprecations.stderr | 29 ------- tests/ui/invalid_pyfunctions.rs | 3 - tests/ui/invalid_pyfunctions.stderr | 23 ++---- 16 files changed, 49 insertions(+), 265 deletions(-) create mode 100644 newsfragments/4729.removed.md delete mode 100644 pyo3-macros-backend/src/deprecations.rs delete mode 100644 tests/ui/deprecations.rs delete mode 100644 tests/ui/deprecations.stderr diff --git a/guide/src/function/signature.md b/guide/src/function/signature.md index 8ebe74456a1..431cad87bfd 100644 --- a/guide/src/function/signature.md +++ b/guide/src/function/signature.md @@ -2,7 +2,7 @@ The `#[pyfunction]` attribute also accepts parameters to control how the generated Python function accepts arguments. Just like in Python, arguments can be positional-only, keyword-only, or accept either. `*args` lists and `**kwargs` dicts can also be accepted. These parameters also work for `#[pymethods]` which will be introduced in the [Python Classes](../class.md) section of the guide. -Like Python, by default PyO3 accepts all arguments as either positional or keyword arguments. Most arguments are required by default, except for trailing `Option<_>` arguments, which are [implicitly given a default of `None`](#trailing-optional-arguments). This behaviour can be configured by the `#[pyo3(signature = (...))]` option which allows writing a signature in Python syntax. +Like Python, by default PyO3 accepts all arguments as either positional or keyword arguments. All arguments are required by default. This behaviour can be configured by the `#[pyo3(signature = (...))]` option which allows writing a signature in Python syntax. This section of the guide goes into detail about use of the `#[pyo3(signature = (...))]` option and its related option `#[pyo3(text_signature = "...")]` @@ -118,82 +118,6 @@ num=-1 > } > ``` -## Trailing optional arguments - -
- -⚠️ Warning: This behaviour is being phased out 🛠️ - -The special casing of trailing optional arguments is deprecated. In a future `pyo3` version, arguments of type `Option<..>` will share the same behaviour as other arguments, they are required unless a default is set using `#[pyo3(signature = (...))]`. - -This is done to better align the Python and Rust definition of such functions and make it more intuitive to rewrite them from Python in Rust. Specifically `def some_fn(a: int, b: Optional[int]): ...` will not automatically default `b` to `none`, but requires an explicit default if desired, where as in current `pyo3` it is handled the other way around. - -During the migration window a `#[pyo3(signature = (...))]` will be required to silence the deprecation warning. After support for trailing optional arguments is fully removed, the signature attribute can be removed if all arguments should be required. -
- - -As a convenience, functions without a `#[pyo3(signature = (...))]` option will treat trailing `Option` arguments as having a default of `None`. In the example below, PyO3 will create `increment` with a signature of `increment(x, amount=None)`. - -```rust -#![allow(deprecated)] -use pyo3::prelude::*; - -/// Returns a copy of `x` increased by `amount`. -/// -/// If `amount` is unspecified or `None`, equivalent to `x + 1`. -#[pyfunction] -fn increment(x: u64, amount: Option) -> u64 { - x + amount.unwrap_or(1) -} -# -# fn main() -> PyResult<()> { -# Python::with_gil(|py| { -# let fun = pyo3::wrap_pyfunction!(increment, py)?; -# -# let inspect = PyModule::import(py, "inspect")?.getattr("signature")?; -# let sig: String = inspect -# .call1((fun,))? -# .call_method0("__str__")? -# .extract()?; -# -# #[cfg(Py_3_8)] // on 3.7 the signature doesn't render b, upstream bug? -# assert_eq!(sig, "(x, amount=None)"); -# -# Ok(()) -# }) -# } -``` - -To make trailing `Option` arguments required, but still accept `None`, add a `#[pyo3(signature = (...))]` annotation. For the example above, this would be `#[pyo3(signature = (x, amount))]`: - -```rust -# use pyo3::prelude::*; -#[pyfunction] -#[pyo3(signature = (x, amount))] -fn increment(x: u64, amount: Option) -> u64 { - x + amount.unwrap_or(1) -} -# -# fn main() -> PyResult<()> { -# Python::with_gil(|py| { -# let fun = pyo3::wrap_pyfunction!(increment, py)?; -# -# let inspect = PyModule::import(py, "inspect")?.getattr("signature")?; -# let sig: String = inspect -# .call1((fun,))? -# .call_method0("__str__")? -# .extract()?; -# -# #[cfg(Py_3_8)] // on 3.7 the signature doesn't render b, upstream bug? -# assert_eq!(sig, "(x, amount)"); -# -# Ok(()) -# }) -# } -``` - -To help avoid confusion, PyO3 requires `#[pyo3(signature = (...))]` when an `Option` argument is surrounded by arguments which aren't `Option`. - ## Making the function signature available to Python The function signature is exposed to Python via the `__text_signature__` attribute. PyO3 automatically generates this for every `#[pyfunction]` and all `#[pymethods]` directly from the Rust function, taking into account any override done with the `#[pyo3(signature = (...))]` option. diff --git a/guide/src/migration.md b/guide/src/migration.md index 8a8f3694f6d..35126dfcaef 100644 --- a/guide/src/migration.md +++ b/guide/src/migration.md @@ -872,7 +872,7 @@ Python::with_gil(|py| -> PyResult<()> {
Click to expand -[Trailing `Option` arguments](./function/signature.md#trailing-optional-arguments) have an automatic default of `None`. To avoid unwanted changes when modifying function signatures, in PyO3 0.18 it was deprecated to have a required argument after an `Option` argument without using `#[pyo3(signature = (...))]` to specify the intended defaults. In PyO3 0.20, this becomes a hard error. +Trailing `Option` arguments have an automatic default of `None`. To avoid unwanted changes when modifying function signatures, in PyO3 0.18 it was deprecated to have a required argument after an `Option` argument without using `#[pyo3(signature = (...))]` to specify the intended defaults. In PyO3 0.20, this becomes a hard error. Before: @@ -1095,7 +1095,7 @@ Starting with PyO3 0.18, this is deprecated and a future PyO3 version will requi Before, x in the below example would be required to be passed from Python code: -```rust,compile_fail +```rust,compile_fail,ignore # #![allow(dead_code)] # use pyo3::prelude::*; diff --git a/newsfragments/4729.removed.md b/newsfragments/4729.removed.md new file mode 100644 index 00000000000..da1498ee69f --- /dev/null +++ b/newsfragments/4729.removed.md @@ -0,0 +1 @@ +removes implicit default of trailing optional arguments (see #2935) \ No newline at end of file diff --git a/pyo3-macros-backend/src/deprecations.rs b/pyo3-macros-backend/src/deprecations.rs deleted file mode 100644 index df48c9da417..00000000000 --- a/pyo3-macros-backend/src/deprecations.rs +++ /dev/null @@ -1,54 +0,0 @@ -use crate::method::{FnArg, FnSpec}; -use proc_macro2::TokenStream; -use quote::quote_spanned; - -pub(crate) fn deprecate_trailing_option_default(spec: &FnSpec<'_>) -> TokenStream { - if spec.signature.attribute.is_none() - && spec.tp.signature_attribute_allowed() - && spec.signature.arguments.iter().any(|arg| { - if let FnArg::Regular(arg) = arg { - arg.option_wrapped_type.is_some() - } else { - false - } - }) - { - use std::fmt::Write; - let mut deprecation_msg = String::from( - "this function has implicit defaults for the trailing `Option` arguments \n\ - = note: these implicit defaults are being phased out \n\ - = help: add `#[pyo3(signature = (", - ); - spec.signature.arguments.iter().for_each(|arg| { - match arg { - FnArg::Regular(arg) => { - if arg.option_wrapped_type.is_some() { - write!(deprecation_msg, "{}=None, ", arg.name) - } else { - write!(deprecation_msg, "{}, ", arg.name) - } - } - FnArg::VarArgs(arg) => write!(deprecation_msg, "{}, ", arg.name), - FnArg::KwArgs(arg) => write!(deprecation_msg, "{}, ", arg.name), - FnArg::Py(_) | FnArg::CancelHandle(_) => Ok(()), - } - .expect("writing to `String` should not fail"); - }); - - //remove trailing space and comma - deprecation_msg.pop(); - deprecation_msg.pop(); - - deprecation_msg.push_str( - "))]` to this function to silence this warning and keep the current behavior", - ); - quote_spanned! { spec.name.span() => - #[deprecated(note = #deprecation_msg)] - #[allow(dead_code)] - const SIGNATURE: () = (); - const _: () = SIGNATURE; - } - } else { - TokenStream::new() - } -} diff --git a/pyo3-macros-backend/src/lib.rs b/pyo3-macros-backend/src/lib.rs index d6c8f287332..7893a94af98 100644 --- a/pyo3-macros-backend/src/lib.rs +++ b/pyo3-macros-backend/src/lib.rs @@ -9,7 +9,6 @@ mod utils; mod attributes; -mod deprecations; mod frompyobject; mod intopyobject; mod konst; diff --git a/pyo3-macros-backend/src/method.rs b/pyo3-macros-backend/src/method.rs index f99e64562b7..a1d7a95df35 100644 --- a/pyo3-macros-backend/src/method.rs +++ b/pyo3-macros-backend/src/method.rs @@ -6,7 +6,6 @@ use proc_macro2::{Span, TokenStream}; use quote::{format_ident, quote, quote_spanned, ToTokens}; use syn::{ext::IdentExt, spanned::Spanned, Ident, Result}; -use crate::deprecations::deprecate_trailing_option_default; use crate::pyversions::is_abi3_before; use crate::utils::{Ctx, LitCStr}; use crate::{ @@ -474,7 +473,7 @@ impl<'a> FnSpec<'a> { let signature = if let Some(signature) = signature { FunctionSignature::from_arguments_and_attribute(arguments, signature)? } else { - FunctionSignature::from_arguments(arguments)? + FunctionSignature::from_arguments(arguments) }; let convention = if matches!(fn_type, FnType::FnNew | FnType::FnNewClass(_)) { @@ -745,8 +744,6 @@ impl<'a> FnSpec<'a> { quote!(#func_name) }; - let deprecation = deprecate_trailing_option_default(self); - Ok(match self.convention { CallingConvention::Noargs => { let mut holders = Holders::new(); @@ -767,7 +764,6 @@ impl<'a> FnSpec<'a> { py: #pyo3_path::Python<'py>, _slf: *mut #pyo3_path::ffi::PyObject, ) -> #pyo3_path::PyResult<*mut #pyo3_path::ffi::PyObject> { - #deprecation let function = #rust_name; // Shadow the function name to avoid #3017 #init_holders let result = #call; @@ -789,7 +785,6 @@ impl<'a> FnSpec<'a> { _nargs: #pyo3_path::ffi::Py_ssize_t, _kwnames: *mut #pyo3_path::ffi::PyObject ) -> #pyo3_path::PyResult<*mut #pyo3_path::ffi::PyObject> { - #deprecation let function = #rust_name; // Shadow the function name to avoid #3017 #arg_convert #init_holders @@ -811,7 +806,6 @@ impl<'a> FnSpec<'a> { _args: *mut #pyo3_path::ffi::PyObject, _kwargs: *mut #pyo3_path::ffi::PyObject ) -> #pyo3_path::PyResult<*mut #pyo3_path::ffi::PyObject> { - #deprecation let function = #rust_name; // Shadow the function name to avoid #3017 #arg_convert #init_holders @@ -836,7 +830,6 @@ impl<'a> FnSpec<'a> { _kwargs: *mut #pyo3_path::ffi::PyObject ) -> #pyo3_path::PyResult<*mut #pyo3_path::ffi::PyObject> { use #pyo3_path::impl_::callback::IntoPyCallbackOutput; - #deprecation let function = #rust_name; // Shadow the function name to avoid #3017 #arg_convert #init_holders diff --git a/pyo3-macros-backend/src/params.rs b/pyo3-macros-backend/src/params.rs index 67054458c98..9517d35b25c 100644 --- a/pyo3-macros-backend/src/params.rs +++ b/pyo3-macros-backend/src/params.rs @@ -245,10 +245,7 @@ pub(crate) fn impl_regular_arg_param( // Option arguments have special treatment: the default should be specified _without_ the // Some() wrapper. Maybe this should be changed in future?! if arg.option_wrapped_type.is_some() { - default = Some(default.map_or_else( - || quote!(::std::option::Option::None), - |tokens| some_wrap(tokens, ctx), - )); + default = default.map(|tokens| some_wrap(tokens, ctx)); } if arg.from_py_with.is_some() { @@ -273,31 +270,32 @@ pub(crate) fn impl_regular_arg_param( )? } } - } else if arg.option_wrapped_type.is_some() { - let holder = holders.push_holder(arg.name.span()); - quote_arg_span! { - #pyo3_path::impl_::extract_argument::extract_optional_argument( - #arg_value, - &mut #holder, - #name_str, - #[allow(clippy::redundant_closure)] - { - || #default - } - )? - } } else if let Some(default) = default { let holder = holders.push_holder(arg.name.span()); - quote_arg_span! { - #pyo3_path::impl_::extract_argument::extract_argument_with_default( - #arg_value, - &mut #holder, - #name_str, - #[allow(clippy::redundant_closure)] - { - || #default - } - )? + if arg.option_wrapped_type.is_some() { + quote_arg_span! { + #pyo3_path::impl_::extract_argument::extract_optional_argument( + #arg_value, + &mut #holder, + #name_str, + #[allow(clippy::redundant_closure)] + { + || #default + } + )? + } + } else { + quote_arg_span! { + #pyo3_path::impl_::extract_argument::extract_argument_with_default( + #arg_value, + &mut #holder, + #name_str, + #[allow(clippy::redundant_closure)] + { + || #default + } + )? + } } } else { let holder = holders.push_holder(arg.name.span()); diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index 16e3c58f6ad..3ac3fa358db 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -1660,7 +1660,7 @@ fn complex_enum_struct_variant_new<'a>( constructor.into_signature(), )? } else { - crate::pyfunction::FunctionSignature::from_arguments(args)? + crate::pyfunction::FunctionSignature::from_arguments(args) }; let spec = FnSpec { @@ -1714,7 +1714,7 @@ fn complex_enum_tuple_variant_new<'a>( constructor.into_signature(), )? } else { - crate::pyfunction::FunctionSignature::from_arguments(args)? + crate::pyfunction::FunctionSignature::from_arguments(args) }; let spec = FnSpec { @@ -1737,7 +1737,7 @@ fn complex_enum_variant_field_getter<'a>( field_span: Span, ctx: &Ctx, ) -> Result { - let signature = crate::pyfunction::FunctionSignature::from_arguments(vec![])?; + let signature = crate::pyfunction::FunctionSignature::from_arguments(vec![]); let self_type = crate::method::SelfType::TryFromBoundRef(field_span); diff --git a/pyo3-macros-backend/src/pyfunction.rs b/pyo3-macros-backend/src/pyfunction.rs index 3059025caf7..f28fa795177 100644 --- a/pyo3-macros-backend/src/pyfunction.rs +++ b/pyo3-macros-backend/src/pyfunction.rs @@ -239,7 +239,7 @@ pub fn impl_wrap_pyfunction( let signature = if let Some(signature) = signature { FunctionSignature::from_arguments_and_attribute(arguments, signature)? } else { - FunctionSignature::from_arguments(arguments)? + FunctionSignature::from_arguments(arguments) }; let spec = method::FnSpec { diff --git a/pyo3-macros-backend/src/pyfunction/signature.rs b/pyo3-macros-backend/src/pyfunction/signature.rs index 0a2d861d2b1..deea3dfa052 100644 --- a/pyo3-macros-backend/src/pyfunction/signature.rs +++ b/pyo3-macros-backend/src/pyfunction/signature.rs @@ -459,7 +459,7 @@ impl<'a> FunctionSignature<'a> { } /// Without `#[pyo3(signature)]` or `#[args]` - just take the Rust function arguments as positional. - pub fn from_arguments(arguments: Vec>) -> syn::Result { + pub fn from_arguments(arguments: Vec>) -> Self { let mut python_signature = PythonSignature::default(); for arg in &arguments { // Python<'_> arguments don't show in Python signature @@ -467,17 +467,11 @@ impl<'a> FunctionSignature<'a> { continue; } - if let FnArg::Regular(RegularArg { - ty, - option_wrapped_type: None, - .. - }) = arg - { + if let FnArg::Regular(RegularArg { .. }) = arg { // This argument is required, all previous arguments must also have been required - ensure_spanned!( - python_signature.required_positional_parameters == python_signature.positional_parameters.len(), - ty.span() => "required arguments after an `Option<_>` argument are ambiguous\n\ - = help: add a `#[pyo3(signature)]` annotation on this function to unambiguously specify the default values for all optional parameters" + assert_eq!( + python_signature.required_positional_parameters, + python_signature.positional_parameters.len(), ); python_signature.required_positional_parameters = @@ -489,11 +483,11 @@ impl<'a> FunctionSignature<'a> { .push(arg.name().unraw().to_string()); } - Ok(Self { + Self { arguments, python_signature, attribute: None, - }) + } } fn default_value_for_parameter(&self, parameter: &str) -> String { diff --git a/pyo3-macros-backend/src/pymethod.rs b/pyo3-macros-backend/src/pymethod.rs index 1254a8d510b..3d2975e4885 100644 --- a/pyo3-macros-backend/src/pymethod.rs +++ b/pyo3-macros-backend/src/pymethod.rs @@ -2,7 +2,6 @@ use std::borrow::Cow; use std::ffi::CString; use crate::attributes::{NameAttribute, RenamingRule}; -use crate::deprecations::deprecate_trailing_option_default; use crate::method::{CallingConvention, ExtractErrorMode, PyArg}; use crate::params::{impl_regular_arg_param, Holders}; use crate::utils::PythonDoc; @@ -685,9 +684,7 @@ pub fn impl_py_setter_def( ctx, ); - let deprecation = deprecate_trailing_option_default(spec); quote! { - #deprecation #from_py_with let _val = #extract; } diff --git a/tests/test_compile_error.rs b/tests/test_compile_error.rs index e4e80e90263..b165c911735 100644 --- a/tests/test_compile_error.rs +++ b/tests/test_compile_error.rs @@ -21,7 +21,6 @@ fn test_compile_errors() { 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"); - t.compile_fail("tests/ui/deprecations.rs"); t.compile_fail("tests/ui/invalid_closure.rs"); t.compile_fail("tests/ui/pyclass_send.rs"); t.compile_fail("tests/ui/invalid_argument_attributes.rs"); diff --git a/tests/ui/deprecations.rs b/tests/ui/deprecations.rs deleted file mode 100644 index 47b00d7eeee..00000000000 --- a/tests/ui/deprecations.rs +++ /dev/null @@ -1,28 +0,0 @@ -#![deny(deprecated)] -#![allow(dead_code)] - -use pyo3::prelude::*; - -fn extract_options(obj: &Bound<'_, PyAny>) -> PyResult> { - obj.extract() -} - -#[pyfunction] -#[pyo3(signature = (_i, _any=None))] -fn pyfunction_option_1(_i: u32, _any: Option) {} - -#[pyfunction] -fn pyfunction_option_2(_i: u32, _any: Option) {} - -#[pyfunction] -fn pyfunction_option_3(_i: u32, _any: Option, _foo: Option) {} - -#[pyfunction] -fn pyfunction_option_4( - _i: u32, - #[pyo3(from_py_with = "extract_options")] _any: Option, - _foo: Option, -) { -} - -fn main() {} diff --git a/tests/ui/deprecations.stderr b/tests/ui/deprecations.stderr deleted file mode 100644 index 0c65bd83417..00000000000 --- a/tests/ui/deprecations.stderr +++ /dev/null @@ -1,29 +0,0 @@ -error: use of deprecated constant `__pyfunction_pyfunction_option_2::SIGNATURE`: this function has implicit defaults for the trailing `Option` arguments - = note: these implicit defaults are being phased out - = help: add `#[pyo3(signature = (_i, _any=None))]` to this function to silence this warning and keep the current behavior - --> tests/ui/deprecations.rs:15:4 - | -15 | fn pyfunction_option_2(_i: u32, _any: Option) {} - | ^^^^^^^^^^^^^^^^^^^ - | -note: the lint level is defined here - --> tests/ui/deprecations.rs:1:9 - | -1 | #![deny(deprecated)] - | ^^^^^^^^^^ - -error: use of deprecated constant `__pyfunction_pyfunction_option_3::SIGNATURE`: this function has implicit defaults for the trailing `Option` arguments - = note: these implicit defaults are being phased out - = help: add `#[pyo3(signature = (_i, _any=None, _foo=None))]` to this function to silence this warning and keep the current behavior - --> tests/ui/deprecations.rs:18:4 - | -18 | fn pyfunction_option_3(_i: u32, _any: Option, _foo: Option) {} - | ^^^^^^^^^^^^^^^^^^^ - -error: use of deprecated constant `__pyfunction_pyfunction_option_4::SIGNATURE`: this function has implicit defaults for the trailing `Option` arguments - = note: these implicit defaults are being phased out - = help: add `#[pyo3(signature = (_i, _any=None, _foo=None))]` to this function to silence this warning and keep the current behavior - --> tests/ui/deprecations.rs:21:4 - | -21 | fn pyfunction_option_4( - | ^^^^^^^^^^^^^^^^^^^ diff --git a/tests/ui/invalid_pyfunctions.rs b/tests/ui/invalid_pyfunctions.rs index 1c0c45d6b95..cb59808ecc7 100644 --- a/tests/ui/invalid_pyfunctions.rs +++ b/tests/ui/invalid_pyfunctions.rs @@ -13,9 +13,6 @@ fn wildcard_argument(_: i32) {} #[pyfunction] fn destructured_argument((_a, _b): (i32, i32)) {} -#[pyfunction] -fn function_with_required_after_option(_opt: Option, _x: i32) {} - #[pyfunction] #[pyo3(signature=(*args))] fn function_with_optional_args(args: Option>) { diff --git a/tests/ui/invalid_pyfunctions.stderr b/tests/ui/invalid_pyfunctions.stderr index 9b35b8dd9fe..271c5a806be 100644 --- a/tests/ui/invalid_pyfunctions.stderr +++ b/tests/ui/invalid_pyfunctions.stderr @@ -22,35 +22,28 @@ error: destructuring in arguments is not supported 14 | fn destructured_argument((_a, _b): (i32, i32)) {} | ^^^^^^^^ -error: required arguments after an `Option<_>` argument are ambiguous - = help: add a `#[pyo3(signature)]` annotation on this function to unambiguously specify the default values for all optional parameters - --> tests/ui/invalid_pyfunctions.rs:17:63 - | -17 | fn function_with_required_after_option(_opt: Option, _x: i32) {} - | ^^^ - error: args cannot be optional - --> tests/ui/invalid_pyfunctions.rs:21:32 + --> tests/ui/invalid_pyfunctions.rs:18:32 | -21 | fn function_with_optional_args(args: Option>) { +18 | fn function_with_optional_args(args: Option>) { | ^^^^ error: kwargs must be Option<_> - --> tests/ui/invalid_pyfunctions.rs:27:34 + --> tests/ui/invalid_pyfunctions.rs:24:34 | -27 | fn function_with_required_kwargs(kwargs: Bound<'_, PyDict>) { +24 | fn function_with_required_kwargs(kwargs: Bound<'_, PyDict>) { | ^^^^^^ error: expected `&PyModule` or `Py` as first argument with `pass_module` - --> tests/ui/invalid_pyfunctions.rs:32:37 + --> tests/ui/invalid_pyfunctions.rs:29:37 | -32 | fn pass_module_but_no_arguments<'py>() {} +29 | fn pass_module_but_no_arguments<'py>() {} | ^^ error[E0277]: the trait bound `&str: From>` is not satisfied - --> tests/ui/invalid_pyfunctions.rs:36:14 + --> tests/ui/invalid_pyfunctions.rs:33:14 | -36 | _string: &str, +33 | _string: &str, | ^ the trait `From>` is not implemented for `&str`, which is required by `BoundRef<'_, '_, pyo3::types::PyModule>: Into<_>` | = help: the following other types implement trait `From`: From 82eaf6d4e7b618145fc1b614f5546a6684f057c7 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Fri, 29 Nov 2024 11:51:11 +0000 Subject: [PATCH 391/495] add missing CHANGELOG entry for 4589 (#4744) --- CHANGELOG.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ab62499596..7f7b1f14d28 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -145,6 +145,7 @@ Re-release of 0.23.0 with fixes to docs.rs build. - Revert removal of private FFI function `_PyLong_NumBits` on Python 3.13 and later. [#4450](https://github.com/PyO3/pyo3/pull/4450) - Fix `__traverse__` functions for base classes not being called by subclasses created with `#[pyclass(extends = ...)]`. [#4563](https://github.com/PyO3/pyo3/pull/4563) - Fix regression in 0.22.3 failing compiles under `#![forbid(unsafe_code)]`. [#4574](https://github.com/PyO3/pyo3/pull/4574) +- Fix `create_exception` macro triggering lint and compile errors due to interaction with `gil-refs` feature. [#4589](https://github.com/PyO3/pyo3/pull/4589) - Workaround possible use-after-free in `_borrowed` methods on `PyWeakRef` and `PyWeakrefProxy` by leaking their contents. [#4590](https://github.com/PyO3/pyo3/pull/4590) - Fix crash calling `PyType_GetSlot` on static types before Python 3.10. [#4599](https://github.com/PyO3/pyo3/pull/4599) @@ -175,7 +176,7 @@ Re-release of 0.23.0 with fixes to docs.rs build. - Fix compile failure in declarative `#[pymodule]` under presence of `#![no_implicit_prelude]`. [#4328](https://github.com/PyO3/pyo3/pull/4328) - Fix use of borrowed reference in `PyDict::get_item` (unsafe in free-threaded Python). [#4355](https://github.com/PyO3/pyo3/pull/4355) - Fix `#[pyclass(eq)]` macro hygiene issues for structs and enums. [#4359](https://github.com/PyO3/pyo3/pull/4359) -- Fix hygiene/span issues of `'#[pyfunction]` and `#[pymethods]` generated code which affected expansion in `macro_rules` context. [#4382](https://github.com/PyO3/pyo3/pull/4382) +- Fix hygiene/span issues of `#[pyfunction]` and `#[pymethods]` generated code which affected expansion in `macro_rules` context. [#4382](https://github.com/PyO3/pyo3/pull/4382) - Fix `unsafe_code` lint error in `#[pyclass]` generated code. [#4396](https://github.com/PyO3/pyo3/pull/4396) - Fix async functions returning a tuple only returning the first element to Python. [#4407](https://github.com/PyO3/pyo3/pull/4407) - Fix use of borrowed reference in `PyList::get_item` (unsafe in free-threaded Python). [#4410](https://github.com/PyO3/pyo3/pull/4410) @@ -209,7 +210,7 @@ Re-release of 0.23.0 with fixes to docs.rs build. ### Fixed - Return `NotImplemented` instead of raising `TypeError` from generated equality method when comparing different types. [#4287](https://github.com/PyO3/pyo3/pull/4287) -- Handle full-path `#[pyo3::prelude::pymodule]` and similar for `#[pyclass]` and `#[pyfunction]` in declarative modules.[#4288](https://github.com/PyO3/pyo3/pull/4288) +- Handle full-path `#[pyo3::prelude::pymodule]` and similar for `#[pyclass]` and `#[pyfunction]` in declarative modules. [#4288](https://github.com/PyO3/pyo3/pull/4288) - Fix 128-bit int regression on big-endian platforms with Python <3.13. [#4291](https://github.com/PyO3/pyo3/pull/4291) - Stop generating code that will never be covered with declarative modules. [#4297](https://github.com/PyO3/pyo3/pull/4297) - Fix invalid deprecation warning for trailing optional on `#[setter]` function. [#4304](https://github.com/PyO3/pyo3/pull/4304) From 82ab509b3cbfbe2e848083396004bc0fd79ad6d3 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Fri, 29 Nov 2024 13:08:06 +0000 Subject: [PATCH 392/495] don't link to abi3 dll on windows for free-threaded build (#4733) * don't link to abi3 dll on windows for free-threaded build * newsfragment --- newsfragments/4733.fixed.md | 1 + pyo3-build-config/src/impl_.rs | 17 ++++++++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 newsfragments/4733.fixed.md diff --git a/newsfragments/4733.fixed.md b/newsfragments/4733.fixed.md new file mode 100644 index 00000000000..bcf3dbad823 --- /dev/null +++ b/newsfragments/4733.fixed.md @@ -0,0 +1 @@ +Fix unresolved symbol link failures (due to linking to wrong DLL) when compiling for Python 3.13t with `abi3` features enabled. diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index 3a0915b4c8e..30684344e39 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -1649,7 +1649,7 @@ fn default_lib_name_windows( // CPython bug: linking against python3_d.dll raises error // https://github.com/python/cpython/issues/101614 Ok(format!("python{}{}_d", version.major, version.minor)) - } else if abi3 && !(implementation.is_pypy() || implementation.is_graalpy()) { + } else if abi3 && !(gil_disabled || implementation.is_pypy() || implementation.is_graalpy()) { if debug { Ok(WINDOWS_ABI3_DEBUG_LIB_NAME.to_owned()) } else { @@ -2544,6 +2544,21 @@ mod tests { .unwrap(), "python313t", ); + assert_eq!( + super::default_lib_name_windows( + PythonVersion { + major: 3, + minor: 13 + }, + CPython, + true, // abi3 true should not affect the free-threaded lib name + false, + false, + true, + ) + .unwrap(), + "python313t", + ); assert_eq!( super::default_lib_name_windows( PythonVersion { From 5df4706a31a5f18116481f3754c13a0a183bfa73 Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Sat, 30 Nov 2024 15:06:27 +0100 Subject: [PATCH 393/495] map `io::ErrorKind::IsADirectory`/`NotADirectory` to Python on 1.83+ (#4747) --- newsfragments/4747.changed.md | 1 + pyo3-build-config/src/lib.rs | 5 +++++ src/err/impls.rs | 17 +++++++++++++++++ 3 files changed, 23 insertions(+) create mode 100644 newsfragments/4747.changed.md diff --git a/newsfragments/4747.changed.md b/newsfragments/4747.changed.md new file mode 100644 index 00000000000..ca04831d064 --- /dev/null +++ b/newsfragments/4747.changed.md @@ -0,0 +1 @@ +Map `io::ErrorKind::IsADirectory`/`NotADirectory` to the corresponding Python exception on Rust 1.83+ \ No newline at end of file diff --git a/pyo3-build-config/src/lib.rs b/pyo3-build-config/src/lib.rs index 642fdf1659f..554200040e4 100644 --- a/pyo3-build-config/src/lib.rs +++ b/pyo3-build-config/src/lib.rs @@ -156,6 +156,10 @@ pub fn print_feature_cfgs() { if rustc_minor_version >= 79 { println!("cargo:rustc-cfg=diagnostic_namespace"); } + + if rustc_minor_version >= 83 { + println!("cargo:rustc-cfg=io_error_more"); + } } /// Registers `pyo3`s config names as reachable cfg expressions @@ -180,6 +184,7 @@ pub fn print_expected_cfgs() { println!("cargo:rustc-check-cfg=cfg(diagnostic_namespace)"); println!("cargo:rustc-check-cfg=cfg(c_str_lit)"); println!("cargo:rustc-check-cfg=cfg(rustc_has_once_lock)"); + println!("cargo:rustc-check-cfg=cfg(io_error_more)"); // allow `Py_3_*` cfgs from the minimum supported version up to the // maximum minor version (+1 for development for the next) diff --git a/src/err/impls.rs b/src/err/impls.rs index b84f46d4306..1af45b7e628 100644 --- a/src/err/impls.rs +++ b/src/err/impls.rs @@ -27,6 +27,15 @@ impl From for io::Error { } else if err.is_instance_of::(py) { io::ErrorKind::TimedOut } else { + #[cfg(io_error_more)] + if err.is_instance_of::(py) { + io::ErrorKind::IsADirectory + } else if err.is_instance_of::(py) { + io::ErrorKind::NotADirectory + } else { + io::ErrorKind::Other + } + #[cfg(not(io_error_more))] io::ErrorKind::Other } }); @@ -54,6 +63,10 @@ impl From for PyErr { io::ErrorKind::AlreadyExists => exceptions::PyFileExistsError::new_err(err), io::ErrorKind::WouldBlock => exceptions::PyBlockingIOError::new_err(err), io::ErrorKind::TimedOut => exceptions::PyTimeoutError::new_err(err), + #[cfg(io_error_more)] + io::ErrorKind::IsADirectory => exceptions::PyIsADirectoryError::new_err(err), + #[cfg(io_error_more)] + io::ErrorKind::NotADirectory => exceptions::PyNotADirectoryError::new_err(err), _ => exceptions::PyOSError::new_err(err), } } @@ -167,5 +180,9 @@ mod tests { check_err(io::ErrorKind::AlreadyExists, "FileExistsError"); check_err(io::ErrorKind::WouldBlock, "BlockingIOError"); check_err(io::ErrorKind::TimedOut, "TimeoutError"); + #[cfg(io_error_more)] + check_err(io::ErrorKind::IsADirectory, "IsADirectoryError"); + #[cfg(io_error_more)] + check_err(io::ErrorKind::NotADirectory, "NotADirectoryError"); } } From 4f53f4a52a2607660e40d722ceb029dddd2d445a Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Sat, 30 Nov 2024 17:59:58 +0000 Subject: [PATCH 394/495] ci: test debug builds using 3.13.0 (#4750) --- .github/workflows/ci.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b0bf3ea4bab..5042ac9cee3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -520,8 +520,8 @@ jobs: components: rust-src - name: Install python3 standalone debug build with nox run: | - PBS_RELEASE="20231002" - PBS_PYTHON_VERSION="3.12.0" + PBS_RELEASE="20241016" + PBS_PYTHON_VERSION="3.13.0" PBS_ARCHIVE="cpython-${PBS_PYTHON_VERSION}+${PBS_RELEASE}-x86_64-unknown-linux-gnu-debug-full.tar.zst" wget "https://github.com/indygreg/python-build-standalone/releases/download/${PBS_RELEASE}/${PBS_ARCHIVE}" tar -I zstd -xf "${PBS_ARCHIVE}" @@ -537,10 +537,10 @@ jobs: PYO3_CONFIG_FILE=$(mktemp) cat > $PYO3_CONFIG_FILE << EOF implementation=CPython - version=3.12 + version=3.13 shared=true abi3=false - lib_name=python3.12d + lib_name=python3.13d lib_dir=${{ github.workspace }}/python/install/lib executable=${{ github.workspace }}/python/install/bin/python3 pointer_width=64 From 2ed3bb40c6cb17f2f7c63381043328ac3ad2f3e7 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Mon, 2 Dec 2024 04:32:26 +0000 Subject: [PATCH 395/495] support python3-dll-a free-threaded generation (#4749) --- newsfragments/4749.fixed.md | 1 + newsfragments/4749.packaging.md | 1 + pyo3-build-config/Cargo.toml | 4 ++-- pyo3-build-config/src/impl_.rs | 15 +++++++++++++-- pyo3-build-config/src/import_lib.rs | 2 ++ 5 files changed, 19 insertions(+), 4 deletions(-) create mode 100644 newsfragments/4749.fixed.md create mode 100644 newsfragments/4749.packaging.md diff --git a/newsfragments/4749.fixed.md b/newsfragments/4749.fixed.md new file mode 100644 index 00000000000..8d8923075f3 --- /dev/null +++ b/newsfragments/4749.fixed.md @@ -0,0 +1 @@ +Fix failure to link on Windows free-threaded Python when using the `generate-import-lib` feature. diff --git a/newsfragments/4749.packaging.md b/newsfragments/4749.packaging.md new file mode 100644 index 00000000000..a87524431d8 --- /dev/null +++ b/newsfragments/4749.packaging.md @@ -0,0 +1 @@ +Bump optional `python3-dll-a` dependency to 0.2.11. diff --git a/pyo3-build-config/Cargo.toml b/pyo3-build-config/Cargo.toml index bcf8b1de2a6..61478dc4d18 100644 --- a/pyo3-build-config/Cargo.toml +++ b/pyo3-build-config/Cargo.toml @@ -13,11 +13,11 @@ rust-version = "1.63" [dependencies] once_cell = "1" -python3-dll-a = { version = "0.2.6", optional = true } +python3-dll-a = { version = "0.2.11", optional = true } target-lexicon = "0.12.14" [build-dependencies] -python3-dll-a = { version = "0.2.6", optional = true } +python3-dll-a = { version = "0.2.11", optional = true } target-lexicon = "0.12.14" [features] diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index 30684344e39..bc97460a795 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -554,8 +554,17 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) if self.lib_dir.is_none() { let target = target_triple_from_env(); let py_version = if self.abi3 { None } else { Some(self.version) }; - self.lib_dir = - import_lib::generate_import_lib(&target, self.implementation, py_version)?; + let abiflags = if self.is_free_threaded() { + Some("t") + } else { + None + }; + self.lib_dir = import_lib::generate_import_lib( + &target, + self.implementation, + py_version, + abiflags, + )?; } Ok(()) } @@ -1521,6 +1530,7 @@ fn default_cross_compile(cross_compile_config: &CrossCompileConfig) -> Result Result { &host, interpreter_config.implementation, py_version, + None, )?; } diff --git a/pyo3-build-config/src/import_lib.rs b/pyo3-build-config/src/import_lib.rs index 0925a861b5b..ee934441f77 100644 --- a/pyo3-build-config/src/import_lib.rs +++ b/pyo3-build-config/src/import_lib.rs @@ -19,6 +19,7 @@ pub(super) fn generate_import_lib( target: &Triple, py_impl: PythonImplementation, py_version: Option, + abiflags: Option<&str>, ) -> Result> { if target.operating_system != OperatingSystem::Windows { return Ok(None); @@ -50,6 +51,7 @@ pub(super) fn generate_import_lib( ImportLibraryGenerator::new(&arch, &env) .version(py_version.map(|v| (v.major, v.minor))) .implementation(implementation) + .abiflags(abiflags) .generate(&out_lib_dir) .context("failed to generate python3.dll import library")?; From 6ec499b2bf266b207e16ccacbc6dc8ab4929bd54 Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Mon, 2 Dec 2024 15:42:40 +0100 Subject: [PATCH 396/495] fix another `unsafe_op_in_unsafe_fn` trigger (#4753) --- pyo3-macros-backend/src/params.rs | 3 ++- tests/ui/forbid_unsafe.rs | 12 ++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/pyo3-macros-backend/src/params.rs b/pyo3-macros-backend/src/params.rs index 9517d35b25c..f967149c725 100644 --- a/pyo3-macros-backend/src/params.rs +++ b/pyo3-macros-backend/src/params.rs @@ -262,9 +262,10 @@ pub(crate) fn impl_regular_arg_param( )? } } else { + let unwrap = quote! {unsafe { #pyo3_path::impl_::extract_argument::unwrap_required_argument(#arg_value) }}; quote_arg_span! { #pyo3_path::impl_::extract_argument::from_py_with( - #pyo3_path::impl_::extract_argument::unwrap_required_argument(#arg_value), + #unwrap, #name_str, #from_py_with as fn(_) -> _, )? diff --git a/tests/ui/forbid_unsafe.rs b/tests/ui/forbid_unsafe.rs index 660f5fa36c0..4ab7639d658 100644 --- a/tests/ui/forbid_unsafe.rs +++ b/tests/ui/forbid_unsafe.rs @@ -28,4 +28,16 @@ mod gh_4394 { pub struct Version; } +mod from_py_with { + use pyo3::prelude::*; + use pyo3::types::PyBytes; + + fn bytes_from_py(bytes: &Bound<'_, PyAny>) -> PyResult> { + Ok(bytes.downcast::()?.as_bytes().to_vec()) + } + + #[pyfunction] + fn f(#[pyo3(from_py_with = "bytes_from_py")] _bytes: Vec) {} +} + fn main() {} From 48436da870d379df01f7c7745bbc21149d6062b6 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Tue, 3 Dec 2024 20:44:51 +0000 Subject: [PATCH 397/495] fix `PYO3_CONFIG_FILE` env var not requesting rebuilds (#4758) * fix `PYO3_CONFIG_FILE` env var not requesting rebuilds * test and newsfragment * fix clippy * relax assertion --- newsfragments/4758.fixed.md | 1 + pyo3-build-config/src/impl_.rs | 26 +++++++++++++++++++++++--- 2 files changed, 24 insertions(+), 3 deletions(-) create mode 100644 newsfragments/4758.fixed.md diff --git a/newsfragments/4758.fixed.md b/newsfragments/4758.fixed.md new file mode 100644 index 00000000000..ef73abd966d --- /dev/null +++ b/newsfragments/4758.fixed.md @@ -0,0 +1 @@ +Fix compile-time regression in PyO3 0.23.0 where changing `PYO3_CONFIG_FILE` would not reconfigure PyO3 for the new interpreter. diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index bc97460a795..f36c5e50226 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -6,6 +6,8 @@ #[path = "import_lib.rs"] mod import_lib; +#[cfg(test)] +use std::cell::RefCell; use std::{ collections::{HashMap, HashSet}, env, @@ -15,8 +17,7 @@ use std::{ io::{BufRead, BufReader, Read, Write}, path::{Path, PathBuf}, process::{Command, Stdio}, - str, - str::FromStr, + str::{self, FromStr}, }; pub use target_lexicon::Triple; @@ -41,6 +42,11 @@ const MINIMUM_SUPPORTED_VERSION_GRAALPY: PythonVersion = PythonVersion { /// Maximum Python version that can be used as minimum required Python version with abi3. pub(crate) const ABI3_MAX_MINOR: u8 = 12; +#[cfg(test)] +thread_local! { + static READ_ENV_VARS: RefCell> = const { RefCell::new(Vec::new()) }; +} + /// Gets an environment variable owned by cargo. /// /// Environment variables set by cargo are expected to be valid UTF8. @@ -54,6 +60,12 @@ pub fn env_var(var: &str) -> Option { if cfg!(feature = "resolve-config") { println!("cargo:rerun-if-env-changed={}", var); } + #[cfg(test)] + { + READ_ENV_VARS.with(|env_vars| { + env_vars.borrow_mut().push(var.to_owned()); + }); + } env::var_os(var) } @@ -420,7 +432,7 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) /// The `abi3` features, if set, may apply an `abi3` constraint to the Python version. #[allow(dead_code)] // only used in build.rs pub(super) fn from_pyo3_config_file_env() -> Option> { - cargo_env_var("PYO3_CONFIG_FILE").map(|path| { + env_var("PYO3_CONFIG_FILE").map(|path| { let path = Path::new(&path); println!("cargo:rerun-if-changed={}", path.display()); // Absolute path is necessary because this build script is run with a cwd different to the @@ -3070,4 +3082,12 @@ mod tests { " )); } + + #[test] + fn test_from_pyo3_config_file_env_rebuild() { + READ_ENV_VARS.with(|vars| vars.borrow_mut().clear()); + let _ = InterpreterConfig::from_pyo3_config_file_env(); + // it's possible that other env vars were also read, hence just checking for contains + READ_ENV_VARS.with(|vars| assert!(vars.borrow().contains(&"PYO3_CONFIG_FILE".to_string()))); + } } From 77202aab7e9d0aded57b21bc592b51f6342078b0 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Wed, 4 Dec 2024 19:58:40 +0000 Subject: [PATCH 398/495] release: 0.23.3 (#4765) --- CHANGELOG.md | 16 +++++++++++++++- Cargo.toml | 8 ++++---- README.md | 4 ++-- 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/4733.fixed.md | 1 - newsfragments/4749.fixed.md | 1 - newsfragments/4749.packaging.md | 1 - newsfragments/4758.fixed.md | 1 - pyo3-build-config/Cargo.toml | 2 +- pyo3-ffi/Cargo.toml | 4 ++-- pyo3-ffi/README.md | 4 ++-- pyo3-macros-backend/Cargo.toml | 6 +++--- pyo3-macros/Cargo.toml | 4 ++-- pyproject.toml | 2 +- tests/ui/reject_generics.stderr | 4 ++-- 19 files changed, 39 insertions(+), 29 deletions(-) delete mode 100644 newsfragments/4733.fixed.md delete mode 100644 newsfragments/4749.fixed.md delete mode 100644 newsfragments/4749.packaging.md delete mode 100644 newsfragments/4758.fixed.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f7b1f14d28..668fbd23d43 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,19 @@ To see unreleased changes, please see the [CHANGELOG on the main branch guide](h +## [0.23.3] - 2024-12-03 + +### Packaging + +- Bump optional `python3-dll-a` dependency to 0.2.11. [#4749](https://github.com/PyO3/pyo3/pull/4749) + +### Fixed + +- Fix unresolved symbol link failures on Windows when compiling for Python 3.13t with `abi3` features enabled. [#4733](https://github.com/PyO3/pyo3/pull/4733) +- Fix unresolved symbol link failures on Windows when compiling for Python 3.13t using the `generate-import-lib` feature. [#4749](https://github.com/PyO3/pyo3/pull/4749) +- Fix compile-time regression in PyO3 0.23.0 where changing `PYO3_CONFIG_FILE` would not reconfigure PyO3 for the new interpreter. [#4758](https://github.com/PyO3/pyo3/pull/4758) + + ## [0.23.2] - 2024-11-25 ### Added @@ -2013,7 +2026,8 @@ Yanked - Initial release -[Unreleased]: https://github.com/pyo3/pyo3/compare/v0.23.2...HEAD +[Unreleased]: https://github.com/pyo3/pyo3/compare/v0.23.3...HEAD +[0.23.3]: https://github.com/pyo3/pyo3/compare/v0.23.2...v0.23.3 [0.23.2]: https://github.com/pyo3/pyo3/compare/v0.23.1...v0.23.2 [0.23.1]: https://github.com/pyo3/pyo3/compare/v0.23.0...v0.23.1 [0.23.0]: https://github.com/pyo3/pyo3/compare/v0.22.5...v0.23.0 diff --git a/Cargo.toml b/Cargo.toml index 778a8f7df2e..18cceffbd0c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3" -version = "0.23.2" +version = "0.23.3" description = "Bindings to Python interpreter" authors = ["PyO3 Project and Contributors "] readme = "README.md" @@ -21,10 +21,10 @@ memoffset = "0.9" once_cell = "1.13" # ffi bindings to the python interpreter, split into a separate crate so they can be used independently -pyo3-ffi = { path = "pyo3-ffi", version = "=0.23.2" } +pyo3-ffi = { path = "pyo3-ffi", version = "=0.23.3" } # support crates for macros feature -pyo3-macros = { path = "pyo3-macros", version = "=0.23.2", optional = true } +pyo3-macros = { path = "pyo3-macros", version = "=0.23.3", optional = true } indoc = { version = "2.0.1", optional = true } unindent = { version = "0.2.1", optional = true } @@ -66,7 +66,7 @@ static_assertions = "1.1.0" uuid = {version = "1.10.0", features = ["v4"] } [build-dependencies] -pyo3-build-config = { path = "pyo3-build-config", version = "=0.23.2", features = ["resolve-config"] } +pyo3-build-config = { path = "pyo3-build-config", version = "=0.23.3", features = ["resolve-config"] } [features] default = ["macros"] diff --git a/README.md b/README.md index 58c6ce17c9d..82b8d390981 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,7 @@ name = "string_sum" crate-type = ["cdylib"] [dependencies] -pyo3 = { version = "0.23.2", features = ["extension-module"] } +pyo3 = { version = "0.23.3", features = ["extension-module"] } ``` **`src/lib.rs`** @@ -140,7 +140,7 @@ Start a new project with `cargo new` and add `pyo3` to the `Cargo.toml` like th ```toml [dependencies.pyo3] -version = "0.23.2" +version = "0.23.3" features = ["auto-initialize"] ``` diff --git a/examples/decorator/.template/pre-script.rhai b/examples/decorator/.template/pre-script.rhai index 56353bbc3e2..aa83bdd81b7 100644 --- a/examples/decorator/.template/pre-script.rhai +++ b/examples/decorator/.template/pre-script.rhai @@ -1,4 +1,4 @@ -variable::set("PYO3_VERSION", "0.23.2"); +variable::set("PYO3_VERSION", "0.23.3"); 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 56353bbc3e2..aa83bdd81b7 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.23.2"); +variable::set("PYO3_VERSION", "0.23.3"); 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 d8bba8b5fa7..dd76eff3b7b 100644 --- a/examples/plugin/.template/pre-script.rhai +++ b/examples/plugin/.template/pre-script.rhai @@ -1,4 +1,4 @@ -variable::set("PYO3_VERSION", "0.23.2"); +variable::set("PYO3_VERSION", "0.23.3"); 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 4d41dacca0c..461282c2832 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.23.2"); +variable::set("PYO3_VERSION", "0.23.3"); 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 56353bbc3e2..aa83bdd81b7 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.23.2"); +variable::set("PYO3_VERSION", "0.23.3"); file::rename(".template/Cargo.toml", "Cargo.toml"); file::rename(".template/pyproject.toml", "pyproject.toml"); file::delete(".template"); diff --git a/newsfragments/4733.fixed.md b/newsfragments/4733.fixed.md deleted file mode 100644 index bcf3dbad823..00000000000 --- a/newsfragments/4733.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Fix unresolved symbol link failures (due to linking to wrong DLL) when compiling for Python 3.13t with `abi3` features enabled. diff --git a/newsfragments/4749.fixed.md b/newsfragments/4749.fixed.md deleted file mode 100644 index 8d8923075f3..00000000000 --- a/newsfragments/4749.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Fix failure to link on Windows free-threaded Python when using the `generate-import-lib` feature. diff --git a/newsfragments/4749.packaging.md b/newsfragments/4749.packaging.md deleted file mode 100644 index a87524431d8..00000000000 --- a/newsfragments/4749.packaging.md +++ /dev/null @@ -1 +0,0 @@ -Bump optional `python3-dll-a` dependency to 0.2.11. diff --git a/newsfragments/4758.fixed.md b/newsfragments/4758.fixed.md deleted file mode 100644 index ef73abd966d..00000000000 --- a/newsfragments/4758.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Fix compile-time regression in PyO3 0.23.0 where changing `PYO3_CONFIG_FILE` would not reconfigure PyO3 for the new interpreter. diff --git a/pyo3-build-config/Cargo.toml b/pyo3-build-config/Cargo.toml index 61478dc4d18..1e951b29bff 100644 --- a/pyo3-build-config/Cargo.toml +++ b/pyo3-build-config/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3-build-config" -version = "0.23.2" +version = "0.23.3" 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 1a8cd7f09d7..ddbb489e2b9 100644 --- a/pyo3-ffi/Cargo.toml +++ b/pyo3-ffi/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3-ffi" -version = "0.23.2" +version = "0.23.3" description = "Python-API bindings for the PyO3 ecosystem" authors = ["PyO3 Project and Contributors "] keywords = ["pyo3", "python", "cpython", "ffi"] @@ -42,7 +42,7 @@ generate-import-lib = ["pyo3-build-config/python3-dll-a"] paste = "1" [build-dependencies] -pyo3-build-config = { path = "../pyo3-build-config", version = "=0.23.2", features = ["resolve-config"] } +pyo3-build-config = { path = "../pyo3-build-config", version = "=0.23.3", features = ["resolve-config"] } [lints] workspace = true diff --git a/pyo3-ffi/README.md b/pyo3-ffi/README.md index d80dad93b3d..5ae73122501 100644 --- a/pyo3-ffi/README.md +++ b/pyo3-ffi/README.md @@ -41,13 +41,13 @@ name = "string_sum" crate-type = ["cdylib"] [dependencies.pyo3-ffi] -version = "0.23.2" +version = "0.23.3" features = ["extension-module"] [build-dependencies] # This is only necessary if you need to configure your build based on # the Python version or the compile-time configuration for the interpreter. -pyo3_build_config = "0.23.2" +pyo3_build_config = "0.23.3" ``` If you need to use conditional compilation based on Python version or how diff --git a/pyo3-macros-backend/Cargo.toml b/pyo3-macros-backend/Cargo.toml index 03c2ed0b189..38ec968d71d 100644 --- a/pyo3-macros-backend/Cargo.toml +++ b/pyo3-macros-backend/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3-macros-backend" -version = "0.23.2" +version = "0.23.3" description = "Code generation for PyO3 package" authors = ["PyO3 Project and Contributors "] keywords = ["pyo3", "python", "cpython", "ffi"] @@ -17,7 +17,7 @@ rust-version = "1.63" [dependencies] heck = "0.5" proc-macro2 = { version = "1.0.60", default-features = false } -pyo3-build-config = { path = "../pyo3-build-config", version = "=0.23.2", features = ["resolve-config"] } +pyo3-build-config = { path = "../pyo3-build-config", version = "=0.23.3", features = ["resolve-config"] } quote = { version = "1", default-features = false } [dependencies.syn] @@ -26,7 +26,7 @@ default-features = false features = ["derive", "parsing", "printing", "clone-impls", "full", "extra-traits"] [build-dependencies] -pyo3-build-config = { path = "../pyo3-build-config", version = "=0.23.2" } +pyo3-build-config = { path = "../pyo3-build-config", version = "=0.23.3" } [lints] workspace = true diff --git a/pyo3-macros/Cargo.toml b/pyo3-macros/Cargo.toml index b85cc7e03c6..a44758b37f5 100644 --- a/pyo3-macros/Cargo.toml +++ b/pyo3-macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3-macros" -version = "0.23.2" +version = "0.23.3" description = "Proc macros for PyO3 package" authors = ["PyO3 Project and Contributors "] keywords = ["pyo3", "python", "cpython", "ffi"] @@ -22,7 +22,7 @@ experimental-async = ["pyo3-macros-backend/experimental-async"] proc-macro2 = { version = "1.0.60", default-features = false } quote = "1" syn = { version = "2", features = ["full", "extra-traits"] } -pyo3-macros-backend = { path = "../pyo3-macros-backend", version = "=0.23.2" } +pyo3-macros-backend = { path = "../pyo3-macros-backend", version = "=0.23.3" } [lints] workspace = true diff --git a/pyproject.toml b/pyproject.toml index e3a55bb6d66..771e6f8a045 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ [tool.towncrier] filename = "CHANGELOG.md" -version = "0.23.2" +version = "0.23.3" start_string = "\n" template = ".towncrier.template.md" title_format = "## [{version}] - {project_date}" diff --git a/tests/ui/reject_generics.stderr b/tests/ui/reject_generics.stderr index 1a811ddafb2..47999f36275 100644 --- a/tests/ui/reject_generics.stderr +++ b/tests/ui/reject_generics.stderr @@ -1,10 +1,10 @@ -error: #[pyclass] cannot have generic parameters. For an explanation, see https://pyo3.rs/v0.23.2/class.html#no-generic-parameters +error: #[pyclass] cannot have generic parameters. For an explanation, see https://pyo3.rs/v0.23.3/class.html#no-generic-parameters --> tests/ui/reject_generics.rs:4:25 | 4 | struct ClassWithGenerics { | ^ -error: #[pyclass] cannot have lifetime parameters. For an explanation, see https://pyo3.rs/v0.23.2/class.html#no-lifetime-parameters +error: #[pyclass] cannot have lifetime parameters. For an explanation, see https://pyo3.rs/v0.23.3/class.html#no-lifetime-parameters --> tests/ui/reject_generics.rs:9:27 | 9 | struct ClassWithLifetimes<'a> { From 5aa2a9b0db641c824c0148fc46bc1e1735b870d2 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Fri, 6 Dec 2024 11:00:06 +0000 Subject: [PATCH 399/495] eagerly complete once in normalized error state (#4766) * eagerly complete once in normalized error state * newsfragment --- newsfragments/4766.fixed.md | 1 + src/err/err_state.rs | 8 +++++++- 2 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 newsfragments/4766.fixed.md diff --git a/newsfragments/4766.fixed.md b/newsfragments/4766.fixed.md new file mode 100644 index 00000000000..3f69e5d5f63 --- /dev/null +++ b/newsfragments/4766.fixed.md @@ -0,0 +1 @@ +Fix unnecessary internal `py.allow_threads` GIL-switch when attempting to access contents of a `PyErr` which originated from Python (could lead to unintended deadlocks). diff --git a/src/err/err_state.rs b/src/err/err_state.rs index 3b9e9800b6e..98be633e91c 100644 --- a/src/err/err_state.rs +++ b/src/err/err_state.rs @@ -43,7 +43,13 @@ impl PyErrState { } pub(crate) fn normalized(normalized: PyErrStateNormalized) -> Self { - Self::from_inner(PyErrStateInner::Normalized(normalized)) + let state = Self::from_inner(PyErrStateInner::Normalized(normalized)); + // This state is already normalized, by completing the Once immediately we avoid + // reaching the `py.allow_threads` in `make_normalized` which is less efficient + // and introduces a GIL switch which could deadlock. + // See https://github.com/PyO3/pyo3/issues/4764 + state.normalized.call_once(|| {}); + state } pub(crate) fn restore(self, py: Python<'_>) { From 7077ada4b06b0ad9bc7647981be42b5d76258d04 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Fri, 6 Dec 2024 15:31:29 -0500 Subject: [PATCH 400/495] Avoid interpolating values into bash (#4774) This can lead to code execution. See https://woodruffw.github.io/zizmor/audits/#template-injection for details --- .github/workflows/coverage-pr-base.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/coverage-pr-base.yml b/.github/workflows/coverage-pr-base.yml index acc645ea876..034d3bb1c6a 100644 --- a/.github/workflows/coverage-pr-base.yml +++ b/.github/workflows/coverage-pr-base.yml @@ -19,9 +19,6 @@ jobs: - name: Set PR base on codecov run: | # fetch the merge commit between the PR base and head - BASE_REF=refs/heads/${{ github.event.pull_request.base.ref }} - MERGE_REF=refs/pull/${{ github.event.pull_request.number }}/merge - git fetch -u --progress --depth=1 origin "+$BASE_REF:$BASE_REF" "+$MERGE_REF:$MERGE_REF" while [ -z "$(git merge-base "$BASE_REF" "$MERGE_REF")" ]; do git fetch -u -q --deepen="10" origin "$BASE_REF" "$MERGE_REF"; @@ -38,3 +35,8 @@ jobs: --slug PyO3/pyo3 \ --token ${{ secrets.CODECOV_TOKEN }} \ --service github + env: + # Don't put these in bash, because we don't want the expansion to + # risk code execution + BASE_REF: "refs/heads/{{ github.event.pull_request.base.ref }}" + MERGE_REF: "refs/pull/${{ github.event.pull_request.number }}/merge" From 6b4dc789db7389588916235e0fa23244f2f155db Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Fri, 6 Dec 2024 23:02:16 +0100 Subject: [PATCH 401/495] fix lints in `pyo3-benches` (#4770) --- pyo3-benches/benches/bench_call.rs | 28 +++++++++++----------- pyo3-benches/benches/bench_extract.rs | 10 ++++---- pyo3-benches/benches/bench_intopyobject.rs | 12 ++++++---- pyo3-benches/benches/bench_pyobject.rs | 10 ++------ pyo3-benches/benches/bench_set.rs | 18 +++++--------- pyo3-benches/benches/bench_tuple.rs | 12 ++++++++++ 6 files changed, 46 insertions(+), 44 deletions(-) diff --git a/pyo3-benches/benches/bench_call.rs b/pyo3-benches/benches/bench_call.rs index bbcf3ac2bdf..b6e090a7dbd 100644 --- a/pyo3-benches/benches/bench_call.rs +++ b/pyo3-benches/benches/bench_call.rs @@ -33,9 +33,9 @@ fn bench_call_1(b: &mut Bencher<'_>) { let foo_module = &module.getattr("foo").unwrap(); let args = ( - <_ as IntoPy>::into_py(1, py).into_bound(py), - <_ as IntoPy>::into_py("s", py).into_bound(py), - <_ as IntoPy>::into_py(1.23, py).into_bound(py), + 1.into_pyobject(py).unwrap(), + "s".into_pyobject(py).unwrap(), + 1.23.into_pyobject(py).unwrap(), ); b.iter(|| { @@ -52,9 +52,9 @@ fn bench_call(b: &mut Bencher<'_>) { let foo_module = &module.getattr("foo").unwrap(); let args = ( - <_ as IntoPy>::into_py(1, py).into_bound(py), - <_ as IntoPy>::into_py("s", py).into_bound(py), - <_ as IntoPy>::into_py(1.23, py).into_bound(py), + 1.into_pyobject(py).unwrap(), + "s".into_pyobject(py).unwrap(), + 1.23.into_pyobject(py).unwrap(), ); let kwargs = [("d", 1), ("e", 42)].into_py_dict(py).unwrap(); @@ -73,7 +73,7 @@ fn bench_call_one_arg(b: &mut Bencher<'_>) { let module = test_module!(py, "def foo(a): pass"); let foo_module = &module.getattr("foo").unwrap(); - let arg = <_ as IntoPy>::into_py(1, py).into_bound(py); + let arg = 1i32.into_pyobject(py).unwrap(); b.iter(|| { for _ in 0..1000 { @@ -117,9 +117,9 @@ class Foo: let foo_module = &module.getattr("Foo").unwrap().call0().unwrap(); let args = ( - <_ as IntoPy>::into_py(1, py).into_bound(py), - <_ as IntoPy>::into_py("s", py).into_bound(py), - <_ as IntoPy>::into_py(1.23, py).into_bound(py), + 1.into_pyobject(py).unwrap(), + "s".into_pyobject(py).unwrap(), + 1.23.into_pyobject(py).unwrap(), ); b.iter(|| { @@ -145,9 +145,9 @@ class Foo: let foo_module = &module.getattr("Foo").unwrap().call0().unwrap(); let args = ( - <_ as IntoPy>::into_py(1, py).into_bound(py), - <_ as IntoPy>::into_py("s", py).into_bound(py), - <_ as IntoPy>::into_py(1.23, py).into_bound(py), + 1.into_pyobject(py).unwrap(), + "s".into_pyobject(py).unwrap(), + 1.23.into_pyobject(py).unwrap(), ); let kwargs = [("d", 1), ("e", 42)].into_py_dict(py).unwrap(); @@ -173,7 +173,7 @@ class Foo: ); let foo_module = &module.getattr("Foo").unwrap().call0().unwrap(); - let arg = <_ as IntoPy>::into_py(1, py).into_bound(py); + let arg = 1i32.into_pyobject(py).unwrap(); b.iter(|| { for _ in 0..1000 { diff --git a/pyo3-benches/benches/bench_extract.rs b/pyo3-benches/benches/bench_extract.rs index a261b14cfa1..2062ccba7b5 100644 --- a/pyo3-benches/benches/bench_extract.rs +++ b/pyo3-benches/benches/bench_extract.rs @@ -26,7 +26,7 @@ fn extract_str_extract_fail(bench: &mut Bencher<'_>) { }); } -#[cfg(any(Py_3_10))] +#[cfg(Py_3_10)] fn extract_str_downcast_success(bench: &mut Bencher<'_>) { Python::with_gil(|py| { let s = PyString::new(py, "Hello, World!").into_any(); @@ -51,7 +51,7 @@ fn extract_str_downcast_fail(bench: &mut Bencher<'_>) { fn extract_int_extract_success(bench: &mut Bencher<'_>) { Python::with_gil(|py| { - let int = 123.to_object(py).into_bound(py); + let int = 123i32.into_pyobject(py).unwrap(); bench.iter(|| black_box(&int).extract::().unwrap()); }); @@ -70,7 +70,7 @@ fn extract_int_extract_fail(bench: &mut Bencher<'_>) { fn extract_int_downcast_success(bench: &mut Bencher<'_>) { Python::with_gil(|py| { - let int = 123.to_object(py).into_bound(py); + let int = 123i32.into_pyobject(py).unwrap(); bench.iter(|| { let py_int = black_box(&int).downcast::().unwrap(); @@ -92,7 +92,7 @@ fn extract_int_downcast_fail(bench: &mut Bencher<'_>) { fn extract_float_extract_success(bench: &mut Bencher<'_>) { Python::with_gil(|py| { - let float = 23.42.to_object(py).into_bound(py); + let float = 23.42f64.into_pyobject(py).unwrap(); bench.iter(|| black_box(&float).extract::().unwrap()); }); @@ -111,7 +111,7 @@ fn extract_float_extract_fail(bench: &mut Bencher<'_>) { fn extract_float_downcast_success(bench: &mut Bencher<'_>) { Python::with_gil(|py| { - let float = 23.42.to_object(py).into_bound(py); + let float = 23.42f64.into_pyobject(py).unwrap(); bench.iter(|| { let py_float = black_box(&float).downcast::().unwrap(); diff --git a/pyo3-benches/benches/bench_intopyobject.rs b/pyo3-benches/benches/bench_intopyobject.rs index 16351c9088b..42af893cd8a 100644 --- a/pyo3-benches/benches/bench_intopyobject.rs +++ b/pyo3-benches/benches/bench_intopyobject.rs @@ -17,7 +17,7 @@ fn bytes_new_small(b: &mut Bencher<'_>) { } fn bytes_new_medium(b: &mut Bencher<'_>) { - let data = (0..u8::MAX).into_iter().collect::>(); + let data = (0..u8::MAX).collect::>(); bench_bytes_new(b, &data); } @@ -37,7 +37,7 @@ fn byte_slice_into_pyobject_small(b: &mut Bencher<'_>) { } fn byte_slice_into_pyobject_medium(b: &mut Bencher<'_>) { - let data = (0..u8::MAX).into_iter().collect::>(); + let data = (0..u8::MAX).collect::>(); bench_bytes_into_pyobject(b, &data); } @@ -46,9 +46,10 @@ fn byte_slice_into_pyobject_large(b: &mut Bencher<'_>) { bench_bytes_into_pyobject(b, &data); } +#[allow(deprecated)] fn byte_slice_into_py(b: &mut Bencher<'_>) { Python::with_gil(|py| { - let data = (0..u8::MAX).into_iter().collect::>(); + let data = (0..u8::MAX).collect::>(); let bytes = data.as_slice(); b.iter_with_large_drop(|| black_box(bytes).into_py(py)); }); @@ -56,14 +57,15 @@ fn byte_slice_into_py(b: &mut Bencher<'_>) { fn vec_into_pyobject(b: &mut Bencher<'_>) { Python::with_gil(|py| { - let bytes = (0..u8::MAX).into_iter().collect::>(); + let bytes = (0..u8::MAX).collect::>(); b.iter_with_large_drop(|| black_box(&bytes).clone().into_pyobject(py)); }); } +#[allow(deprecated)] fn vec_into_py(b: &mut Bencher<'_>) { Python::with_gil(|py| { - let bytes = (0..u8::MAX).into_iter().collect::>(); + let bytes = (0..u8::MAX).collect::>(); b.iter_with_large_drop(|| black_box(&bytes).clone().into_py(py)); }); } diff --git a/pyo3-benches/benches/bench_pyobject.rs b/pyo3-benches/benches/bench_pyobject.rs index a57a98a8d30..c731216c79f 100644 --- a/pyo3-benches/benches/bench_pyobject.rs +++ b/pyo3-benches/benches/bench_pyobject.rs @@ -22,13 +22,7 @@ fn drop_many_objects(b: &mut Bencher<'_>) { fn drop_many_objects_without_gil(b: &mut Bencher<'_>) { b.iter_batched( - || { - Python::with_gil(|py| { - (0..1000) - .map(|_| py.None().into_py(py)) - .collect::>() - }) - }, + || Python::with_gil(|py| (0..1000).map(|_| py.None()).collect::>()), |objs| { drop(objs); @@ -76,7 +70,7 @@ fn drop_many_objects_multiple_threads(b: &mut Bencher<'_>) { for sender in &sender { let objs = Python::with_gil(|py| { (0..1000 / THREADS) - .map(|_| py.None().into_py(py)) + .map(|_| py.None()) .collect::>() }); diff --git a/pyo3-benches/benches/bench_set.rs b/pyo3-benches/benches/bench_set.rs index 0be06309595..2d468740ea0 100644 --- a/pyo3-benches/benches/bench_set.rs +++ b/pyo3-benches/benches/bench_set.rs @@ -1,7 +1,7 @@ use codspeed_criterion_compat::{criterion_group, criterion_main, Bencher, Criterion}; -use pyo3::prelude::*; use pyo3::types::PySet; +use pyo3::{prelude::*, IntoPyObjectExt}; use std::{ collections::{BTreeSet, HashSet}, hint::black_box, @@ -12,7 +12,7 @@ fn set_new(b: &mut Bencher<'_>) { const LEN: usize = 100_000; // Create Python objects up-front, so that the benchmark doesn't need to include // the cost of allocating LEN Python integers - let elements: Vec = (0..LEN).map(|i| i.into_py(py)).collect(); + let elements: Vec = (0..LEN).map(|i| i.into_py_any(py).unwrap()).collect(); b.iter_with_large_drop(|| PySet::new(py, &elements).unwrap()); }); } @@ -20,7 +20,7 @@ fn set_new(b: &mut Bencher<'_>) { fn iter_set(b: &mut Bencher<'_>) { Python::with_gil(|py| { const LEN: usize = 100_000; - let set = PySet::new(py, &(0..LEN).collect::>()).unwrap(); + let set = PySet::new(py, 0..LEN).unwrap(); let mut sum = 0; b.iter(|| { for x in &set { @@ -34,9 +34,7 @@ fn iter_set(b: &mut Bencher<'_>) { fn extract_hashset(b: &mut Bencher<'_>) { Python::with_gil(|py| { const LEN: usize = 100_000; - let any = PySet::new(py, &(0..LEN).collect::>()) - .unwrap() - .into_any(); + let any = PySet::new(py, 0..LEN).unwrap().into_any(); b.iter_with_large_drop(|| black_box(&any).extract::>()); }); } @@ -44,9 +42,7 @@ fn extract_hashset(b: &mut Bencher<'_>) { fn extract_btreeset(b: &mut Bencher<'_>) { Python::with_gil(|py| { const LEN: usize = 100_000; - let any = PySet::new(py, &(0..LEN).collect::>()) - .unwrap() - .into_any(); + let any = PySet::new(py, 0..LEN).unwrap().into_any(); b.iter_with_large_drop(|| black_box(&any).extract::>()); }); } @@ -54,9 +50,7 @@ fn extract_btreeset(b: &mut Bencher<'_>) { fn extract_hashbrown_set(b: &mut Bencher<'_>) { Python::with_gil(|py| { const LEN: usize = 100_000; - let any = PySet::new(py, &(0..LEN).collect::>()) - .unwrap() - .into_any(); + let any = PySet::new(py, 0..LEN).unwrap().into_any(); b.iter_with_large_drop(|| black_box(&any).extract::>()); }); } diff --git a/pyo3-benches/benches/bench_tuple.rs b/pyo3-benches/benches/bench_tuple.rs index a5e43d6ef43..72146c80b54 100644 --- a/pyo3-benches/benches/bench_tuple.rs +++ b/pyo3-benches/benches/bench_tuple.rs @@ -115,12 +115,23 @@ fn tuple_to_list(b: &mut Bencher<'_>) { }); } +#[allow(deprecated)] fn tuple_into_py(b: &mut Bencher<'_>) { Python::with_gil(|py| { b.iter(|| -> PyObject { (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12).into_py(py) }); }); } +fn tuple_into_pyobject(b: &mut Bencher<'_>) { + Python::with_gil(|py| { + b.iter(|| { + (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12) + .into_pyobject(py) + .unwrap() + }); + }); +} + fn criterion_benchmark(c: &mut Criterion) { c.bench_function("iter_tuple", iter_tuple); c.bench_function("tuple_new", tuple_new); @@ -137,6 +148,7 @@ fn criterion_benchmark(c: &mut Criterion) { c.bench_function("tuple_new_list", tuple_new_list); c.bench_function("tuple_to_list", tuple_to_list); c.bench_function("tuple_into_py", tuple_into_py); + c.bench_function("tuple_into_pyobject", tuple_into_pyobject); } criterion_group!(benches, criterion_benchmark); From 992865b2c3c4dffe9cd1c4894f1573682c3300de Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Sun, 8 Dec 2024 22:26:00 +0100 Subject: [PATCH 402/495] ci: fix coverage-pr-base `BASE_REF` expansion (#4779) --- .github/workflows/coverage-pr-base.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/coverage-pr-base.yml b/.github/workflows/coverage-pr-base.yml index 034d3bb1c6a..33118697636 100644 --- a/.github/workflows/coverage-pr-base.yml +++ b/.github/workflows/coverage-pr-base.yml @@ -38,5 +38,5 @@ jobs: env: # Don't put these in bash, because we don't want the expansion to # risk code execution - BASE_REF: "refs/heads/{{ github.event.pull_request.base.ref }}" + BASE_REF: "refs/heads/${{ github.event.pull_request.base.ref }}" MERGE_REF: "refs/pull/${{ github.event.pull_request.number }}/merge" From 926f5cf8e427d08b0a4670d89e3b0f342c299489 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers <7943856+bschoenmaeckers@users.noreply.github.com> Date: Wed, 11 Dec 2024 18:33:39 +0100 Subject: [PATCH 403/495] Fix chrono deprecation warning (#4785) * Fix chrono deprecation warning * use allow(deprecated) instead --------- Co-authored-by: Nathan Goldbaum --- src/conversions/chrono.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/conversions/chrono.rs b/src/conversions/chrono.rs index 90f9c69761d..bdee4934409 100644 --- a/src/conversions/chrono.rs +++ b/src/conversions/chrono.rs @@ -971,8 +971,12 @@ mod tests { // Also check that trying to convert an out of bound value errors. Python::with_gil(|py| { - assert!(Duration::min_value().into_pyobject(py).is_err()); - assert!(Duration::max_value().into_pyobject(py).is_err()); + // min_value and max_value were deprecated in chrono 0.4.39 + #[allow(deprecated)] + { + assert!(Duration::min_value().into_pyobject(py).is_err()); + assert!(Duration::max_value().into_pyobject(py).is_err()); + } }); } From 0777c6e68557001cc348421cb27be01b85e83fcb Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers <7943856+bschoenmaeckers@users.noreply.github.com> Date: Thu, 12 Dec 2024 19:20:13 +0100 Subject: [PATCH 404/495] fix `chrono::DateTime` intoPyObject conversion (#4790) * fix `chrono::DateTime` intoPyObject conversion * Add test --- newsfragments/4790.fixed.md | 1 + src/conversions/chrono.rs | 61 +++++++++++++++++++++++++++++++++---- 2 files changed, 56 insertions(+), 6 deletions(-) create mode 100644 newsfragments/4790.fixed.md diff --git a/newsfragments/4790.fixed.md b/newsfragments/4790.fixed.md new file mode 100644 index 00000000000..9b5e1bf60f1 --- /dev/null +++ b/newsfragments/4790.fixed.md @@ -0,0 +1 @@ +fix chrono::DateTime intoPyObject conversion when `Tz` is `chrono_tz::Tz` diff --git a/src/conversions/chrono.rs b/src/conversions/chrono.rs index bdee4934409..02bc65e59c5 100644 --- a/src/conversions/chrono.rs +++ b/src/conversions/chrono.rs @@ -54,7 +54,7 @@ use crate::types::{ timezone_utc, PyDate, PyDateAccess, PyDateTime, PyDelta, PyDeltaAccess, PyTime, PyTimeAccess, PyTzInfo, PyTzInfoAccess, }; -use crate::{ffi, Bound, FromPyObject, PyAny, PyErr, PyObject, PyResult, Python}; +use crate::{ffi, Bound, FromPyObject, IntoPyObjectExt, PyAny, PyErr, PyObject, PyResult, Python}; #[cfg(Py_LIMITED_API)] use crate::{intern, DowncastError}; #[allow(deprecated)] @@ -418,11 +418,14 @@ impl ToPyObject for DateTime { #[allow(deprecated)] impl IntoPy for DateTime { fn into_py(self, py: Python<'_>) -> PyObject { - self.into_pyobject(py).unwrap().into_any().unbind() + self.to_object(py) } } -impl<'py, Tz: TimeZone> IntoPyObject<'py> for DateTime { +impl<'py, Tz: TimeZone> IntoPyObject<'py> for DateTime +where + Tz: IntoPyObject<'py>, +{ #[cfg(Py_LIMITED_API)] type Target = PyAny; #[cfg(not(Py_LIMITED_API))] @@ -436,7 +439,10 @@ impl<'py, Tz: TimeZone> IntoPyObject<'py> for DateTime { } } -impl<'py, Tz: TimeZone> IntoPyObject<'py> for &DateTime { +impl<'py, Tz: TimeZone> IntoPyObject<'py> for &DateTime +where + Tz: IntoPyObject<'py>, +{ #[cfg(Py_LIMITED_API)] type Target = PyAny; #[cfg(not(Py_LIMITED_API))] @@ -445,7 +451,11 @@ impl<'py, Tz: TimeZone> IntoPyObject<'py> for &DateTime { type Error = PyErr; fn into_pyobject(self, py: Python<'py>) -> Result { - let tz = self.offset().fix().into_pyobject(py)?; + let tz = self.timezone().into_bound_py_any(py)?; + + #[cfg(not(Py_LIMITED_API))] + let tz = tz.downcast()?; + let DateArgs { year, month, day } = (&self.naive_local().date()).into(); let TimeArgs { hour, @@ -456,7 +466,7 @@ impl<'py, Tz: TimeZone> IntoPyObject<'py> for &DateTime { } = (&self.naive_local().time()).into(); #[cfg(not(Py_LIMITED_API))] - let datetime = PyDateTime::new(py, year, month, day, hour, min, sec, micro, Some(&tz))?; + let datetime = PyDateTime::new(py, year, month, day, hour, min, sec, micro, Some(tz))?; #[cfg(Py_LIMITED_API)] let datetime = DatetimeTypes::try_get(py).and_then(|dt| { @@ -1174,6 +1184,35 @@ mod tests { }) } + #[test] + #[cfg(all(Py_3_9, feature = "chrono-tz", not(windows)))] + fn test_pyo3_datetime_into_pyobject_tz() { + Python::with_gil(|py| { + let datetime = NaiveDate::from_ymd_opt(2024, 12, 11) + .unwrap() + .and_hms_opt(23, 3, 13) + .unwrap() + .and_local_timezone(chrono_tz::Tz::Europe__London) + .unwrap(); + let datetime = datetime.into_pyobject(py).unwrap(); + let py_datetime = new_py_datetime_ob( + py, + "datetime", + ( + 2024, + 12, + 11, + 23, + 3, + 13, + 0, + python_zoneinfo(py, "Europe/London"), + ), + ); + assert_eq!(datetime.compare(&py_datetime).unwrap(), Ordering::Equal); + }) + } + #[test] fn test_pyo3_datetime_frompyobject_utc() { Python::with_gil(|py| { @@ -1377,6 +1416,16 @@ mod tests { .unwrap() } + #[cfg(all(Py_3_9, feature = "chrono-tz", not(windows)))] + fn python_zoneinfo<'py>(py: Python<'py>, timezone: &str) -> Bound<'py, PyAny> { + py.import("zoneinfo") + .unwrap() + .getattr("ZoneInfo") + .unwrap() + .call1((timezone,)) + .unwrap() + } + #[cfg(not(any(target_arch = "wasm32", Py_GIL_DISABLED)))] mod proptests { use super::*; From 603a55f204e35b200a988168864e44ee7600310e Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Thu, 12 Dec 2024 19:52:03 +0100 Subject: [PATCH 405/495] fix `#[pyclass]` could not be named `Probe` (#4794) --- newsfragments/4794.fixed.md | 1 + pyo3-macros-backend/src/pyclass.rs | 5 ++--- tests/test_compile_error.rs | 1 + tests/ui/pyclass_probe.rs | 6 ++++++ tests/ui/pyclass_send.stderr | 4 ++-- 5 files changed, 12 insertions(+), 5 deletions(-) create mode 100644 newsfragments/4794.fixed.md create mode 100644 tests/ui/pyclass_probe.rs diff --git a/newsfragments/4794.fixed.md b/newsfragments/4794.fixed.md new file mode 100644 index 00000000000..9076234a5b3 --- /dev/null +++ b/newsfragments/4794.fixed.md @@ -0,0 +1 @@ +fix `#[pyclass]` could not be named `Probe` \ No newline at end of file diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index 3ac3fa358db..914cd19c1a0 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -2294,10 +2294,9 @@ impl<'a> PyClassImplsBuilder<'a> { let assertions = if attr.options.unsendable.is_some() { TokenStream::new() } else { - let assert = quote_spanned! { cls.span() => assert_pyclass_sync::<#cls>(); }; + let assert = quote_spanned! { cls.span() => #pyo3_path::impl_::pyclass::assert_pyclass_sync::<#cls>(); }; quote! { const _: () = { - use #pyo3_path::impl_::pyclass::*; #assert }; } @@ -2337,7 +2336,7 @@ impl<'a> PyClassImplsBuilder<'a> { static DOC: #pyo3_path::sync::GILOnceCell<::std::borrow::Cow<'static, ::std::ffi::CStr>> = #pyo3_path::sync::GILOnceCell::new(); DOC.get_or_try_init(py, || { let collector = PyClassImplCollector::::new(); - build_pyclass_doc(<#cls as #pyo3_path::PyTypeInfo>::NAME, #doc, collector.new_text_signature()) + build_pyclass_doc(::NAME, #doc, collector.new_text_signature()) }).map(::std::ops::Deref::deref) } diff --git a/tests/test_compile_error.rs b/tests/test_compile_error.rs index b165c911735..05d9ccd6d2e 100644 --- a/tests/test_compile_error.rs +++ b/tests/test_compile_error.rs @@ -67,4 +67,5 @@ fn test_compile_errors() { #[cfg(all(not(Py_LIMITED_API), Py_3_11))] t.compile_fail("tests/ui/invalid_base_class.rs"); t.pass("tests/ui/ambiguous_associated_items.rs"); + t.pass("tests/ui/pyclass_probe.rs"); } diff --git a/tests/ui/pyclass_probe.rs b/tests/ui/pyclass_probe.rs new file mode 100644 index 00000000000..590af194f6f --- /dev/null +++ b/tests/ui/pyclass_probe.rs @@ -0,0 +1,6 @@ +use pyo3::prelude::*; + +#[pyclass] +pub struct Probe {} + +fn main() {} diff --git a/tests/ui/pyclass_send.stderr b/tests/ui/pyclass_send.stderr index 3ef9591f820..516df060b13 100644 --- a/tests/ui/pyclass_send.stderr +++ b/tests/ui/pyclass_send.stderr @@ -10,7 +10,7 @@ note: required because it appears within the type `NotSyncNotSend` | 5 | struct NotSyncNotSend(*mut c_void); | ^^^^^^^^^^^^^^ -note: required by a bound in `pyo3::impl_::pyclass::assertions::assert_pyclass_sync` +note: required by a bound in `assert_pyclass_sync` --> src/impl_/pyclass/assertions.rs | | pub const fn assert_pyclass_sync() @@ -52,7 +52,7 @@ note: required because it appears within the type `SendNotSync` | 8 | struct SendNotSync(*mut c_void); | ^^^^^^^^^^^ -note: required by a bound in `pyo3::impl_::pyclass::assertions::assert_pyclass_sync` +note: required by a bound in `assert_pyclass_sync` --> src/impl_/pyclass/assertions.rs | | pub const fn assert_pyclass_sync() From b17de4f54e6b33f68c2766e182dbe579c1fcf505 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers <7943856+bschoenmaeckers@users.noreply.github.com> Date: Mon, 16 Dec 2024 20:14:13 +0100 Subject: [PATCH 406/495] use `datetime.fold` to distinguish ambiguous datetimes when converting (#4791) * use `datetime.fold` to distinguish ambiguous datetimes when converting * Set correct fold when converting to ambiguous `chrono::DateTime` --- newsfragments/4791.fixed.md | 1 + src/conversions/chrono.rs | 44 +++++++++++++++++++++++------- src/conversions/chrono_tz.rs | 52 ++++++++++++++++++++++++++++++++++++ 3 files changed, 87 insertions(+), 10 deletions(-) create mode 100644 newsfragments/4791.fixed.md diff --git a/newsfragments/4791.fixed.md b/newsfragments/4791.fixed.md new file mode 100644 index 00000000000..2aab452eb51 --- /dev/null +++ b/newsfragments/4791.fixed.md @@ -0,0 +1 @@ +use `datetime.fold` to distinguish ambiguous datetimes when converting to and from `chrono::DateTime` diff --git a/src/conversions/chrono.rs b/src/conversions/chrono.rs index 02bc65e59c5..04febb43b78 100644 --- a/src/conversions/chrono.rs +++ b/src/conversions/chrono.rs @@ -48,6 +48,8 @@ use crate::sync::GILOnceCell; use crate::types::any::PyAnyMethods; #[cfg(not(Py_LIMITED_API))] use crate::types::datetime::timezone_from_offset; +#[cfg(Py_LIMITED_API)] +use crate::types::IntoPyDict; use crate::types::PyNone; #[cfg(not(Py_LIMITED_API))] use crate::types::{ @@ -61,7 +63,8 @@ use crate::{intern, DowncastError}; use crate::{IntoPy, ToPyObject}; use chrono::offset::{FixedOffset, Utc}; use chrono::{ - DateTime, Datelike, Duration, NaiveDate, NaiveDateTime, NaiveTime, Offset, TimeZone, Timelike, + DateTime, Datelike, Duration, LocalResult, NaiveDate, NaiveDateTime, NaiveTime, Offset, + TimeZone, Timelike, }; #[allow(deprecated)] @@ -465,14 +468,21 @@ where truncated_leap_second, } = (&self.naive_local().time()).into(); + let fold = matches!( + self.timezone().offset_from_local_datetime(&self.naive_local()), + LocalResult::Ambiguous(_, latest) if self.offset().fix() == latest.fix() + ); + #[cfg(not(Py_LIMITED_API))] - let datetime = PyDateTime::new(py, year, month, day, hour, min, sec, micro, Some(tz))?; + let datetime = + PyDateTime::new_with_fold(py, year, month, day, hour, min, sec, micro, Some(tz), fold)?; #[cfg(Py_LIMITED_API)] let datetime = DatetimeTypes::try_get(py).and_then(|dt| { - dt.datetime - .bind(py) - .call1((year, month, day, hour, min, sec, micro, tz)) + dt.datetime.bind(py).call( + (year, month, day, hour, min, sec, micro, tz), + Some(&[("fold", fold as u8)].into_py_dict(py)?), + ) })?; if truncated_leap_second { @@ -503,12 +513,26 @@ impl FromPyObject<'py>> FromPyObject<'_> for DateTime Ok(value), + LocalResult::Ambiguous(earliest, latest) => { + #[cfg(not(Py_LIMITED_API))] + let fold = dt.get_fold(); + + #[cfg(Py_LIMITED_API)] + let fold = dt.getattr(intern!(dt.py(), "fold"))?.extract::()? > 0; + + if fold { + Ok(latest) + } else { + Ok(earliest) + } + } + LocalResult::None => Err(PyValueError::new_err(format!( + "The datetime {:?} contains an incompatible timezone", dt - )) - }) + ))), + } } } diff --git a/src/conversions/chrono_tz.rs b/src/conversions/chrono_tz.rs index bb1a74c1519..60a3bab4918 100644 --- a/src/conversions/chrono_tz.rs +++ b/src/conversions/chrono_tz.rs @@ -98,6 +98,10 @@ impl FromPyObject<'_> for Tz { #[cfg(all(test, not(windows)))] // Troubles loading timezones on Windows mod tests { use super::*; + use crate::prelude::PyAnyMethods; + use crate::Python; + use chrono::{DateTime, Utc}; + use chrono_tz::Tz; #[test] fn test_frompyobject() { @@ -114,6 +118,54 @@ mod tests { }); } + #[test] + fn test_ambiguous_datetime_to_pyobject() { + let dates = [ + DateTime::::from_str("2020-10-24 23:00:00 UTC").unwrap(), + DateTime::::from_str("2020-10-25 00:00:00 UTC").unwrap(), + DateTime::::from_str("2020-10-25 01:00:00 UTC").unwrap(), + ]; + + let dates = dates.map(|dt| dt.with_timezone(&Tz::Europe__London)); + + assert_eq!( + dates.map(|dt| dt.to_string()), + [ + "2020-10-25 00:00:00 BST", + "2020-10-25 01:00:00 BST", + "2020-10-25 01:00:00 GMT" + ] + ); + + let dates = Python::with_gil(|py| { + let pydates = dates.map(|dt| dt.into_pyobject(py).unwrap()); + assert_eq!( + pydates + .clone() + .map(|dt| dt.getattr("hour").unwrap().extract::().unwrap()), + [0, 1, 1] + ); + + assert_eq!( + pydates + .clone() + .map(|dt| dt.getattr("fold").unwrap().extract::().unwrap() > 0), + [false, false, true] + ); + + pydates.map(|dt| dt.extract::>().unwrap()) + }); + + assert_eq!( + dates.map(|dt| dt.to_string()), + [ + "2020-10-25 00:00:00 BST", + "2020-10-25 01:00:00 BST", + "2020-10-25 01:00:00 GMT" + ] + ); + } + #[test] #[cfg(not(Py_GIL_DISABLED))] // https://github.com/python/cpython/issues/116738#issuecomment-2404360445 fn test_into_pyobject() { From c869e515b480a09ac52666d8c64544d0093587d5 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Wed, 18 Dec 2024 08:39:54 -0700 Subject: [PATCH 407/495] docs: grammar fixes for free-threaded guide (#4805) --- guide/src/free-threading.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/guide/src/free-threading.md b/guide/src/free-threading.md index f212cb0b9a9..573fd7b2609 100644 --- a/guide/src/free-threading.md +++ b/guide/src/free-threading.md @@ -183,10 +183,10 @@ mutability](./class.md#bound-and-interior-mutability),) but now in free-threaded Python there are more opportunities to trigger these panics from Python because there is no GIL to lock concurrent access to mutably borrowed data from Python. -The most straightforward way to trigger this problem to use the Python +The most straightforward way to trigger this problem is to use the Python [`threading`] module to simultaneously call a rust function that mutably borrows a -[`pyclass`]({{#PYO3_DOCS_URL}}/pyo3/attr.pyclass.html). For example, -consider the following implementation: +[`pyclass`]({{#PYO3_DOCS_URL}}/pyo3/attr.pyclass.html) in multiple threads. For +example, consider the following implementation: ```rust # use pyo3::prelude::*; @@ -240,7 +240,7 @@ RuntimeError: Already borrowed We plan to allow user-selectable semantics for mutable pyclass definitions in PyO3 0.24, allowing some form of opt-in locking to emulate the GIL if that is needed. For now you should explicitly add locking, possibly using conditional -compilation or using the critical section API to avoid creating deadlocks with +compilation or using the critical section API, to avoid creating deadlocks with the GIL. ### Cannot build extensions using the limited API @@ -252,8 +252,8 @@ PyO3 will print a warning and ignore that setting when building extensions using the free-threaded interpreter. This means that if your package makes use of the ABI forward compatibility -provided by the limited API to uploads only one wheel for each release of your -package, you will need to update and tooling or instructions to also upload a +provided by the limited API to upload only one wheel for each release of your +package, you will need to update your release procedure to also upload a version-specific free-threaded wheel. See [the guide section](./building-and-distribution/multiple-python-versions.md) From ea8c461ac1568890f9f1e4f710e94ebefc7a8dd0 Mon Sep 17 00:00:00 2001 From: messense Date: Thu, 19 Dec 2024 01:21:55 +0800 Subject: [PATCH 408/495] Fix generating import lib for PyPy when abi3 feature is enabled (#4806) --- newsfragments/4806.fixed.md | 1 + pyo3-build-config/src/impl_.rs | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 newsfragments/4806.fixed.md diff --git a/newsfragments/4806.fixed.md b/newsfragments/4806.fixed.md new file mode 100644 index 00000000000..b5f3d8a554d --- /dev/null +++ b/newsfragments/4806.fixed.md @@ -0,0 +1 @@ +Fix generating import lib for PyPy when `abi3` feature is enabled. diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index f36c5e50226..43702eebef9 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -565,7 +565,11 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) // Auto generate python3.dll import libraries for Windows targets. if self.lib_dir.is_none() { let target = target_triple_from_env(); - let py_version = if self.abi3 { None } else { Some(self.version) }; + let py_version = if self.implementation == PythonImplementation::CPython && self.abi3 { + None + } else { + Some(self.version) + }; let abiflags = if self.is_free_threaded() { Some("t") } else { From 54cfaf23f6f19c3f5564b577df568bf9c21b3272 Mon Sep 17 00:00:00 2001 From: Thomas Tanon Date: Mon, 23 Dec 2024 14:53:51 +0100 Subject: [PATCH 409/495] derive(FromPyObject): support raw identifiers (#4814) --- newsfragments/4814.fixed.md | 1 + pyo3-macros-backend/src/frompyobject.rs | 9 +++--- tests/test_frompyobject.rs | 38 +++++++++++++++++++++++++ 3 files changed, 44 insertions(+), 4 deletions(-) create mode 100644 newsfragments/4814.fixed.md diff --git a/newsfragments/4814.fixed.md b/newsfragments/4814.fixed.md new file mode 100644 index 00000000000..6634efc2b9f --- /dev/null +++ b/newsfragments/4814.fixed.md @@ -0,0 +1 @@ +`derive(FromPyObject)` support raw identifiers like `r#box`. \ No newline at end of file diff --git a/pyo3-macros-backend/src/frompyobject.rs b/pyo3-macros-backend/src/frompyobject.rs index 14c8755e9be..565c54da1f3 100644 --- a/pyo3-macros-backend/src/frompyobject.rs +++ b/pyo3-macros-backend/src/frompyobject.rs @@ -3,6 +3,7 @@ use crate::utils::Ctx; use proc_macro2::TokenStream; use quote::{format_ident, quote}; use syn::{ + ext::IdentExt, parenthesized, parse::{Parse, ParseStream}, parse_quote, @@ -323,11 +324,11 @@ impl<'a> Container<'a> { fn build_struct(&self, struct_fields: &[NamedStructField<'_>], ctx: &Ctx) -> TokenStream { let Ctx { pyo3_path, .. } = ctx; let self_ty = &self.path; - let struct_name = &self.name(); - let mut fields: Punctuated = Punctuated::new(); + let struct_name = self.name(); + let mut fields: Punctuated = Punctuated::new(); for field in struct_fields { - let ident = &field.ident; - let field_name = ident.to_string(); + let ident = field.ident; + let field_name = ident.unraw().to_string(); let getter = match field.getter.as_ref().unwrap_or(&FieldGetter::GetAttr(None)) { FieldGetter::GetAttr(Some(name)) => { quote!(#pyo3_path::types::PyAnyMethods::getattr(obj, #pyo3_path::intern!(obj.py(), #name))) diff --git a/tests/test_frompyobject.rs b/tests/test_frompyobject.rs index 344a47acf72..2192caf1f7c 100644 --- a/tests/test_frompyobject.rs +++ b/tests/test_frompyobject.rs @@ -648,3 +648,41 @@ fn test_transparent_from_py_with() { assert_eq!(result, expected); }); } + +#[derive(Debug, FromPyObject, PartialEq, Eq)] +pub struct WithKeywordAttr { + r#box: usize, +} + +#[pyclass] +pub struct WithKeywordAttrC { + #[pyo3(get)] + r#box: usize, +} + +#[test] +fn test_with_keyword_attr() { + Python::with_gil(|py| { + let cls = WithKeywordAttrC { r#box: 3 }.into_pyobject(py).unwrap(); + let result = cls.extract::().unwrap(); + let expected = WithKeywordAttr { r#box: 3 }; + assert_eq!(result, expected); + }); +} + +#[derive(Debug, FromPyObject, PartialEq, Eq)] +pub struct WithKeywordItem { + #[pyo3(item)] + r#box: usize, +} + +#[test] +fn test_with_keyword_item() { + Python::with_gil(|py| { + let dict = PyDict::new(py); + dict.set_item("box", 3).unwrap(); + let result = dict.extract::().unwrap(); + let expected = WithKeywordItem { r#box: 3 }; + assert_eq!(result, expected); + }); +} From 787980e561a128bc882901bbe69d3be921f9639c Mon Sep 17 00:00:00 2001 From: Lily Foote Date: Mon, 23 Dec 2024 16:04:12 +0000 Subject: [PATCH 410/495] Fix copy/paste typo in PyListMethods docs (#4818) --- src/types/list.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types/list.rs b/src/types/list.rs index af2b557cba9..2e124c82400 100644 --- a/src/types/list.rs +++ b/src/types/list.rs @@ -18,7 +18,7 @@ use crate::types::sequence::PySequenceMethods; /// [`Py`][crate::Py] or [`Bound<'py, PyList>`][Bound]. /// /// For APIs available on `list` objects, see the [`PyListMethods`] trait which is implemented for -/// [`Bound<'py, PyDict>`][Bound]. +/// [`Bound<'py, PyList>`][Bound]. #[repr(transparent)] pub struct PyList(PyAny); From 117d9865df5cb6d708fb0c4dab433309c5a9633f Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Mon, 23 Dec 2024 17:07:06 +0100 Subject: [PATCH 411/495] fix nightly ci (#4816) --- pyo3-build-config/src/lib.rs | 5 +++++ src/impl_/pymethods.rs | 9 +++++---- src/internal_tricks.rs | 30 +++++++++++++++++++++++++++++- 3 files changed, 39 insertions(+), 5 deletions(-) diff --git a/pyo3-build-config/src/lib.rs b/pyo3-build-config/src/lib.rs index 554200040e4..195f658f39e 100644 --- a/pyo3-build-config/src/lib.rs +++ b/pyo3-build-config/src/lib.rs @@ -160,6 +160,10 @@ pub fn print_feature_cfgs() { if rustc_minor_version >= 83 { println!("cargo:rustc-cfg=io_error_more"); } + + if rustc_minor_version >= 85 { + println!("cargo:rustc-cfg=fn_ptr_eq"); + } } /// Registers `pyo3`s config names as reachable cfg expressions @@ -185,6 +189,7 @@ pub fn print_expected_cfgs() { println!("cargo:rustc-check-cfg=cfg(c_str_lit)"); println!("cargo:rustc-check-cfg=cfg(rustc_has_once_lock)"); println!("cargo:rustc-check-cfg=cfg(io_error_more)"); + println!("cargo:rustc-check-cfg=cfg(fn_ptr_eq)"); // allow `Py_3_*` cfgs from the minimum supported version up to the // maximum minor version (+1 for development for the next) diff --git a/src/impl_/pymethods.rs b/src/impl_/pymethods.rs index 58d0c93c240..e6cbaec86f5 100644 --- a/src/impl_/pymethods.rs +++ b/src/impl_/pymethods.rs @@ -21,6 +21,7 @@ use std::panic::{catch_unwind, AssertUnwindSafe}; use std::ptr::null_mut; use super::trampoline; +use crate::internal_tricks::{clear_eq, traverse_eq}; /// Python 3.8 and up - __ipow__ has modulo argument correctly populated. #[cfg(Py_3_8)] @@ -364,7 +365,7 @@ unsafe fn call_super_traverse( // First find the current type by the current_traverse function loop { traverse = get_slot(ty, TP_TRAVERSE); - if traverse == Some(current_traverse) { + if traverse_eq(traverse, current_traverse) { break; } ty = get_slot(ty, TP_BASE); @@ -375,7 +376,7 @@ unsafe fn call_super_traverse( } // Get first base which has a different traverse function - while traverse == Some(current_traverse) { + while traverse_eq(traverse, current_traverse) { ty = get_slot(ty, TP_BASE); if ty.is_null() { break; @@ -429,7 +430,7 @@ unsafe fn call_super_clear( // First find the current type by the current_clear function loop { clear = ty.get_slot(TP_CLEAR); - if clear == Some(current_clear) { + if clear_eq(clear, current_clear) { break; } let base = ty.get_slot(TP_BASE); @@ -441,7 +442,7 @@ unsafe fn call_super_clear( } // Get first base which has a different clear function - while clear == Some(current_clear) { + while clear_eq(clear, current_clear) { let base = ty.get_slot(TP_BASE); if base.is_null() { break; diff --git a/src/internal_tricks.rs b/src/internal_tricks.rs index 97b13aff2a8..dad3f7c4c03 100644 --- a/src/internal_tricks.rs +++ b/src/internal_tricks.rs @@ -1,4 +1,4 @@ -use crate::ffi::{Py_ssize_t, PY_SSIZE_T_MAX}; +use crate::ffi::{self, Py_ssize_t, PY_SSIZE_T_MAX}; pub struct PrivateMarker; macro_rules! private_decl { @@ -47,3 +47,31 @@ pub(crate) const fn ptr_from_ref(t: &T) -> *const T { pub(crate) fn ptr_from_mut(t: &mut T) -> *mut T { t as *mut T } + +// TODO: use ptr::fn_addr_eq on MSRV 1.85 +pub(crate) fn clear_eq(f: Option, g: ffi::inquiry) -> bool { + #[cfg(fn_ptr_eq)] + { + let Some(f) = f else { return false }; + std::ptr::fn_addr_eq(f, g) + } + + #[cfg(not(fn_ptr_eq))] + { + f == Some(g) + } +} + +// TODO: use ptr::fn_addr_eq on MSRV 1.85 +pub(crate) fn traverse_eq(f: Option, g: ffi::traverseproc) -> bool { + #[cfg(fn_ptr_eq)] + { + let Some(f) = f else { return false }; + std::ptr::fn_addr_eq(f, g) + } + + #[cfg(not(fn_ptr_eq))] + { + f == Some(g) + } +} From 14c54022cb0c16e035e3e768a1261864614807f6 Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Mon, 23 Dec 2024 20:05:56 +0100 Subject: [PATCH 412/495] switch trait sealing in `impl_` module (#4817) --- src/impl_/pyclass.rs | 25 +++++++++++++------------ src/internal_tricks.rs | 17 ----------------- 2 files changed, 13 insertions(+), 29 deletions(-) diff --git a/src/impl_/pyclass.rs b/src/impl_/pyclass.rs index ac5c6e3e3f0..59bb2b4bd5a 100644 --- a/src/impl_/pyclass.rs +++ b/src/impl_/pyclass.rs @@ -43,18 +43,27 @@ pub fn weaklist_offset() -> ffi::Py_ssize_t { PyClassObject::::weaklist_offset() } +mod sealed { + pub trait Sealed {} + + impl Sealed for super::PyClassDummySlot {} + impl Sealed for super::PyClassDictSlot {} + impl Sealed for super::PyClassWeakRefSlot {} + impl Sealed for super::ThreadCheckerImpl {} + impl Sealed for super::SendablePyClass {} +} + /// Represents the `__dict__` field for `#[pyclass]`. -pub trait PyClassDict { +pub trait PyClassDict: sealed::Sealed { /// Initial form of a [PyObject](crate::ffi::PyObject) `__dict__` reference. const INIT: Self; /// Empties the dictionary of its key-value pairs. #[inline] fn clear_dict(&mut self, _py: Python<'_>) {} - private_decl! {} } /// Represents the `__weakref__` field for `#[pyclass]`. -pub trait PyClassWeakRef { +pub trait PyClassWeakRef: sealed::Sealed { /// Initializes a `weakref` instance. const INIT: Self; /// Clears the weak references to the given object. @@ -64,19 +73,16 @@ pub trait PyClassWeakRef { /// - The GIL must be held. #[inline] unsafe fn clear_weakrefs(&mut self, _obj: *mut ffi::PyObject, _py: Python<'_>) {} - private_decl! {} } /// Zero-sized dummy field. pub struct PyClassDummySlot; impl PyClassDict for PyClassDummySlot { - private_impl! {} const INIT: Self = PyClassDummySlot; } impl PyClassWeakRef for PyClassDummySlot { - private_impl! {} const INIT: Self = PyClassDummySlot; } @@ -88,7 +94,6 @@ impl PyClassWeakRef for PyClassDummySlot { pub struct PyClassDictSlot(*mut ffi::PyObject); impl PyClassDict for PyClassDictSlot { - private_impl! {} const INIT: Self = Self(std::ptr::null_mut()); #[inline] fn clear_dict(&mut self, _py: Python<'_>) { @@ -106,7 +111,6 @@ impl PyClassDict for PyClassDictSlot { pub struct PyClassWeakRefSlot(*mut ffi::PyObject); impl PyClassWeakRef for PyClassWeakRefSlot { - private_impl! {} const INIT: Self = Self(std::ptr::null_mut()); #[inline] unsafe fn clear_weakrefs(&mut self, obj: *mut ffi::PyObject, _py: Python<'_>) { @@ -1034,12 +1038,11 @@ impl PyClassNewTextSignature for &'_ PyClassImplCollector { // Thread checkers #[doc(hidden)] -pub trait PyClassThreadChecker: Sized { +pub trait PyClassThreadChecker: Sized + sealed::Sealed { fn ensure(&self); fn check(&self) -> bool; fn can_drop(&self, py: Python<'_>) -> bool; fn new() -> Self; - private_decl! {} } /// Default thread checker for `#[pyclass]`. @@ -1062,7 +1065,6 @@ impl PyClassThreadChecker for SendablePyClass { fn new() -> Self { SendablePyClass(PhantomData) } - private_impl! {} } /// Thread checker for `#[pyclass(unsendable)]` types. @@ -1111,7 +1113,6 @@ impl PyClassThreadChecker for ThreadCheckerImpl { fn new() -> Self { ThreadCheckerImpl(thread::current().id()) } - private_impl! {} } /// Trait denoting that this class is suitable to be used as a base type for PyClass. diff --git a/src/internal_tricks.rs b/src/internal_tricks.rs index dad3f7c4c03..1c011253254 100644 --- a/src/internal_tricks.rs +++ b/src/internal_tricks.rs @@ -1,21 +1,4 @@ use crate::ffi::{self, Py_ssize_t, PY_SSIZE_T_MAX}; -pub struct PrivateMarker; - -macro_rules! private_decl { - () => { - /// This trait is private to implement; this method exists to make it - /// impossible to implement outside the crate. - fn __private__(&self) -> crate::internal_tricks::PrivateMarker; - }; -} - -macro_rules! private_impl { - () => { - fn __private__(&self) -> crate::internal_tricks::PrivateMarker { - crate::internal_tricks::PrivateMarker - } - }; -} macro_rules! pyo3_exception { ($doc: expr, $name: ident, $base: ty) => { From 41bb53291e2e11dd6407c4702481c8d8308d4c38 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Mon, 23 Dec 2024 16:04:28 -0500 Subject: [PATCH 413/495] Raise target-lexicon version and update for API changes (#4822) --- newsfragments/4822.changed.md | 1 + pyo3-build-config/Cargo.toml | 4 ++-- pyo3-build-config/src/impl_.rs | 4 ++-- pyo3-build-config/src/lib.rs | 2 +- 4 files changed, 6 insertions(+), 5 deletions(-) create mode 100644 newsfragments/4822.changed.md diff --git a/newsfragments/4822.changed.md b/newsfragments/4822.changed.md new file mode 100644 index 00000000000..a06613292c4 --- /dev/null +++ b/newsfragments/4822.changed.md @@ -0,0 +1 @@ +Bumped `target-lexicon` dependency to 0.13 diff --git a/pyo3-build-config/Cargo.toml b/pyo3-build-config/Cargo.toml index 1e951b29bff..656a03d44b3 100644 --- a/pyo3-build-config/Cargo.toml +++ b/pyo3-build-config/Cargo.toml @@ -14,11 +14,11 @@ rust-version = "1.63" [dependencies] once_cell = "1" python3-dll-a = { version = "0.2.11", optional = true } -target-lexicon = "0.12.14" +target-lexicon = "0.13" [build-dependencies] python3-dll-a = { version = "0.2.11", optional = true } -target-lexicon = "0.12.14" +target-lexicon = "0.13" [features] default = [] diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index 43702eebef9..e00e0aab963 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -938,8 +938,8 @@ impl CrossCompileConfig { && host.operating_system == OperatingSystem::Windows; // Not cross-compiling to compile for x86-64 Python from macOS arm64 and vice versa - compatible |= target.operating_system == OperatingSystem::Darwin - && host.operating_system == OperatingSystem::Darwin; + compatible |= matches!(target.operating_system, OperatingSystem::Darwin(_)) + && matches!(host.operating_system, OperatingSystem::Darwin(_)); !compatible } diff --git a/pyo3-build-config/src/lib.rs b/pyo3-build-config/src/lib.rs index 195f658f39e..420b81c738b 100644 --- a/pyo3-build-config/src/lib.rs +++ b/pyo3-build-config/src/lib.rs @@ -65,7 +65,7 @@ pub fn add_extension_module_link_args() { } fn _add_extension_module_link_args(triple: &Triple, mut writer: impl std::io::Write) { - if triple.operating_system == OperatingSystem::Darwin { + if matches!(triple.operating_system, OperatingSystem::Darwin(_)) { writeln!(writer, "cargo:rustc-cdylib-link-arg=-undefined").unwrap(); writeln!(writer, "cargo:rustc-cdylib-link-arg=dynamic_lookup").unwrap(); } else if triple == &Triple::from_str("wasm32-unknown-emscripten").unwrap() { From 9e63b34c0e86d17abd18b25cdc7c7787752f4a43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20=C5=A0im=C3=A1=C4=8Dek?= Date: Tue, 24 Dec 2024 11:57:40 +0100 Subject: [PATCH 414/495] Fix struct layouts on GraalPy (#4802) * Fix struct layouts on GraalPy * Add changelog item --- newsfragments/4802.fixed.md | 1 + pyo3-ffi/src/abstract_.rs | 1 - pyo3-ffi/src/cpython/abstract_.rs | 4 +++- pyo3-ffi/src/cpython/complexobject.rs | 1 - pyo3-ffi/src/cpython/floatobject.rs | 1 - pyo3-ffi/src/cpython/genobject.rs | 2 +- pyo3-ffi/src/cpython/listobject.rs | 4 ++-- pyo3-ffi/src/cpython/object.rs | 2 -- pyo3-ffi/src/cpython/objimpl.rs | 1 + pyo3-ffi/src/cpython/pyerrors.rs | 28 ++++++++++++------------- pyo3-ffi/src/cpython/tupleobject.rs | 1 - pyo3-ffi/src/cpython/unicodeobject.rs | 13 ++++-------- pyo3-ffi/src/datetime.rs | 30 ++++++++++----------------- pyo3-ffi/src/object.rs | 3 +++ pyo3-ffi/src/pyhash.rs | 8 ++++--- src/types/mod.rs | 2 +- 16 files changed, 46 insertions(+), 56 deletions(-) create mode 100644 newsfragments/4802.fixed.md diff --git a/newsfragments/4802.fixed.md b/newsfragments/4802.fixed.md new file mode 100644 index 00000000000..55d79c71734 --- /dev/null +++ b/newsfragments/4802.fixed.md @@ -0,0 +1 @@ +Fixed missing struct fields on GraalPy when subclassing builtin classes diff --git a/pyo3-ffi/src/abstract_.rs b/pyo3-ffi/src/abstract_.rs index 1899545011a..ce6c9b94735 100644 --- a/pyo3-ffi/src/abstract_.rs +++ b/pyo3-ffi/src/abstract_.rs @@ -17,7 +17,6 @@ pub unsafe fn PyObject_DelAttr(o: *mut PyObject, attr_name: *mut PyObject) -> c_ extern "C" { #[cfg(all( not(PyPy), - not(GraalPy), any(Py_3_10, all(not(Py_LIMITED_API), Py_3_9)) // Added to python in 3.9 but to limited API in 3.10 ))] #[cfg_attr(PyPy, link_name = "PyPyObject_CallNoArgs")] diff --git a/pyo3-ffi/src/cpython/abstract_.rs b/pyo3-ffi/src/cpython/abstract_.rs index 83295e58f61..477ad02b747 100644 --- a/pyo3-ffi/src/cpython/abstract_.rs +++ b/pyo3-ffi/src/cpython/abstract_.rs @@ -1,5 +1,7 @@ use crate::{PyObject, Py_ssize_t}; -use std::os::raw::{c_char, c_int}; +#[cfg(not(all(Py_3_11, GraalPy)))] +use std::os::raw::c_char; +use std::os::raw::c_int; #[cfg(not(Py_3_11))] use crate::Py_buffer; diff --git a/pyo3-ffi/src/cpython/complexobject.rs b/pyo3-ffi/src/cpython/complexobject.rs index 255f9c27034..4cc86db5667 100644 --- a/pyo3-ffi/src/cpython/complexobject.rs +++ b/pyo3-ffi/src/cpython/complexobject.rs @@ -19,7 +19,6 @@ pub struct Py_complex { #[repr(C)] pub struct PyComplexObject { pub ob_base: PyObject, - #[cfg(not(GraalPy))] pub cval: Py_complex, } diff --git a/pyo3-ffi/src/cpython/floatobject.rs b/pyo3-ffi/src/cpython/floatobject.rs index 8c7ee88543d..e7caa441c5d 100644 --- a/pyo3-ffi/src/cpython/floatobject.rs +++ b/pyo3-ffi/src/cpython/floatobject.rs @@ -6,7 +6,6 @@ use std::os::raw::c_double; #[repr(C)] pub struct PyFloatObject { pub ob_base: PyObject, - #[cfg(not(GraalPy))] pub ob_fval: c_double, } diff --git a/pyo3-ffi/src/cpython/genobject.rs b/pyo3-ffi/src/cpython/genobject.rs index 17348b2f7bd..4be310a8c88 100644 --- a/pyo3-ffi/src/cpython/genobject.rs +++ b/pyo3-ffi/src/cpython/genobject.rs @@ -2,7 +2,7 @@ use crate::object::*; use crate::PyFrameObject; #[cfg(not(any(PyPy, GraalPy)))] use crate::_PyErr_StackItem; -#[cfg(Py_3_11)] +#[cfg(all(Py_3_11, not(GraalPy)))] use std::os::raw::c_char; use std::os::raw::c_int; use std::ptr::addr_of_mut; diff --git a/pyo3-ffi/src/cpython/listobject.rs b/pyo3-ffi/src/cpython/listobject.rs index 963ddfbea87..694e6bc4290 100644 --- a/pyo3-ffi/src/cpython/listobject.rs +++ b/pyo3-ffi/src/cpython/listobject.rs @@ -2,7 +2,7 @@ use crate::object::*; #[cfg(not(PyPy))] use crate::pyport::Py_ssize_t; -#[cfg(not(any(PyPy, GraalPy)))] +#[cfg(not(PyPy))] #[repr(C)] pub struct PyListObject { pub ob_base: PyVarObject, @@ -10,7 +10,7 @@ pub struct PyListObject { pub allocated: Py_ssize_t, } -#[cfg(any(PyPy, GraalPy))] +#[cfg(PyPy)] pub struct PyListObject { pub ob_base: PyObject, } diff --git a/pyo3-ffi/src/cpython/object.rs b/pyo3-ffi/src/cpython/object.rs index 35ddf25a2de..75eef11aae3 100644 --- a/pyo3-ffi/src/cpython/object.rs +++ b/pyo3-ffi/src/cpython/object.rs @@ -211,8 +211,6 @@ pub type printfunc = #[derive(Debug)] pub struct PyTypeObject { pub ob_base: object::PyVarObject, - #[cfg(GraalPy)] - pub ob_size: Py_ssize_t, pub tp_name: *const c_char, pub tp_basicsize: Py_ssize_t, pub tp_itemsize: Py_ssize_t, diff --git a/pyo3-ffi/src/cpython/objimpl.rs b/pyo3-ffi/src/cpython/objimpl.rs index 3e0270ddc8f..98a19abeb81 100644 --- a/pyo3-ffi/src/cpython/objimpl.rs +++ b/pyo3-ffi/src/cpython/objimpl.rs @@ -1,3 +1,4 @@ +#[cfg(not(all(Py_3_11, GraalPy)))] use libc::size_t; use std::os::raw::c_int; diff --git a/pyo3-ffi/src/cpython/pyerrors.rs b/pyo3-ffi/src/cpython/pyerrors.rs index ca08b44a95c..c6e10e5f07b 100644 --- a/pyo3-ffi/src/cpython/pyerrors.rs +++ b/pyo3-ffi/src/cpython/pyerrors.rs @@ -6,19 +6,19 @@ use crate::Py_ssize_t; #[derive(Debug)] pub struct PyBaseExceptionObject { pub ob_base: PyObject, - #[cfg(not(any(PyPy, GraalPy)))] + #[cfg(not(PyPy))] pub dict: *mut PyObject, - #[cfg(not(any(PyPy, GraalPy)))] + #[cfg(not(PyPy))] pub args: *mut PyObject, - #[cfg(all(Py_3_11, not(any(PyPy, GraalPy))))] + #[cfg(all(Py_3_11, not(PyPy)))] pub notes: *mut PyObject, - #[cfg(not(any(PyPy, GraalPy)))] + #[cfg(not(PyPy))] pub traceback: *mut PyObject, - #[cfg(not(any(PyPy, GraalPy)))] + #[cfg(not(PyPy))] pub context: *mut PyObject, - #[cfg(not(any(PyPy, GraalPy)))] + #[cfg(not(PyPy))] pub cause: *mut PyObject, - #[cfg(not(any(PyPy, GraalPy)))] + #[cfg(not(PyPy))] pub suppress_context: char, } @@ -134,19 +134,19 @@ pub struct PyOSErrorObject { #[derive(Debug)] pub struct PyStopIterationObject { pub ob_base: PyObject, - #[cfg(not(any(PyPy, GraalPy)))] + #[cfg(not(PyPy))] pub dict: *mut PyObject, - #[cfg(not(any(PyPy, GraalPy)))] + #[cfg(not(PyPy))] pub args: *mut PyObject, - #[cfg(all(Py_3_11, not(any(PyPy, GraalPy))))] + #[cfg(all(Py_3_11, not(PyPy)))] pub notes: *mut PyObject, - #[cfg(not(any(PyPy, GraalPy)))] + #[cfg(not(PyPy))] pub traceback: *mut PyObject, - #[cfg(not(any(PyPy, GraalPy)))] + #[cfg(not(PyPy))] pub context: *mut PyObject, - #[cfg(not(any(PyPy, GraalPy)))] + #[cfg(not(PyPy))] pub cause: *mut PyObject, - #[cfg(not(any(PyPy, GraalPy)))] + #[cfg(not(PyPy))] pub suppress_context: char, pub value: *mut PyObject, diff --git a/pyo3-ffi/src/cpython/tupleobject.rs b/pyo3-ffi/src/cpython/tupleobject.rs index 1d988d2bef0..9616d4372cc 100644 --- a/pyo3-ffi/src/cpython/tupleobject.rs +++ b/pyo3-ffi/src/cpython/tupleobject.rs @@ -5,7 +5,6 @@ use crate::pyport::Py_ssize_t; #[repr(C)] pub struct PyTupleObject { pub ob_base: PyVarObject, - #[cfg(not(GraalPy))] pub ob_item: [*mut PyObject; 1], } diff --git a/pyo3-ffi/src/cpython/unicodeobject.rs b/pyo3-ffi/src/cpython/unicodeobject.rs index 1414b4ceb38..fae626b8d25 100644 --- a/pyo3-ffi/src/cpython/unicodeobject.rs +++ b/pyo3-ffi/src/cpython/unicodeobject.rs @@ -1,4 +1,4 @@ -#[cfg(not(any(PyPy, GraalPy)))] +#[cfg(not(PyPy))] use crate::Py_hash_t; use crate::{PyObject, Py_UCS1, Py_UCS2, Py_UCS4, Py_ssize_t}; use libc::wchar_t; @@ -250,9 +250,8 @@ impl From for u32 { #[repr(C)] pub struct PyASCIIObject { pub ob_base: PyObject, - #[cfg(not(GraalPy))] pub length: Py_ssize_t, - #[cfg(not(any(PyPy, GraalPy)))] + #[cfg(not(PyPy))] pub hash: Py_hash_t, /// A bit field with various properties. /// @@ -265,9 +264,8 @@ pub struct PyASCIIObject { /// unsigned int ascii:1; /// unsigned int ready:1; /// unsigned int :24; - #[cfg(not(GraalPy))] pub state: u32, - #[cfg(not(any(Py_3_12, GraalPy)))] + #[cfg(not(Py_3_12))] pub wstr: *mut wchar_t, } @@ -379,11 +377,9 @@ impl PyASCIIObject { #[repr(C)] pub struct PyCompactUnicodeObject { pub _base: PyASCIIObject, - #[cfg(not(GraalPy))] pub utf8_length: Py_ssize_t, - #[cfg(not(GraalPy))] pub utf8: *mut c_char, - #[cfg(not(any(Py_3_12, GraalPy)))] + #[cfg(not(Py_3_12))] pub wstr_length: Py_ssize_t, } @@ -398,7 +394,6 @@ pub union PyUnicodeObjectData { #[repr(C)] pub struct PyUnicodeObject { pub _base: PyCompactUnicodeObject, - #[cfg(not(GraalPy))] pub data: PyUnicodeObjectData, } diff --git a/pyo3-ffi/src/datetime.rs b/pyo3-ffi/src/datetime.rs index 76d12151afc..e529e0fce50 100644 --- a/pyo3-ffi/src/datetime.rs +++ b/pyo3-ffi/src/datetime.rs @@ -9,13 +9,12 @@ use crate::PyCapsule_Import; #[cfg(GraalPy)] use crate::{PyLong_AsLong, PyLong_Check, PyObject_GetAttrString, Py_DecRef}; use crate::{PyObject, PyObject_TypeCheck, PyTypeObject, Py_TYPE}; -#[cfg(not(GraalPy))] use std::os::raw::c_char; use std::os::raw::c_int; use std::ptr; use std::sync::Once; use std::{cell::UnsafeCell, ffi::CStr}; -#[cfg(not(any(PyPy, GraalPy)))] +#[cfg(not(PyPy))] use {crate::Py_hash_t, std::os::raw::c_uchar}; // Type struct wrappers const _PyDateTime_DATE_DATASIZE: usize = 4; @@ -27,13 +26,10 @@ const _PyDateTime_DATETIME_DATASIZE: usize = 10; /// Structure representing a `datetime.timedelta`. pub struct PyDateTime_Delta { pub ob_base: PyObject, - #[cfg(not(any(PyPy, GraalPy)))] + #[cfg(not(PyPy))] pub hashcode: Py_hash_t, - #[cfg(not(GraalPy))] pub days: c_int, - #[cfg(not(GraalPy))] pub seconds: c_int, - #[cfg(not(GraalPy))] pub microseconds: c_int, } @@ -56,19 +52,17 @@ pub struct _PyDateTime_BaseTime { /// Structure representing a `datetime.time`. pub struct PyDateTime_Time { pub ob_base: PyObject, - #[cfg(not(any(PyPy, GraalPy)))] + #[cfg(not(PyPy))] pub hashcode: Py_hash_t, - #[cfg(not(GraalPy))] pub hastzinfo: c_char, - #[cfg(not(any(PyPy, GraalPy)))] + #[cfg(not(PyPy))] pub data: [c_uchar; _PyDateTime_TIME_DATASIZE], - #[cfg(not(any(PyPy, GraalPy)))] + #[cfg(not(PyPy))] pub fold: c_uchar, /// # Safety /// /// Care should be taken when reading this field. If the time does not have a /// tzinfo then CPython may allocate as a `_PyDateTime_BaseTime` without this field. - #[cfg(not(GraalPy))] pub tzinfo: *mut PyObject, } @@ -77,11 +71,11 @@ pub struct PyDateTime_Time { /// Structure representing a `datetime.date` pub struct PyDateTime_Date { pub ob_base: PyObject, - #[cfg(not(any(PyPy, GraalPy)))] + #[cfg(not(PyPy))] pub hashcode: Py_hash_t, - #[cfg(not(any(PyPy, GraalPy)))] + #[cfg(not(PyPy))] pub hastzinfo: c_char, - #[cfg(not(any(PyPy, GraalPy)))] + #[cfg(not(PyPy))] pub data: [c_uchar; _PyDateTime_DATE_DATASIZE], } @@ -101,19 +95,17 @@ pub struct _PyDateTime_BaseDateTime { /// Structure representing a `datetime.datetime`. pub struct PyDateTime_DateTime { pub ob_base: PyObject, - #[cfg(not(any(PyPy, GraalPy)))] + #[cfg(not(PyPy))] pub hashcode: Py_hash_t, - #[cfg(not(GraalPy))] pub hastzinfo: c_char, - #[cfg(not(any(PyPy, GraalPy)))] + #[cfg(not(PyPy))] pub data: [c_uchar; _PyDateTime_DATETIME_DATASIZE], - #[cfg(not(any(PyPy, GraalPy)))] + #[cfg(not(PyPy))] pub fold: c_uchar, /// # Safety /// /// Care should be taken when reading this field. If the time does not have a /// tzinfo then CPython may allocate as a `_PyDateTime_BaseDateTime` without this field. - #[cfg(not(GraalPy))] pub tzinfo: *mut PyObject, } diff --git a/pyo3-ffi/src/object.rs b/pyo3-ffi/src/object.rs index fc3484be102..3f086ac1e92 100644 --- a/pyo3-ffi/src/object.rs +++ b/pyo3-ffi/src/object.rs @@ -129,6 +129,9 @@ pub struct PyVarObject { pub ob_base: PyObject, #[cfg(not(GraalPy))] pub ob_size: Py_ssize_t, + // On GraalPy the field is physically there, but not always populated. We hide it to prevent accidental misuse + #[cfg(GraalPy)] + pub _ob_size_graalpy: Py_ssize_t, } // skipped private _PyVarObject_CAST diff --git a/pyo3-ffi/src/pyhash.rs b/pyo3-ffi/src/pyhash.rs index f42f9730f1b..4f14e04a695 100644 --- a/pyo3-ffi/src/pyhash.rs +++ b/pyo3-ffi/src/pyhash.rs @@ -1,7 +1,9 @@ -#[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))] +#[cfg(not(any(Py_LIMITED_API, PyPy)))] use crate::pyport::{Py_hash_t, Py_ssize_t}; #[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))] -use std::os::raw::{c_char, c_void}; +use std::os::raw::c_char; +#[cfg(not(any(Py_LIMITED_API, PyPy)))] +use std::os::raw::c_void; use std::os::raw::{c_int, c_ulong}; @@ -10,7 +12,7 @@ extern "C" { // skipped non-limited _Py_HashPointer // skipped non-limited _Py_HashPointerRaw - #[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))] + #[cfg(not(any(Py_LIMITED_API, PyPy)))] pub fn _Py_HashBytes(src: *const c_void, len: Py_ssize_t) -> Py_hash_t; } diff --git a/src/types/mod.rs b/src/types/mod.rs index d84f099e773..39ab3fd501e 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -23,7 +23,7 @@ pub use self::float::{PyFloat, PyFloatMethods}; pub use self::frame::PyFrame; pub use self::frozenset::{PyFrozenSet, PyFrozenSetBuilder, PyFrozenSetMethods}; pub use self::function::PyCFunction; -#[cfg(all(not(Py_LIMITED_API), not(all(PyPy, not(Py_3_8))), not(GraalPy)))] +#[cfg(all(not(Py_LIMITED_API), not(all(PyPy, not(Py_3_8)))))] pub use self::function::PyFunction; pub use self::iterator::PyIterator; pub use self::list::{PyList, PyListMethods}; From 3965f5f7cb6908f7e7f2b62076948daf509a529a Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Tue, 24 Dec 2024 13:48:32 +0100 Subject: [PATCH 415/495] reintroduce `vectorcall` optimization with new `PyCallArgs` trait (#4768) --- guide/src/performance.md | 13 ++ newsfragments/4768.added.md | 1 + newsfragments/4768.changed.md | 1 + src/call.rs | 150 +++++++++++++++++ src/lib.rs | 1 + src/types/any.rs | 60 +++---- src/types/tuple.rs | 249 +++++++++++++++++++++++++++++ tests/test_compile_error.rs | 1 + tests/ui/invalid_pycallargs.rs | 8 + tests/ui/invalid_pycallargs.stderr | 29 ++++ 10 files changed, 484 insertions(+), 29 deletions(-) create mode 100644 newsfragments/4768.added.md create mode 100644 newsfragments/4768.changed.md create mode 100644 src/call.rs create mode 100644 tests/ui/invalid_pycallargs.rs create mode 100644 tests/ui/invalid_pycallargs.stderr diff --git a/guide/src/performance.md b/guide/src/performance.md index fb2288dd566..5a57585c4a0 100644 --- a/guide/src/performance.md +++ b/guide/src/performance.md @@ -97,6 +97,19 @@ impl PartialEq for FooBound<'_> { } ``` +## Calling Python callables (`__call__`) +CPython support multiple calling protocols: [`tp_call`] and [`vectorcall`]. [`vectorcall`] is a more efficient protocol unlocking faster calls. +PyO3 will try to dispatch Python `call`s using the [`vectorcall`] calling convention to archive maximum performance if possible and falling back to [`tp_call`] otherwise. +This is implemented using the (internal) `PyCallArgs` trait. It defines how Rust types can be used as Python `call` arguments. This trait is currently implemented for +- Rust tuples, where each member implements `IntoPyObject`, +- `Bound<'_, PyTuple>` +- `Py` +Rust tuples may make use of [`vectorcall`] where as `Bound<'_, PyTuple>` and `Py` can only use [`tp_call`]. For maximum performance prefer using Rust tuples as arguments. + + +[`tp_call`]: https://docs.python.org/3/c-api/call.html#the-tp-call-protocol +[`vectorcall`]: https://docs.python.org/3/c-api/call.html#the-vectorcall-protocol + ## Disable the global reference pool PyO3 uses global mutable state to keep track of deferred reference count updates implied by `impl Drop for Py` being called without the GIL being held. The necessary synchronization to obtain and apply these reference count updates when PyO3-based code next acquires the GIL is somewhat expensive and can become a significant part of the cost of crossing the Python-Rust boundary. diff --git a/newsfragments/4768.added.md b/newsfragments/4768.added.md new file mode 100644 index 00000000000..1ce9c6f5b92 --- /dev/null +++ b/newsfragments/4768.added.md @@ -0,0 +1 @@ +Added `PyCallArgs` trait for arguments into the Python calling protocol. This enabled using a faster calling convention for certain types, improving performance. \ No newline at end of file diff --git a/newsfragments/4768.changed.md b/newsfragments/4768.changed.md new file mode 100644 index 00000000000..6b09fd0e093 --- /dev/null +++ b/newsfragments/4768.changed.md @@ -0,0 +1 @@ +`PyAnyMethods::call` an friends now require `PyCallArgs` for their positional arguments. \ No newline at end of file diff --git a/src/call.rs b/src/call.rs new file mode 100644 index 00000000000..1da1b67530b --- /dev/null +++ b/src/call.rs @@ -0,0 +1,150 @@ +//! Defines how Python calls are dispatched, see [`PyCallArgs`].for more information. + +use crate::ffi_ptr_ext::FfiPtrExt as _; +use crate::types::{PyAnyMethods as _, PyDict, PyString, PyTuple}; +use crate::{ffi, Borrowed, Bound, IntoPyObjectExt as _, Py, PyAny, PyResult}; + +pub(crate) mod private { + use super::*; + + pub trait Sealed {} + + impl Sealed for () {} + impl Sealed for Bound<'_, PyTuple> {} + impl Sealed for Py {} + + pub struct Token; +} + +/// This trait marks types that can be used as arguments to Python function +/// calls. +/// +/// This trait is currently implemented for Rust tuple (up to a size of 12), +/// [`Bound<'py, PyTuple>`] and [`Py`]. Custom types that are +/// convertable to `PyTuple` via `IntoPyObject` need to do so before passing it +/// to `call`. +/// +/// This trait is not intended to used by downstream crates directly. As such it +/// has no publicly available methods and cannot be implemented ouside of +/// `pyo3`. The corresponding public API is available through [`call`] +/// ([`call0`], [`call1`] and friends) on [`PyAnyMethods`]. +/// +/// # What is `PyCallArgs` used for? +/// `PyCallArgs` is used internally in `pyo3` to dispatch the Python calls in +/// the most optimal way for the current build configuration. Certain types, +/// such as Rust tuples, do allow the usage of a faster calling convention of +/// the Python interpreter (if available). More types that may take advantage +/// from this may be added in the future. +/// +/// [`call0`]: crate::types::PyAnyMethods::call0 +/// [`call1`]: crate::types::PyAnyMethods::call1 +/// [`call`]: crate::types::PyAnyMethods::call +/// [`PyAnyMethods`]: crate::types::PyAnyMethods +#[cfg_attr( + diagnostic_namespace, + diagnostic::on_unimplemented( + message = "`{Self}` cannot used as a Python `call` argument", + note = "`PyCallArgs` is implemented for Rust tuples, `Bound<'py, PyTuple>` and `Py`", + note = "if your type is convertable to `PyTuple` via `IntoPyObject`, call `.into_pyobject(py)` manually", + note = "if you meant to pass the type as a single argument, wrap it in a 1-tuple, `(,)`" + ) +)] +pub trait PyCallArgs<'py>: Sized + private::Sealed { + #[doc(hidden)] + fn call( + self, + function: Borrowed<'_, 'py, PyAny>, + kwargs: Borrowed<'_, 'py, PyDict>, + token: private::Token, + ) -> PyResult>; + + #[doc(hidden)] + fn call_positional( + self, + function: Borrowed<'_, 'py, PyAny>, + token: private::Token, + ) -> PyResult>; + + #[doc(hidden)] + fn call_method_positional( + self, + object: Borrowed<'_, 'py, PyAny>, + method_name: Borrowed<'_, 'py, PyString>, + _: private::Token, + ) -> PyResult> { + object + .getattr(method_name) + .and_then(|method| method.call1(self)) + } +} + +impl<'py> PyCallArgs<'py> for () { + fn call( + self, + function: Borrowed<'_, 'py, PyAny>, + kwargs: Borrowed<'_, 'py, PyDict>, + token: private::Token, + ) -> PyResult> { + let args = self.into_pyobject_or_pyerr(function.py())?; + args.call(function, kwargs, token) + } + + fn call_positional( + self, + function: Borrowed<'_, 'py, PyAny>, + token: private::Token, + ) -> PyResult> { + let args = self.into_pyobject_or_pyerr(function.py())?; + args.call_positional(function, token) + } +} + +impl<'py> PyCallArgs<'py> for Bound<'py, PyTuple> { + fn call( + self, + function: Borrowed<'_, 'py, PyAny>, + kwargs: Borrowed<'_, '_, PyDict>, + _: private::Token, + ) -> PyResult> { + unsafe { + ffi::PyObject_Call(function.as_ptr(), self.as_ptr(), kwargs.as_ptr()) + .assume_owned_or_err(function.py()) + } + } + + fn call_positional( + self, + function: Borrowed<'_, 'py, PyAny>, + _: private::Token, + ) -> PyResult> { + unsafe { + ffi::PyObject_Call(function.as_ptr(), self.as_ptr(), std::ptr::null_mut()) + .assume_owned_or_err(function.py()) + } + } +} + +impl<'py> PyCallArgs<'py> for Py { + fn call( + self, + function: Borrowed<'_, 'py, PyAny>, + kwargs: Borrowed<'_, '_, PyDict>, + _: private::Token, + ) -> PyResult> { + unsafe { + ffi::PyObject_Call(function.as_ptr(), self.as_ptr(), kwargs.as_ptr()) + .assume_owned_or_err(function.py()) + } + } + + fn call_positional( + self, + function: Borrowed<'_, 'py, PyAny>, + _: private::Token, + ) -> PyResult> { + unsafe { + ffi::PyObject_Call(function.as_ptr(), self.as_ptr(), std::ptr::null_mut()) + .assume_owned_or_err(function.py()) + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 46a2fd53d32..e5146c81c00 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -428,6 +428,7 @@ mod internal_tricks; mod internal; pub mod buffer; +pub mod call; pub mod conversion; mod conversions; #[cfg(feature = "experimental-async")] diff --git a/src/types/any.rs b/src/types/any.rs index d060c187631..1ebc5d40a0b 100644 --- a/src/types/any.rs +++ b/src/types/any.rs @@ -1,3 +1,4 @@ +use crate::call::PyCallArgs; use crate::class::basic::CompareOp; use crate::conversion::{AsPyPointer, FromPyObjectBound, IntoPyObject}; use crate::err::{DowncastError, DowncastIntoError, PyErr, PyResult}; @@ -10,7 +11,7 @@ use crate::py_result_ext::PyResultExt; use crate::type_object::{PyTypeCheck, PyTypeInfo}; #[cfg(not(any(PyPy, GraalPy)))] use crate::types::PySuper; -use crate::types::{PyDict, PyIterator, PyList, PyString, PyTuple, PyType}; +use crate::types::{PyDict, PyIterator, PyList, PyString, PyType}; use crate::{err, ffi, Borrowed, BoundObject, IntoPyObjectExt, Python}; use std::cell::UnsafeCell; use std::cmp::Ordering; @@ -436,7 +437,7 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed { /// ``` fn call(&self, args: A, kwargs: Option<&Bound<'py, PyDict>>) -> PyResult> where - A: IntoPyObject<'py, Target = PyTuple>; + A: PyCallArgs<'py>; /// Calls the object without arguments. /// @@ -491,7 +492,7 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed { /// ``` fn call1(&self, args: A) -> PyResult> where - A: IntoPyObject<'py, Target = PyTuple>; + A: PyCallArgs<'py>; /// Calls a method on the object. /// @@ -538,7 +539,7 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed { ) -> PyResult> where N: IntoPyObject<'py, Target = PyString>, - A: IntoPyObject<'py, Target = PyTuple>; + A: PyCallArgs<'py>; /// Calls a method on the object without arguments. /// @@ -614,7 +615,7 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed { fn call_method1(&self, name: N, args: A) -> PyResult> where N: IntoPyObject<'py, Target = PyString>, - A: IntoPyObject<'py, Target = PyTuple>; + A: PyCallArgs<'py>; /// Returns whether the object is considered to be true. /// @@ -1209,25 +1210,17 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { fn call(&self, args: A, kwargs: Option<&Bound<'py, PyDict>>) -> PyResult> where - A: IntoPyObject<'py, Target = PyTuple>, + A: PyCallArgs<'py>, { - fn inner<'py>( - any: &Bound<'py, PyAny>, - args: Borrowed<'_, 'py, PyTuple>, - kwargs: Option<&Bound<'py, PyDict>>, - ) -> PyResult> { - unsafe { - ffi::PyObject_Call( - any.as_ptr(), - args.as_ptr(), - kwargs.map_or(std::ptr::null_mut(), |dict| dict.as_ptr()), - ) - .assume_owned_or_err(any.py()) - } + if let Some(kwargs) = kwargs { + args.call( + self.as_borrowed(), + kwargs.as_borrowed(), + crate::call::private::Token, + ) + } else { + args.call_positional(self.as_borrowed(), crate::call::private::Token) } - - let py = self.py(); - inner(self, args.into_pyobject_or_pyerr(py)?.as_borrowed(), kwargs) } #[inline] @@ -1237,9 +1230,9 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { fn call1(&self, args: A) -> PyResult> where - A: IntoPyObject<'py, Target = PyTuple>, + A: PyCallArgs<'py>, { - self.call(args, None) + args.call_positional(self.as_borrowed(), crate::call::private::Token) } #[inline] @@ -1251,10 +1244,14 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { ) -> PyResult> where N: IntoPyObject<'py, Target = PyString>, - A: IntoPyObject<'py, Target = PyTuple>, + A: PyCallArgs<'py>, { - self.getattr(name) - .and_then(|method| method.call(args, kwargs)) + if kwargs.is_none() { + self.call_method1(name, args) + } else { + self.getattr(name) + .and_then(|method| method.call(args, kwargs)) + } } #[inline] @@ -1273,9 +1270,14 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { fn call_method1(&self, name: N, args: A) -> PyResult> where N: IntoPyObject<'py, Target = PyString>, - A: IntoPyObject<'py, Target = PyTuple>, + A: PyCallArgs<'py>, { - self.call_method(name, args, None) + let name = name.into_pyobject_or_pyerr(self.py())?; + args.call_method_positional( + self.as_borrowed(), + name.as_borrowed(), + crate::call::private::Token, + ) } fn is_truthy(&self) -> PyResult { diff --git a/src/types/tuple.rs b/src/types/tuple.rs index 216a376d833..a5de938140a 100644 --- a/src/types/tuple.rs +++ b/src/types/tuple.rs @@ -566,6 +566,255 @@ macro_rules! tuple_conversion ({$length:expr,$(($refN:ident, $n:tt, $T:ident)),+ } } + impl<'py, $($T),+> crate::call::private::Sealed for ($($T,)+) where $($T: IntoPyObject<'py>,)+ {} + impl<'py, $($T),+> crate::call::PyCallArgs<'py> for ($($T,)+) + where + $($T: IntoPyObject<'py>,)+ + { + #[cfg(all(Py_3_9, not(any(PyPy, GraalPy, Py_LIMITED_API))))] + fn call( + self, + function: Borrowed<'_, 'py, PyAny>, + kwargs: Borrowed<'_, '_, crate::types::PyDict>, + _: crate::call::private::Token, + ) -> PyResult> { + let py = function.py(); + // We need this to drop the arguments correctly. + let args_bound = [$(self.$n.into_bound_py_any(py)?,)*]; + // Prepend one null argument for `PY_VECTORCALL_ARGUMENTS_OFFSET`. + let mut args = [std::ptr::null_mut(), $(args_bound[$n].as_ptr()),*]; + unsafe { + ffi::PyObject_VectorcallDict( + function.as_ptr(), + args.as_mut_ptr().add(1), + $length + ffi::PY_VECTORCALL_ARGUMENTS_OFFSET, + kwargs.as_ptr(), + ) + .assume_owned_or_err(py) + } + } + + #[cfg(all(Py_3_9, not(any(PyPy, GraalPy, Py_LIMITED_API))))] + fn call_positional( + self, + function: Borrowed<'_, 'py, PyAny>, + _: crate::call::private::Token, + ) -> PyResult> { + let py = function.py(); + // We need this to drop the arguments correctly. + let args_bound = [$(self.$n.into_bound_py_any(py)?,)*]; + if $length == 1 { + unsafe { + ffi::PyObject_CallOneArg( + function.as_ptr(), + args_bound[0].as_ptr() + ) + .assume_owned_or_err(py) + } + } else { + // Prepend one null argument for `PY_VECTORCALL_ARGUMENTS_OFFSET`. + let mut args = [std::ptr::null_mut(), $(args_bound[$n].as_ptr()),*]; + unsafe { + ffi::PyObject_Vectorcall( + function.as_ptr(), + args.as_mut_ptr().add(1), + $length + ffi::PY_VECTORCALL_ARGUMENTS_OFFSET, + std::ptr::null_mut(), + ) + .assume_owned_or_err(py) + } + } + } + + #[cfg(all(Py_3_9, not(any(PyPy, GraalPy, Py_LIMITED_API))))] + fn call_method_positional( + self, + object: Borrowed<'_, 'py, PyAny>, + method_name: Borrowed<'_, 'py, crate::types::PyString>, + _: crate::call::private::Token, + ) -> PyResult> { + let py = object.py(); + // We need this to drop the arguments correctly. + let args_bound = [$(self.$n.into_bound_py_any(py)?,)*]; + if $length == 1 { + unsafe { + ffi::PyObject_CallMethodOneArg( + object.as_ptr(), + method_name.as_ptr(), + args_bound[0].as_ptr(), + ) + .assume_owned_or_err(py) + } + } else { + let mut args = [object.as_ptr(), $(args_bound[$n].as_ptr()),*]; + unsafe { + ffi::PyObject_VectorcallMethod( + method_name.as_ptr(), + args.as_mut_ptr(), + // +1 for the receiver. + 1 + $length + ffi::PY_VECTORCALL_ARGUMENTS_OFFSET, + std::ptr::null_mut(), + ) + .assume_owned_or_err(py) + } + } + } + + #[cfg(not(all(Py_3_9, not(any(PyPy, GraalPy, Py_LIMITED_API)))))] + fn call( + self, + function: Borrowed<'_, 'py, PyAny>, + kwargs: Borrowed<'_, 'py, crate::types::PyDict>, + token: crate::call::private::Token, + ) -> PyResult> { + self.into_pyobject_or_pyerr(function.py())?.call(function, kwargs, token) + } + + #[cfg(not(all(Py_3_9, not(any(PyPy, GraalPy, Py_LIMITED_API)))))] + fn call_positional( + self, + function: Borrowed<'_, 'py, PyAny>, + token: crate::call::private::Token, + ) -> PyResult> { + self.into_pyobject_or_pyerr(function.py())?.call_positional(function, token) + } + + #[cfg(not(all(Py_3_9, not(any(PyPy, GraalPy, Py_LIMITED_API)))))] + fn call_method_positional( + self, + object: Borrowed<'_, 'py, PyAny>, + method_name: Borrowed<'_, 'py, crate::types::PyString>, + token: crate::call::private::Token, + ) -> PyResult> { + self.into_pyobject_or_pyerr(object.py())?.call_method_positional(object, method_name, token) + } + } + + impl<'a, 'py, $($T),+> crate::call::private::Sealed for &'a ($($T,)+) where $(&'a $T: IntoPyObject<'py>,)+ $($T: 'a,)+ /*MSRV */ {} + impl<'a, 'py, $($T),+> crate::call::PyCallArgs<'py> for &'a ($($T,)+) + where + $(&'a $T: IntoPyObject<'py>,)+ + $($T: 'a,)+ // MSRV + { + #[cfg(all(Py_3_9, not(any(PyPy, GraalPy, Py_LIMITED_API))))] + fn call( + self, + function: Borrowed<'_, 'py, PyAny>, + kwargs: Borrowed<'_, '_, crate::types::PyDict>, + _: crate::call::private::Token, + ) -> PyResult> { + let py = function.py(); + // We need this to drop the arguments correctly. + let args_bound = [$(self.$n.into_bound_py_any(py)?,)*]; + // Prepend one null argument for `PY_VECTORCALL_ARGUMENTS_OFFSET`. + let mut args = [std::ptr::null_mut(), $(args_bound[$n].as_ptr()),*]; + unsafe { + ffi::PyObject_VectorcallDict( + function.as_ptr(), + args.as_mut_ptr().add(1), + $length + ffi::PY_VECTORCALL_ARGUMENTS_OFFSET, + kwargs.as_ptr(), + ) + .assume_owned_or_err(py) + } + } + + #[cfg(all(Py_3_9, not(any(PyPy, GraalPy, Py_LIMITED_API))))] + fn call_positional( + self, + function: Borrowed<'_, 'py, PyAny>, + _: crate::call::private::Token, + ) -> PyResult> { + let py = function.py(); + // We need this to drop the arguments correctly. + let args_bound = [$(self.$n.into_bound_py_any(py)?,)*]; + if $length == 1 { + unsafe { + ffi::PyObject_CallOneArg( + function.as_ptr(), + args_bound[0].as_ptr() + ) + .assume_owned_or_err(py) + } + } else { + // Prepend one null argument for `PY_VECTORCALL_ARGUMENTS_OFFSET`. + let mut args = [std::ptr::null_mut(), $(args_bound[$n].as_ptr()),*]; + unsafe { + ffi::PyObject_Vectorcall( + function.as_ptr(), + args.as_mut_ptr().add(1), + $length + ffi::PY_VECTORCALL_ARGUMENTS_OFFSET, + std::ptr::null_mut(), + ) + .assume_owned_or_err(py) + } + } + } + + #[cfg(all(Py_3_9, not(any(PyPy, GraalPy, Py_LIMITED_API))))] + fn call_method_positional( + self, + object: Borrowed<'_, 'py, PyAny>, + method_name: Borrowed<'_, 'py, crate::types::PyString>, + _: crate::call::private::Token, + ) -> PyResult> { + let py = object.py(); + // We need this to drop the arguments correctly. + let args_bound = [$(self.$n.into_bound_py_any(py)?,)*]; + if $length == 1 { + unsafe { + ffi::PyObject_CallMethodOneArg( + object.as_ptr(), + method_name.as_ptr(), + args_bound[0].as_ptr(), + ) + .assume_owned_or_err(py) + } + } else { + let mut args = [object.as_ptr(), $(args_bound[$n].as_ptr()),*]; + unsafe { + ffi::PyObject_VectorcallMethod( + method_name.as_ptr(), + args.as_mut_ptr(), + // +1 for the receiver. + 1 + $length + ffi::PY_VECTORCALL_ARGUMENTS_OFFSET, + std::ptr::null_mut(), + ) + .assume_owned_or_err(py) + } + } + } + + #[cfg(not(all(Py_3_9, not(any(PyPy, GraalPy, Py_LIMITED_API)))))] + fn call( + self, + function: Borrowed<'_, 'py, PyAny>, + kwargs: Borrowed<'_, 'py, crate::types::PyDict>, + token: crate::call::private::Token, + ) -> PyResult> { + self.into_pyobject_or_pyerr(function.py())?.call(function, kwargs, token) + } + + #[cfg(not(all(Py_3_9, not(any(PyPy, GraalPy, Py_LIMITED_API)))))] + fn call_positional( + self, + function: Borrowed<'_, 'py, PyAny>, + token: crate::call::private::Token, + ) -> PyResult> { + self.into_pyobject_or_pyerr(function.py())?.call_positional(function, token) + } + + #[cfg(not(all(Py_3_9, not(any(PyPy, GraalPy, Py_LIMITED_API)))))] + fn call_method_positional( + self, + object: Borrowed<'_, 'py, PyAny>, + method_name: Borrowed<'_, 'py, crate::types::PyString>, + token: crate::call::private::Token, + ) -> PyResult> { + self.into_pyobject_or_pyerr(object.py())?.call_method_positional(object, method_name, token) + } + } + #[allow(deprecated)] impl <$($T: IntoPy),+> IntoPy> for ($($T,)+) { fn into_py(self, py: Python<'_>) -> Py { diff --git a/tests/test_compile_error.rs b/tests/test_compile_error.rs index 05d9ccd6d2e..10b692a604c 100644 --- a/tests/test_compile_error.rs +++ b/tests/test_compile_error.rs @@ -20,6 +20,7 @@ fn test_compile_errors() { t.compile_fail("tests/ui/invalid_pymethod_enum.rs"); t.compile_fail("tests/ui/invalid_pymethod_names.rs"); t.compile_fail("tests/ui/invalid_pymodule_args.rs"); + t.compile_fail("tests/ui/invalid_pycallargs.rs"); t.compile_fail("tests/ui/reject_generics.rs"); t.compile_fail("tests/ui/invalid_closure.rs"); t.compile_fail("tests/ui/pyclass_send.rs"); diff --git a/tests/ui/invalid_pycallargs.rs b/tests/ui/invalid_pycallargs.rs new file mode 100644 index 00000000000..b77dbb20dcb --- /dev/null +++ b/tests/ui/invalid_pycallargs.rs @@ -0,0 +1,8 @@ +use pyo3::prelude::*; + +fn main() { + Python::with_gil(|py| { + let any = py.None().into_bound(py); + any.call1("foo"); + }) +} diff --git a/tests/ui/invalid_pycallargs.stderr b/tests/ui/invalid_pycallargs.stderr new file mode 100644 index 00000000000..93c0bc19b7f --- /dev/null +++ b/tests/ui/invalid_pycallargs.stderr @@ -0,0 +1,29 @@ +error[E0277]: `&str` cannot used as a Python `call` argument + --> tests/ui/invalid_pycallargs.rs:6:19 + | +6 | any.call1("foo"); + | ----- ^^^^^ the trait `PyCallArgs<'_>` is not implemented for `&str` + | | + | required by a bound introduced by this call + | + = note: `PyCallArgs` is implemented for Rust tuples, `Bound<'py, PyTuple>` and `Py` + = note: if your type is convertable to `PyTuple` via `IntoPyObject`, call `.into_pyobject(py)` manually + = note: if you meant to pass the type as a single argument, wrap it in a 1-tuple, `(,)` + = help: the following other types implement trait `PyCallArgs<'py>`: + &'a (T0, T1) + &'a (T0, T1, T2) + &'a (T0, T1, T2, T3) + &'a (T0, T1, T2, T3, T4) + &'a (T0, T1, T2, T3, T4, T5) + &'a (T0, T1, T2, T3, T4, T5, T6) + &'a (T0, T1, T2, T3, T4, T5, T6, T7) + &'a (T0, T1, T2, T3, T4, T5, T6, T7, T8) + and $N others +note: required by a bound in `call1` + --> src/types/any.rs + | + | fn call1(&self, args: A) -> PyResult> + | ----- required by a bound in this associated function + | where + | A: PyCallArgs<'py>; + | ^^^^^^^^^^^^^^^ required by this bound in `PyAnyMethods::call1` From a93998d6b8288ffba8f6320fb15ae3f1257f2d5b Mon Sep 17 00:00:00 2001 From: messense Date: Wed, 1 Jan 2025 05:58:58 +0800 Subject: [PATCH 416/495] Fix generating import lib for python3.13t when abi3 feature is enabled (#4808) --- newsfragments/4808.fixed.md | 1 + pyo3-build-config/Cargo.toml | 4 +- pyo3-build-config/src/impl_.rs | 108 +++++++++++++++++++++++++-------- 3 files changed, 86 insertions(+), 27 deletions(-) create mode 100644 newsfragments/4808.fixed.md diff --git a/newsfragments/4808.fixed.md b/newsfragments/4808.fixed.md new file mode 100644 index 00000000000..2e7c3a8a23c --- /dev/null +++ b/newsfragments/4808.fixed.md @@ -0,0 +1 @@ +Fix generating import lib for python3.13t when `abi3` feature is enabled. diff --git a/pyo3-build-config/Cargo.toml b/pyo3-build-config/Cargo.toml index 656a03d44b3..2378d30757f 100644 --- a/pyo3-build-config/Cargo.toml +++ b/pyo3-build-config/Cargo.toml @@ -13,11 +13,11 @@ rust-version = "1.63" [dependencies] once_cell = "1" -python3-dll-a = { version = "0.2.11", optional = true } +python3-dll-a = { version = "0.2.12", optional = true } target-lexicon = "0.13" [build-dependencies] -python3-dll-a = { version = "0.2.11", optional = true } +python3-dll-a = { version = "0.2.12", optional = true } target-lexicon = "0.13" [features] diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index e00e0aab963..2e5172d15a2 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -497,7 +497,7 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) let mut lib_dir = None; let mut executable = None; let mut pointer_width = None; - let mut build_flags = None; + let mut build_flags: Option = None; let mut suppress_build_script_link_lines = None; let mut extra_build_script_lines = vec![]; @@ -535,10 +535,12 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) let version = version.ok_or("missing value for version")?; let implementation = implementation.unwrap_or(PythonImplementation::CPython); let abi3 = abi3.unwrap_or(false); + let build_flags = build_flags.unwrap_or_default(); + let gil_disabled = build_flags.0.contains(&BuildFlag::Py_GIL_DISABLED); // Fixup lib_name if it's not set let lib_name = lib_name.or_else(|| { if let Ok(Ok(target)) = env::var("TARGET").map(|target| target.parse::()) { - default_lib_name_for_target(version, implementation, abi3, &target) + default_lib_name_for_target(version, implementation, abi3, gil_disabled, &target) } else { None } @@ -553,7 +555,7 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) lib_dir, executable, pointer_width, - build_flags: build_flags.unwrap_or_default(), + build_flags, suppress_build_script_link_lines: suppress_build_script_link_lines.unwrap_or(false), extra_build_script_lines, }) @@ -565,7 +567,10 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) // Auto generate python3.dll import libraries for Windows targets. if self.lib_dir.is_none() { let target = target_triple_from_env(); - let py_version = if self.implementation == PythonImplementation::CPython && self.abi3 { + let py_version = if self.implementation == PythonImplementation::CPython + && self.abi3 + && !self.is_free_threaded() + { None } else { Some(self.version) @@ -893,6 +898,9 @@ pub struct CrossCompileConfig { /// The compile target triple (e.g. aarch64-unknown-linux-gnu) target: Triple, + + /// Python ABI flags, used to detect free-threaded Python builds. + abiflags: Option, } impl CrossCompileConfig { @@ -907,7 +915,7 @@ impl CrossCompileConfig { ) -> Result> { if env_vars.any() || Self::is_cross_compiling_from_to(host, target) { let lib_dir = env_vars.lib_dir_path()?; - let version = env_vars.parse_version()?; + let (version, abiflags) = env_vars.parse_version()?; let implementation = env_vars.parse_implementation()?; let target = target.clone(); @@ -916,6 +924,7 @@ impl CrossCompileConfig { version, implementation, target, + abiflags, })) } else { Ok(None) @@ -989,22 +998,25 @@ impl CrossCompileEnvVars { } /// Parses `PYO3_CROSS_PYTHON_VERSION` environment variable value - /// into `PythonVersion`. - fn parse_version(&self) -> Result> { - let version = self - .pyo3_cross_python_version - .as_ref() - .map(|os_string| { + /// into `PythonVersion` and ABI flags. + fn parse_version(&self) -> Result<(Option, Option)> { + match self.pyo3_cross_python_version.as_ref() { + Some(os_string) => { let utf8_str = os_string .to_str() .ok_or("PYO3_CROSS_PYTHON_VERSION is not valid a UTF-8 string")?; - utf8_str + let (utf8_str, abiflags) = if let Some(version) = utf8_str.strip_suffix('t') { + (version, Some("t".to_string())) + } else { + (utf8_str, None) + }; + let version = utf8_str .parse() - .context("failed to parse PYO3_CROSS_PYTHON_VERSION") - }) - .transpose()?; - - Ok(version) + .context("failed to parse PYO3_CROSS_PYTHON_VERSION")?; + Ok((Some(version), abiflags)) + } + None => Ok((None, None)), + } } /// Parses `PYO3_CROSS_PYTHON_IMPLEMENTATION` environment variable value @@ -1530,16 +1542,27 @@ fn default_cross_compile(cross_compile_config: &CrossCompileConfig) -> Result Option { if target.operating_system == OperatingSystem::Windows { - Some(default_lib_name_windows(version, implementation, abi3, false, false, false).unwrap()) + Some( + default_lib_name_windows(version, implementation, abi3, false, false, gil_disabled) + .unwrap(), + ) } else if is_linking_libpython_for_target(target) { - Some(default_lib_name_unix(version, implementation, None, false).unwrap()) + Some(default_lib_name_unix(version, implementation, None, gil_disabled).unwrap()) } else { None } @@ -1906,7 +1933,14 @@ pub fn make_interpreter_config() -> Result { // Auto generate python3.dll import libraries for Windows targets. #[cfg(feature = "python3-dll-a")] { - let py_version = if interpreter_config.abi3 { + let gil_disabled = interpreter_config + .build_flags + .0 + .contains(&BuildFlag::Py_GIL_DISABLED); + let py_version = if interpreter_config.implementation == PythonImplementation::CPython + && interpreter_config.abi3 + && !gil_disabled + { None } else { Some(interpreter_config.version) @@ -2706,7 +2740,7 @@ mod tests { assert_eq!( env_vars.parse_version().unwrap(), - Some(PythonVersion { major: 3, minor: 9 }) + (Some(PythonVersion { major: 3, minor: 9 }), None), ); let env_vars = CrossCompileEnvVars { @@ -2716,7 +2750,25 @@ mod tests { pyo3_cross_python_implementation: None, }; - assert_eq!(env_vars.parse_version().unwrap(), None); + assert_eq!(env_vars.parse_version().unwrap(), (None, None)); + + let env_vars = CrossCompileEnvVars { + pyo3_cross: None, + pyo3_cross_lib_dir: None, + pyo3_cross_python_version: Some("3.13t".into()), + pyo3_cross_python_implementation: None, + }; + + assert_eq!( + env_vars.parse_version().unwrap(), + ( + Some(PythonVersion { + major: 3, + minor: 13 + }), + Some("t".into()) + ), + ); let env_vars = CrossCompileEnvVars { pyo3_cross: None, @@ -2799,6 +2851,11 @@ mod tests { version: Some(interpreter_config.version), implementation: Some(interpreter_config.implementation), target: triple!("x86_64-unknown-linux-gnu"), + abiflags: if interpreter_config.is_free_threaded() { + Some("t".into()) + } else { + None + }, }; let sysconfigdata_path = match find_sysconfigdata(&cross) { @@ -3074,6 +3131,7 @@ mod tests { version: None, implementation: None, target: triple!("x86_64-unknown-linux-gnu"), + abiflags: None, }) .unwrap_err(); From 54735daf3fe571913852bf91f7505fcb494de434 Mon Sep 17 00:00:00 2001 From: Jiahao Yuan Date: Wed, 1 Jan 2025 05:59:19 +0800 Subject: [PATCH 417/495] fix: cross-compilation compatibility checks for Windows (#4800) * fix: cross-compilation compatibility checks for Windows * add newsfragments * add simple test --- newsfragments/4800.fixed.md | 1 + pyo3-build-config/src/impl_.rs | 16 ++++++++++++++-- 2 files changed, 15 insertions(+), 2 deletions(-) create mode 100644 newsfragments/4800.fixed.md diff --git a/newsfragments/4800.fixed.md b/newsfragments/4800.fixed.md new file mode 100644 index 00000000000..615e622a963 --- /dev/null +++ b/newsfragments/4800.fixed.md @@ -0,0 +1 @@ +fix: cross-compilation compatibility checks for Windows diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index 2e5172d15a2..6a7bb03fcb2 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -22,7 +22,7 @@ use std::{ pub use target_lexicon::Triple; -use target_lexicon::{Environment, OperatingSystem}; +use target_lexicon::{Architecture, Environment, OperatingSystem}; use crate::{ bail, ensure, @@ -944,7 +944,9 @@ impl CrossCompileConfig { // Not cross-compiling to compile for 32-bit Python from windows 64-bit compatible |= target.operating_system == OperatingSystem::Windows - && host.operating_system == OperatingSystem::Windows; + && host.operating_system == OperatingSystem::Windows + && matches!(target.architecture, Architecture::X86_32(_)) + && host.architecture == Architecture::X86_64; // Not cross-compiling to compile for x86-64 Python from macOS arm64 and vice versa compatible |= matches!(target.operating_system, OperatingSystem::Darwin(_)) @@ -2955,6 +2957,16 @@ mod tests { .is_none()); } + #[test] + fn test_is_cross_compiling_from_to() { + assert!(cross_compiling_from_to( + &triple!("x86_64-pc-windows-msvc"), + &triple!("aarch64-pc-windows-msvc") + ) + .unwrap() + .is_some()); + } + #[test] fn test_run_python_script() { // as above, this should be okay in CI where Python is presumed installed From 352ab06edb527f826335b80f06172fd4e6b8fd50 Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Tue, 31 Dec 2024 23:00:09 +0100 Subject: [PATCH 418/495] fix error with complex enums with many fields (#4832) --- newsfragments/4832.fixed.md | 1 + pyo3-macros-backend/src/pyclass.rs | 47 ++++++++++++++--------------- pyo3-macros-backend/src/pymethod.rs | 2 +- src/tests/hygiene/pyclass.rs | 38 +++++++++++++++++++++++ 4 files changed, 63 insertions(+), 25 deletions(-) create mode 100644 newsfragments/4832.fixed.md diff --git a/newsfragments/4832.fixed.md b/newsfragments/4832.fixed.md new file mode 100644 index 00000000000..13df6deae57 --- /dev/null +++ b/newsfragments/4832.fixed.md @@ -0,0 +1 @@ +`#[pyclass]` complex enums support more than 12 variant fields. \ No newline at end of file diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index 914cd19c1a0..d9fb6652017 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -19,8 +19,9 @@ use crate::method::{FnArg, FnSpec, PyArg, RegularArg}; use crate::pyfunction::ConstructorAttribute; use crate::pyimpl::{gen_py_const, get_cfg_attributes, PyClassMethodsType}; use crate::pymethod::{ - impl_py_getter_def, impl_py_setter_def, MethodAndMethodDef, MethodAndSlotDef, PropertyType, - SlotDef, __GETITEM__, __HASH__, __INT__, __LEN__, __REPR__, __RICHCMP__, __STR__, + impl_py_class_attribute, impl_py_getter_def, impl_py_setter_def, MethodAndMethodDef, + MethodAndSlotDef, PropertyType, SlotDef, __GETITEM__, __HASH__, __INT__, __LEN__, __REPR__, + __RICHCMP__, __STR__, }; use crate::pyversions::is_abi3_before; use crate::utils::{self, apply_renaming_rule, Ctx, LitCStr, PythonDoc}; @@ -1185,34 +1186,30 @@ fn impl_complex_enum_variant_cls( } fn impl_complex_enum_variant_match_args( - ctx: &Ctx, + ctx @ Ctx { pyo3_path, .. }: &Ctx, variant_cls_type: &syn::Type, field_names: &mut Vec, -) -> (MethodAndMethodDef, syn::ImplItemConst) { +) -> syn::Result<(MethodAndMethodDef, syn::ImplItemFn)> { let ident = format_ident!("__match_args__"); - let match_args_const_impl: syn::ImplItemConst = { - let args_tp = field_names.iter().map(|_| { - quote! { &'static str } - }); + let mut match_args_impl: syn::ImplItemFn = { parse_quote! { - #[allow(non_upper_case_globals)] - const #ident: ( #(#args_tp,)* ) = ( - #(stringify!(#field_names),)* - ); + #[classattr] + fn #ident(py: #pyo3_path::Python<'_>) -> #pyo3_path::PyResult<#pyo3_path::Bound<'_, #pyo3_path::types::PyTuple>> { + #pyo3_path::types::PyTuple::new::<&str, _>(py, [ + #(stringify!(#field_names),)* + ]) + } } }; - let spec = ConstSpec { - rust_ident: ident, - attributes: ConstAttributes { - is_class_attr: true, - name: None, - }, - }; - - let variant_match_args = gen_py_const(variant_cls_type, &spec, ctx); + let spec = FnSpec::parse( + &mut match_args_impl.sig, + &mut match_args_impl.attrs, + Default::default(), + )?; + let variant_match_args = impl_py_class_attribute(variant_cls_type, &spec, ctx)?; - (variant_match_args, match_args_const_impl) + Ok((variant_match_args, match_args_impl)) } fn impl_complex_enum_struct_variant_cls( @@ -1260,7 +1257,7 @@ fn impl_complex_enum_struct_variant_cls( } let (variant_match_args, match_args_const_impl) = - impl_complex_enum_variant_match_args(ctx, &variant_cls_type, &mut field_names); + impl_complex_enum_variant_match_args(ctx, &variant_cls_type, &mut field_names)?; field_getters.push(variant_match_args); @@ -1268,6 +1265,7 @@ fn impl_complex_enum_struct_variant_cls( #[doc(hidden)] #[allow(non_snake_case)] impl #variant_cls { + #[allow(clippy::too_many_arguments)] fn __pymethod_constructor__(py: #pyo3_path::Python<'_>, #(#fields_with_types,)*) -> #pyo3_path::PyClassInitializer<#variant_cls> { let base_value = #enum_name::#variant_ident { #(#field_names,)* }; <#pyo3_path::PyClassInitializer<#enum_name> as ::std::convert::From<#enum_name>>::from(base_value).add_subclass(#variant_cls) @@ -1434,7 +1432,7 @@ fn impl_complex_enum_tuple_variant_cls( slots.push(variant_getitem); let (variant_match_args, match_args_method_impl) = - impl_complex_enum_variant_match_args(ctx, &variant_cls_type, &mut field_names); + impl_complex_enum_variant_match_args(ctx, &variant_cls_type, &mut field_names)?; field_getters.push(variant_match_args); @@ -1442,6 +1440,7 @@ fn impl_complex_enum_tuple_variant_cls( #[doc(hidden)] #[allow(non_snake_case)] impl #variant_cls { + #[allow(clippy::too_many_arguments)] fn __pymethod_constructor__(py: #pyo3_path::Python<'_>, #(#field_names : #field_types,)*) -> #pyo3_path::PyClassInitializer<#variant_cls> { let base_value = #enum_name::#variant_ident ( #(#field_names,)* ); <#pyo3_path::PyClassInitializer<#enum_name> as ::std::convert::From<#enum_name>>::from(base_value).add_subclass(#variant_cls) diff --git a/pyo3-macros-backend/src/pymethod.rs b/pyo3-macros-backend/src/pymethod.rs index 3d2975e4885..c21f6d4556e 100644 --- a/pyo3-macros-backend/src/pymethod.rs +++ b/pyo3-macros-backend/src/pymethod.rs @@ -526,7 +526,7 @@ fn impl_clear_slot(cls: &syn::Type, spec: &FnSpec<'_>, ctx: &Ctx) -> syn::Result }) } -fn impl_py_class_attribute( +pub(crate) fn impl_py_class_attribute( cls: &syn::Type, spec: &FnSpec<'_>, ctx: &Ctx, diff --git a/src/tests/hygiene/pyclass.rs b/src/tests/hygiene/pyclass.rs index 4270da34be3..17c3ce41e11 100644 --- a/src/tests/hygiene/pyclass.rs +++ b/src/tests/hygiene/pyclass.rs @@ -92,6 +92,44 @@ pub enum TupleEnumEqOrd { Variant2(u32), } +#[crate::pyclass(crate = "crate")] +pub enum ComplexEnumManyVariantFields { + ManyStructFields { + field_1: u16, + field_2: u32, + field_3: u32, + field_4: i32, + field_5: u32, + field_6: u32, + field_7: u8, + field_8: u32, + field_9: i32, + field_10: u32, + field_11: u32, + field_12: u32, + field_13: u32, + field_14: i16, + field_15: u32, + }, + ManyTupleFields( + u16, + u32, + u32, + i32, + u32, + u32, + u8, + u32, + i32, + u32, + u32, + u32, + u32, + i16, + u32, + ), +} + #[crate::pyclass(str = "{x}, {y}, {z}")] #[pyo3(crate = "crate")] pub struct PointFmt { From e33dcdba9751c4635e318b1e2d426f06b5af3914 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Wed, 1 Jan 2025 07:01:19 -0700 Subject: [PATCH 419/495] docs: Expand docs on when and why allow_threads is necessary (#4767) * Expand docs on when and why allow_threads is necessary * spelling * simplify example a little * use less indirection in the example * Update guide/src/parallelism.md * Add note about the GIL preventing parallelism * Update guide/src/free-threading.md Co-authored-by: Bruno Kolenbrander <59372212+mejrs@users.noreply.github.com> * pared down text about need to use with_gil * rearrange slightly --------- Co-authored-by: Bruno Kolenbrander <59372212+mejrs@users.noreply.github.com> --- guide/src/free-threading.md | 42 +++++++++++++++++++------- guide/src/parallelism.md | 59 ++++++++++++++++++++++++++++++++++++- 2 files changed, 89 insertions(+), 12 deletions(-) diff --git a/guide/src/free-threading.md b/guide/src/free-threading.md index 573fd7b2609..8ee9a2e100e 100644 --- a/guide/src/free-threading.md +++ b/guide/src/free-threading.md @@ -156,20 +156,40 @@ freethreaded build, holding a `'py` lifetime means only that the thread is currently attached to the Python interpreter -- other threads can be simultaneously interacting with the interpreter. -The main reason for obtaining a `'py` lifetime is to interact with Python +You still need to obtain a `'py` lifetime is to interact with Python objects or call into the CPython C API. If you are not yet attached to the Python runtime, you can register a thread using the [`Python::with_gil`] function. Threads created via the Python [`threading`] module do not not need to -do this, but all other OS threads that interact with the Python runtime must -explicitly attach using `with_gil` and obtain a `'py` liftime. - -Since there is no GIL in the free-threaded build, releasing the GIL for -long-running tasks is no longer necessary to ensure other threads run, but you -should still detach from the interpreter runtime using [`Python::allow_threads`] -when doing long-running tasks that do not require the CPython runtime. The -garbage collector can only run if all threads are detached from the runtime (in -a stop-the-world state), so detaching from the runtime allows freeing unused -memory. +do this, and pyo3 will handle setting up the [`Python<'py>`] token when CPython +calls into your extension. + +### Global synchronization events can cause hangs and deadlocks + +The free-threaded build triggers global synchronization events in the following +situations: + +* During garbage collection in order to get a globally consistent view of + reference counts and references between objects +* In Python 3.13, when the first background thread is started in + order to mark certain objects as immortal +* When either `sys.settrace` or `sys.setprofile` are called in order to + instrument running code objects and threads +* Before `os.fork()` is called. + +This is a non-exhaustive list and there may be other situations in future Python +versions that can trigger global synchronization events. + +This means that you should detach from the interpreter runtime using +[`Python::allow_threads`] in exactly the same situations as you should detach +from the runtime in the GIL-enabled build: when doing long-running tasks that do +not require the CPython runtime or when doing any task that needs to re-attach +to the runtime (see the [guide +section](parallelism.md#sharing-python-objects-between-rust-threads) that +covers this). In the former case, you would observe a hang on threads that are +waiting on the long-running task to complete, and in the latter case you would +see a deadlock while a thread tries to attach after the runtime triggers a +global synchronization event, but the spawning thread prevents the +synchronization event from completing. ### Exceptions and panics for multithreaded access of mutable `pyclass` instances diff --git a/guide/src/parallelism.md b/guide/src/parallelism.md index a288b14be19..64ff1c8c9c0 100644 --- a/guide/src/parallelism.md +++ b/guide/src/parallelism.md @@ -1,6 +1,6 @@ # Parallelism -CPython has the infamous [Global Interpreter Lock](https://docs.python.org/3/glossary.html#term-global-interpreter-lock), which prevents several threads from executing Python bytecode in parallel. This makes threading in Python a bad fit for [CPU-bound](https://en.wikipedia.org/wiki/CPU-bound) tasks and often forces developers to accept the overhead of multiprocessing. +CPython has the infamous [Global Interpreter Lock](https://docs.python.org/3/glossary.html#term-global-interpreter-lock) (GIL), which prevents several threads from executing Python bytecode in parallel. This makes threading in Python a bad fit for [CPU-bound](https://en.wikipedia.org/wiki/CPU-bound) tasks and often forces developers to accept the overhead of multiprocessing. There is an experimental "free-threaded" version of CPython 3.13 that does not have a GIL, see the PyO3 docs on [free-threaded Python](./free-threading.md) for more information about that. In PyO3 parallelism can be easily achieved in Rust-only code. Let's take a look at our [word-count](https://github.com/PyO3/pyo3/blob/main/examples/word-count/src/lib.rs) example, where we have a `search` function that utilizes the [rayon](https://github.com/rayon-rs/rayon) crate to count words in parallel. ```rust,no_run @@ -117,4 +117,61 @@ test_word_count_python_sequential 27.3985 (15.82) 45.452 You can see that the Python threaded version is not much slower than the Rust sequential version, which means compared to an execution on a single CPU core the speed has doubled. +## Sharing Python objects between Rust threads + +In the example above we made a Python interface to a low-level rust function, +and then leveraged the python `threading` module to run the low-level function +in parallel. It is also possible to spawn threads in Rust that acquire the GIL +and operate on Python objects. However, care must be taken to avoid writing code +that deadlocks with the GIL in these cases. + +* Note: This example is meant to illustrate how to drop and re-acquire the GIL + to avoid creating deadlocks. Unless the spawned threads subsequently + release the GIL or you are using the free-threaded build of CPython, you + will not see any speedups due to multi-threaded parallelism using `rayon` + to parallelize code that acquires and holds the GIL for the entire + execution of the spawned thread. + +In the example below, we share a `Vec` of User ID objects defined using the +`pyclass` macro and spawn threads to process the collection of data into a `Vec` +of booleans based on a predicate using a rayon parallel iterator: + +```rust,no_run +use pyo3::prelude::*; + +// These traits let us use int_par_iter and map +use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; + +#[pyclass] +struct UserID { + id: i64, +} + +let allowed_ids: Vec = Python::with_gil(|outer_py| { + let instances: Vec> = (0..10).map(|x| Py::new(outer_py, UserID { id: x }).unwrap()).collect(); + outer_py.allow_threads(|| { + instances.par_iter().map(|instance| { + Python::with_gil(|inner_py| { + instance.borrow(inner_py).id > 5 + }) + }).collect() + }) +}); +assert!(allowed_ids.into_iter().filter(|b| *b).count() == 4); +``` + +It's important to note that there is an `outer_py` GIL lifetime token as well as +an `inner_py` token. Sharing GIL lifetime tokens between threads is not allowed +and threads must individually acquire the GIL to access data wrapped by a python +object. + +It's also important to see that this example uses [`Python::allow_threads`] to +wrap the code that spawns OS threads via `rayon`. If this example didn't use +`allow_threads`, a rayon worker thread would block on acquiring the GIL while a +thread that owns the GIL spins forever waiting for the result of the rayon +thread. Calling `allow_threads` allows the GIL to be released in the thread +collecting the results from the worker threads. You should always call +`allow_threads` in situations that spawn worker threads, but especially so in +cases where worker threads need to acquire the GIL, to prevent deadlocks. + [`Python::allow_threads`]: {{#PYO3_DOCS_URL}}/pyo3/marker/struct.Python.html#method.allow_threads From 5c0be2c9dc8d0908d33f551533bde1f41a658b09 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Wed, 1 Jan 2025 14:49:39 +0000 Subject: [PATCH 420/495] ci: add more tests for cross-compilation (#4773) --- .github/workflows/ci.yml | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5042ac9cee3..f4b13f80dd7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -656,14 +656,17 @@ jobs: # ubuntu x86_64 -> windows x86_64 - os: "ubuntu-latest" target: "x86_64-pc-windows-gnu" - flags: "-i python3.12 --features abi3 --features generate-import-lib" - manylinux: off + flags: "-i python3.12 --features generate-import-lib" # macos x86_64 -> aarch64 - os: "macos-13" # last x86_64 macos runners target: "aarch64-apple-darwin" # macos aarch64 -> x86_64 - os: "macos-latest" target: "x86_64-apple-darwin" + # windows x86_64 -> aarch64 + - os: "windows-latest" + target: "aarch64-pc-windows-msvc" + flags: "-i python3.12 --features generate-import-lib" steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 @@ -677,11 +680,18 @@ jobs: - name: Setup cross-compiler if: ${{ matrix.target == 'x86_64-pc-windows-gnu' }} run: sudo apt-get install -y mingw-w64 llvm - - uses: PyO3/maturin-action@v1 + - name: Compile version-specific library + uses: PyO3/maturin-action@v1 with: target: ${{ matrix.target }} manylinux: ${{ matrix.manylinux }} args: --release -m examples/maturin-starter/Cargo.toml ${{ matrix.flags }} + - name: Compile abi3 library + uses: PyO3/maturin-action@v1 + with: + target: ${{ matrix.target }} + manylinux: ${{ matrix.manylinux }} + args: --release -m examples/maturin-starter/Cargo.toml --features abi3 ${{ matrix.flags }} test-cross-compilation-windows: needs: [fmt] From f08bc95ee0cc0e9fb06d25fdc04cdada49e36777 Mon Sep 17 00:00:00 2001 From: messense Date: Thu, 2 Jan 2025 14:45:10 +0800 Subject: [PATCH 421/495] Add an API to set rpath when using macOS system Python (#4833) --- build.rs | 7 ++- guide/src/building-and-distribution.md | 22 +++---- newsfragments/4833.added.md | 1 + pyo3-build-config/src/impl_.rs | 37 ++++++++++++ pyo3-build-config/src/lib.rs | 83 ++++++++++++++++++++++++++ 5 files changed, 138 insertions(+), 12 deletions(-) create mode 100644 newsfragments/4833.added.md diff --git a/build.rs b/build.rs index 5d638291f3b..68a658bf285 100644 --- a/build.rs +++ b/build.rs @@ -1,7 +1,9 @@ use std::env; use pyo3_build_config::pyo3_build_script_impl::{cargo_env_var, errors::Result}; -use pyo3_build_config::{bail, print_feature_cfgs, InterpreterConfig}; +use pyo3_build_config::{ + add_python_framework_link_args, bail, print_feature_cfgs, InterpreterConfig, +}; fn ensure_auto_initialize_ok(interpreter_config: &InterpreterConfig) -> Result<()> { if cargo_env_var("CARGO_FEATURE_AUTO_INITIALIZE").is_some() && !interpreter_config.shared { @@ -42,6 +44,9 @@ fn configure_pyo3() -> Result<()> { // Emit cfgs like `invalid_from_utf8_lint` print_feature_cfgs(); + // Make `cargo test` etc work on macOS with Xcode bundled Python + add_python_framework_link_args(); + Ok(()) } diff --git a/guide/src/building-and-distribution.md b/guide/src/building-and-distribution.md index 1a806304d22..d3474fedaf7 100644 --- a/guide/src/building-and-distribution.md +++ b/guide/src/building-and-distribution.md @@ -144,7 +144,17 @@ rustflags = [ ] ``` -Using the MacOS system python3 (`/usr/bin/python3`, as opposed to python installed via homebrew, pyenv, nix, etc.) may result in runtime errors such as `Library not loaded: @rpath/Python3.framework/Versions/3.8/Python3`. These can be resolved with another addition to `.cargo/config.toml`: +Using the MacOS system python3 (`/usr/bin/python3`, as opposed to python installed via homebrew, pyenv, nix, etc.) may result in runtime errors such as `Library not loaded: @rpath/Python3.framework/Versions/3.8/Python3`. + +The easiest way to set the correct linker arguments is to add a `build.rs` with the following content: + +```rust,ignore +fn main() { + pyo3_build_config::add_python_framework_link_args(); +} +``` + +Alternatively it can be resolved with another addition to `.cargo/config.toml`: ```toml [build] @@ -153,16 +163,6 @@ rustflags = [ ] ``` -Alternatively, one can include in `build.rs`: - -```rust -fn main() { - println!( - "cargo:rustc-link-arg=-Wl,-rpath,/Library/Developer/CommandLineTools/Library/Frameworks" - ); -} -``` - For more discussion on and workarounds for MacOS linking problems [see this issue](https://github.com/PyO3/pyo3/issues/1800#issuecomment-906786649). Finally, don't forget that on MacOS the `extension-module` feature will cause `cargo test` to fail without the `--no-default-features` flag (see [the FAQ](https://pyo3.rs/main/faq.html#i-cant-run-cargo-test-or-i-cant-build-in-a-cargo-workspace-im-having-linker-issues-like-symbol-not-found-or-undefined-reference-to-_pyexc_systemerror)). diff --git a/newsfragments/4833.added.md b/newsfragments/4833.added.md new file mode 100644 index 00000000000..4e1e0005305 --- /dev/null +++ b/newsfragments/4833.added.md @@ -0,0 +1 @@ +Add `pyo3_build_config::add_python_framework_link_args` build script API to set rpath when using macOS system Python. diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index 6a7bb03fcb2..05bb58ac7aa 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -167,6 +167,8 @@ pub struct InterpreterConfig { /// /// Serialized to multiple `extra_build_script_line` values. pub extra_build_script_lines: Vec, + /// macOS Python3.framework requires special rpath handling + pub python_framework_prefix: Option, } impl InterpreterConfig { @@ -245,6 +247,7 @@ WINDOWS = platform.system() == "Windows" # macOS framework packages use shared linking FRAMEWORK = bool(get_config_var("PYTHONFRAMEWORK")) +FRAMEWORK_PREFIX = get_config_var("PYTHONFRAMEWORKPREFIX") # unix-style shared library enabled SHARED = bool(get_config_var("Py_ENABLE_SHARED")) @@ -253,6 +256,7 @@ print("implementation", platform.python_implementation()) print("version_major", sys.version_info[0]) print("version_minor", sys.version_info[1]) print("shared", PYPY or GRAALPY or ANACONDA or WINDOWS or FRAMEWORK or SHARED) +print("python_framework_prefix", FRAMEWORK_PREFIX) print_if_set("ld_version", get_config_var("LDVERSION")) print_if_set("libdir", get_config_var("LIBDIR")) print_if_set("base_prefix", base_prefix) @@ -289,6 +293,7 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) }; let shared = map["shared"].as_str() == "True"; + let python_framework_prefix = map.get("python_framework_prefix").cloned(); let version = PythonVersion { major: map["version_major"] @@ -359,6 +364,7 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) build_flags: BuildFlags::from_interpreter(interpreter)?, suppress_build_script_link_lines: false, extra_build_script_lines: vec![], + python_framework_prefix, }) } @@ -396,6 +402,9 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) Some(s) => !s.is_empty(), _ => false, }; + let python_framework_prefix = sysconfigdata + .get_value("PYTHONFRAMEWORKPREFIX") + .map(str::to_string); let lib_dir = get_key!(sysconfigdata, "LIBDIR").ok().map(str::to_string); let gil_disabled = match sysconfigdata.get_value("Py_GIL_DISABLED") { Some(value) => value == "1", @@ -424,6 +433,7 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) build_flags, suppress_build_script_link_lines: false, extra_build_script_lines: vec![], + python_framework_prefix, }) } @@ -500,6 +510,7 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) let mut build_flags: Option = None; let mut suppress_build_script_link_lines = None; let mut extra_build_script_lines = vec![]; + let mut python_framework_prefix = None; for (i, line) in lines.enumerate() { let line = line.context("failed to read line from config")?; @@ -528,6 +539,7 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) "extra_build_script_line" => { extra_build_script_lines.push(value.to_string()); } + "python_framework_prefix" => parse_value!(python_framework_prefix, value), unknown => warn!("unknown config key `{}`", unknown), } } @@ -558,6 +570,7 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) build_flags, suppress_build_script_link_lines: suppress_build_script_link_lines.unwrap_or(false), extra_build_script_lines, + python_framework_prefix, }) } @@ -650,6 +663,7 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) write_option_line!(executable)?; write_option_line!(pointer_width)?; write_line!(build_flags)?; + write_option_line!(python_framework_prefix)?; write_line!(suppress_build_script_link_lines)?; for line in &self.extra_build_script_lines { writeln!(writer, "extra_build_script_line={}", line) @@ -1587,6 +1601,7 @@ fn default_cross_compile(cross_compile_config: &CrossCompileConfig) -> Result Result = Vec::new(); config.to_writer(&mut buf).unwrap(); @@ -2039,6 +2056,7 @@ mod tests { }, suppress_build_script_link_lines: false, extra_build_script_lines: vec![], + python_framework_prefix: None, }; let mut buf: Vec = Vec::new(); config.to_writer(&mut buf).unwrap(); @@ -2060,6 +2078,7 @@ mod tests { version: MINIMUM_SUPPORTED_VERSION, suppress_build_script_link_lines: true, extra_build_script_lines: vec!["cargo:test1".to_string(), "cargo:test2".to_string()], + python_framework_prefix: None, }; let mut buf: Vec = Vec::new(); config.to_writer(&mut buf).unwrap(); @@ -2086,6 +2105,7 @@ mod tests { build_flags: BuildFlags::default(), suppress_build_script_link_lines: false, extra_build_script_lines: vec![], + python_framework_prefix: None, } ) } @@ -2108,6 +2128,7 @@ mod tests { build_flags: BuildFlags::default(), suppress_build_script_link_lines: false, extra_build_script_lines: vec![], + python_framework_prefix: None, } ) } @@ -2210,6 +2231,7 @@ mod tests { version: PythonVersion::PY37, suppress_build_script_link_lines: false, extra_build_script_lines: vec![], + python_framework_prefix: None, } ); } @@ -2239,6 +2261,7 @@ mod tests { version: PythonVersion::PY37, suppress_build_script_link_lines: false, extra_build_script_lines: vec![], + python_framework_prefix: None, } ); @@ -2265,6 +2288,7 @@ mod tests { version: PythonVersion::PY37, suppress_build_script_link_lines: false, extra_build_script_lines: vec![], + python_framework_prefix: None, } ); } @@ -2288,6 +2312,7 @@ mod tests { build_flags: BuildFlags::default(), suppress_build_script_link_lines: false, extra_build_script_lines: vec![], + python_framework_prefix: None, } ); } @@ -2311,6 +2336,7 @@ mod tests { build_flags: BuildFlags::default(), suppress_build_script_link_lines: false, extra_build_script_lines: vec![], + python_framework_prefix: None, } ); } @@ -2345,6 +2371,7 @@ mod tests { build_flags: BuildFlags::default(), suppress_build_script_link_lines: false, extra_build_script_lines: vec![], + python_framework_prefix: None, } ); } @@ -2379,6 +2406,7 @@ mod tests { build_flags: BuildFlags::default(), suppress_build_script_link_lines: false, extra_build_script_lines: vec![], + python_framework_prefix: None, } ); } @@ -2413,6 +2441,7 @@ mod tests { build_flags: BuildFlags::default(), suppress_build_script_link_lines: false, extra_build_script_lines: vec![], + python_framework_prefix: None, } ); } @@ -2449,6 +2478,7 @@ mod tests { build_flags: BuildFlags::default(), suppress_build_script_link_lines: false, extra_build_script_lines: vec![], + python_framework_prefix: None, } ); } @@ -2796,6 +2826,7 @@ mod tests { version: PythonVersion { major: 3, minor: 7 }, suppress_build_script_link_lines: false, extra_build_script_lines: vec![], + python_framework_prefix: None, }; config @@ -2818,6 +2849,7 @@ mod tests { version: PythonVersion { major: 3, minor: 7 }, suppress_build_script_link_lines: false, extra_build_script_lines: vec![], + python_framework_prefix: None, }; assert!(config @@ -2882,6 +2914,7 @@ mod tests { version: interpreter_config.version, suppress_build_script_link_lines: false, extra_build_script_lines: vec![], + python_framework_prefix: None, } ) } @@ -3006,6 +3039,7 @@ mod tests { build_flags: BuildFlags::default(), suppress_build_script_link_lines: false, extra_build_script_lines: vec![], + python_framework_prefix: None, }; assert_eq!( interpreter_config.build_script_outputs(), @@ -3045,6 +3079,7 @@ mod tests { build_flags: BuildFlags::default(), suppress_build_script_link_lines: false, extra_build_script_lines: vec![], + python_framework_prefix: None, }; assert_eq!( @@ -3092,6 +3127,7 @@ mod tests { build_flags, suppress_build_script_link_lines: false, extra_build_script_lines: vec![], + python_framework_prefix: None, }; assert_eq!( @@ -3125,6 +3161,7 @@ mod tests { build_flags, suppress_build_script_link_lines: false, extra_build_script_lines: vec![], + python_framework_prefix: None, }; assert_eq!( diff --git a/pyo3-build-config/src/lib.rs b/pyo3-build-config/src/lib.rs index 420b81c738b..9070f6d7401 100644 --- a/pyo3-build-config/src/lib.rs +++ b/pyo3-build-config/src/lib.rs @@ -74,6 +74,44 @@ fn _add_extension_module_link_args(triple: &Triple, mut writer: impl std::io::Wr } } +/// Adds linker arguments suitable for linking against the Python framework on macOS. +/// +/// This should be called from a build script. +/// +/// The following link flags are added: +/// - macOS: `-Wl,-rpath,` +/// +/// All other platforms currently are no-ops. +#[cfg(feature = "resolve-config")] +pub fn add_python_framework_link_args() { + let interpreter_config = pyo3_build_script_impl::resolve_interpreter_config().unwrap(); + _add_python_framework_link_args( + &interpreter_config, + &impl_::target_triple_from_env(), + impl_::is_linking_libpython(), + std::io::stdout(), + ) +} + +#[cfg(feature = "resolve-config")] +fn _add_python_framework_link_args( + interpreter_config: &InterpreterConfig, + triple: &Triple, + link_libpython: bool, + mut writer: impl std::io::Write, +) { + if matches!(triple.operating_system, OperatingSystem::Darwin(_)) && link_libpython { + if let Some(framework_prefix) = interpreter_config.python_framework_prefix.as_ref() { + writeln!( + writer, + "cargo:rustc-link-arg=-Wl,-rpath,{}", + framework_prefix + ) + .unwrap(); + } + } +} + /// Loads the configuration determined from the build environment. /// /// Because this will never change in a given compilation run, this is cached in a `once_cell`. @@ -306,4 +344,49 @@ mod tests { cargo:rustc-cdylib-link-arg=-sWASM_BIGINT\n" ); } + + #[cfg(feature = "resolve-config")] + #[test] + fn python_framework_link_args() { + let mut buf = Vec::new(); + + let interpreter_config = InterpreterConfig { + implementation: PythonImplementation::CPython, + version: PythonVersion { + major: 3, + minor: 13, + }, + shared: true, + abi3: false, + lib_name: None, + lib_dir: None, + executable: None, + pointer_width: None, + build_flags: BuildFlags::default(), + suppress_build_script_link_lines: false, + extra_build_script_lines: vec![], + python_framework_prefix: Some( + "/Applications/Xcode.app/Contents/Developer/Library/Frameworks".to_string(), + ), + }; + // Does nothing on non-mac + _add_python_framework_link_args( + &interpreter_config, + &Triple::from_str("x86_64-pc-windows-msvc").unwrap(), + true, + &mut buf, + ); + assert_eq!(buf, Vec::new()); + + _add_python_framework_link_args( + &interpreter_config, + &Triple::from_str("x86_64-apple-darwin").unwrap(), + true, + &mut buf, + ); + assert_eq!( + std::str::from_utf8(&buf).unwrap(), + "cargo:rustc-link-arg=-Wl,-rpath,/Applications/Xcode.app/Contents/Developer/Library/Frameworks\n" + ); + } } From 93823d2e53886aa9c866495be47983b4258ee6c1 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Thu, 2 Jan 2025 15:03:22 -0700 Subject: [PATCH 422/495] Fix PyDict issues on free-threaded build (#4788) * clarify safety docs for PyDict API * get dict size using PyDict_Size on free-threaded build * avoid unnecessary critical sections in PyDict * add changelog entry * fix build error on GIL-enabled build * address code review comments * move attribute below doc comment * ignore unsafe_op_in_unsafe_fn in next_unchecked --------- Co-authored-by: David Hewitt --- newsfragments/4788.fixed.md | 4 +++ src/types/dict.rs | 57 +++++++++++++++++++++++++------------ 2 files changed, 43 insertions(+), 18 deletions(-) create mode 100644 newsfragments/4788.fixed.md diff --git a/newsfragments/4788.fixed.md b/newsfragments/4788.fixed.md new file mode 100644 index 00000000000..804cd60fd3d --- /dev/null +++ b/newsfragments/4788.fixed.md @@ -0,0 +1,4 @@ +* Fixed thread-unsafe access of dict internals in BoundDictIterator on the + free-threaded build. +* Avoided creating unnecessary critical sections in BoundDictIterator + implementation on the free-threaded build. diff --git a/src/types/dict.rs b/src/types/dict.rs index b3c8e37962b..0d2e6ff335f 100644 --- a/src/types/dict.rs +++ b/src/types/dict.rs @@ -181,7 +181,8 @@ pub trait PyDictMethods<'py>: crate::sealed::Sealed { /// Iterates over the contents of this dictionary while holding a critical section on the dict. /// This is useful when the GIL is disabled and the dictionary is shared between threads. /// It is not guaranteed that the dictionary will not be modified during iteration when the - /// closure calls arbitrary Python code that releases the current critical section. + /// closure calls arbitrary Python code that releases the critical section held by the + /// iterator. Otherwise, the dictionary will not be modified during iteration. /// /// This method is a small performance optimization over `.iter().try_for_each()` when the /// nightly feature is not enabled because we cannot implement an optimised version of @@ -396,19 +397,26 @@ impl<'a, 'py> Borrowed<'a, 'py, PyDict> { /// Iterates over the contents of this dictionary without incrementing reference counts. /// /// # Safety - /// It must be known that this dictionary will not be modified during iteration. + /// It must be known that this dictionary will not be modified during iteration, + /// for example, when parsing arguments in a keyword arguments dictionary. pub(crate) unsafe fn iter_borrowed(self) -> BorrowedDictIter<'a, 'py> { BorrowedDictIter::new(self) } } fn dict_len(dict: &Bound<'_, PyDict>) -> Py_ssize_t { - #[cfg(any(not(Py_3_8), PyPy, GraalPy, Py_LIMITED_API))] + #[cfg(any(not(Py_3_8), PyPy, GraalPy, Py_LIMITED_API, Py_GIL_DISABLED))] unsafe { ffi::PyDict_Size(dict.as_ptr()) } - #[cfg(all(Py_3_8, not(PyPy), not(GraalPy), not(Py_LIMITED_API)))] + #[cfg(all( + Py_3_8, + not(PyPy), + not(GraalPy), + not(Py_LIMITED_API), + not(Py_GIL_DISABLED) + ))] unsafe { (*dict.as_ptr().cast::()).ma_used } @@ -429,8 +437,11 @@ enum DictIterImpl { } impl DictIterImpl { + #[deny(unsafe_op_in_unsafe_fn)] #[inline] - fn next<'py>( + /// Safety: the dict should be locked with a critical section on the free-threaded build + /// and otherwise not shared between threads in code that releases the GIL. + unsafe fn next_unchecked<'py>( &mut self, dict: &Bound<'py, PyDict>, ) -> Option<(Bound<'py, PyAny>, Bound<'py, PyAny>)> { @@ -440,7 +451,7 @@ impl DictIterImpl { remaining, ppos, .. - } => crate::sync::with_critical_section(dict, || { + } => { let ma_used = dict_len(dict); // These checks are similar to what CPython does. @@ -470,20 +481,20 @@ impl DictIterImpl { let mut key: *mut ffi::PyObject = std::ptr::null_mut(); let mut value: *mut ffi::PyObject = std::ptr::null_mut(); - if unsafe { ffi::PyDict_Next(dict.as_ptr(), ppos, &mut key, &mut value) } != 0 { + if unsafe { ffi::PyDict_Next(dict.as_ptr(), ppos, &mut key, &mut value) != 0 } { *remaining -= 1; let py = dict.py(); // Safety: // - PyDict_Next returns borrowed values // - we have already checked that `PyDict_Next` succeeded, so we can assume these to be non-null Some(( - unsafe { key.assume_borrowed_unchecked(py) }.to_owned(), - unsafe { value.assume_borrowed_unchecked(py) }.to_owned(), + unsafe { key.assume_borrowed_unchecked(py).to_owned() }, + unsafe { value.assume_borrowed_unchecked(py).to_owned() }, )) } else { None } - }), + } } } @@ -504,7 +515,17 @@ impl<'py> Iterator for BoundDictIterator<'py> { #[inline] fn next(&mut self) -> Option { - self.inner.next(&self.dict) + #[cfg(Py_GIL_DISABLED)] + { + self.inner + .with_critical_section(&self.dict, |inner| unsafe { + inner.next_unchecked(&self.dict) + }) + } + #[cfg(not(Py_GIL_DISABLED))] + { + unsafe { self.inner.next_unchecked(&self.dict) } + } } #[inline] @@ -522,7 +543,7 @@ impl<'py> Iterator for BoundDictIterator<'py> { { self.inner.with_critical_section(&self.dict, |inner| { let mut accum = init; - while let Some(x) = inner.next(&self.dict) { + while let Some(x) = unsafe { inner.next_unchecked(&self.dict) } { accum = f(accum, x); } accum @@ -539,7 +560,7 @@ impl<'py> Iterator for BoundDictIterator<'py> { { self.inner.with_critical_section(&self.dict, |inner| { let mut accum = init; - while let Some(x) = inner.next(&self.dict) { + while let Some(x) = unsafe { inner.next_unchecked(&self.dict) } { accum = f(accum, x)? } R::from_output(accum) @@ -554,7 +575,7 @@ impl<'py> Iterator for BoundDictIterator<'py> { F: FnMut(Self::Item) -> bool, { self.inner.with_critical_section(&self.dict, |inner| { - while let Some(x) = inner.next(&self.dict) { + while let Some(x) = unsafe { inner.next_unchecked(&self.dict) } { if !f(x) { return false; } @@ -571,7 +592,7 @@ impl<'py> Iterator for BoundDictIterator<'py> { F: FnMut(Self::Item) -> bool, { self.inner.with_critical_section(&self.dict, |inner| { - while let Some(x) = inner.next(&self.dict) { + while let Some(x) = unsafe { inner.next_unchecked(&self.dict) } { if f(x) { return true; } @@ -588,7 +609,7 @@ impl<'py> Iterator for BoundDictIterator<'py> { P: FnMut(&Self::Item) -> bool, { self.inner.with_critical_section(&self.dict, |inner| { - while let Some(x) = inner.next(&self.dict) { + while let Some(x) = unsafe { inner.next_unchecked(&self.dict) } { if predicate(&x) { return Some(x); } @@ -605,7 +626,7 @@ impl<'py> Iterator for BoundDictIterator<'py> { F: FnMut(Self::Item) -> Option, { self.inner.with_critical_section(&self.dict, |inner| { - while let Some(x) = inner.next(&self.dict) { + while let Some(x) = unsafe { inner.next_unchecked(&self.dict) } { if let found @ Some(_) = f(x) { return found; } @@ -623,7 +644,7 @@ impl<'py> Iterator for BoundDictIterator<'py> { { self.inner.with_critical_section(&self.dict, |inner| { let mut acc = 0; - while let Some(x) = inner.next(&self.dict) { + while let Some(x) = unsafe { inner.next_unchecked(&self.dict) } { if predicate(x) { return Some(acc); } From 46702033f7d92e274ad3dedd57dbb623c3854170 Mon Sep 17 00:00:00 2001 From: Bruno Kolenbrander <59372212+mejrs@users.noreply.github.com> Date: Wed, 8 Jan 2025 21:08:52 +0100 Subject: [PATCH 423/495] Allow useless conversion (#4838) --- pyo3-macros-backend/src/quotes.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/pyo3-macros-backend/src/quotes.rs b/pyo3-macros-backend/src/quotes.rs index 47b82605bd1..d961b4c0acd 100644 --- a/pyo3-macros-backend/src/quotes.rs +++ b/pyo3-macros-backend/src/quotes.rs @@ -17,6 +17,7 @@ pub(crate) fn ok_wrap(obj: TokenStream, ctx: &Ctx) -> TokenStream { let pyo3_path = pyo3_path.to_tokens_spanned(*output_span); quote_spanned! { *output_span => { let obj = #obj; + #[allow(clippy::useless_conversion)] #pyo3_path::impl_::wrap::converter(&obj).wrap(obj).map_err(::core::convert::Into::<#pyo3_path::PyErr>::into) }} } From c0f08c289fd52b1d52ca4bddc45af12438a04aed Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Wed, 8 Jan 2025 15:57:37 -0700 Subject: [PATCH 424/495] Implement locked iteration for PyList (#4789) * implement locked iteration for PyList * fix limited API and PyPy support * fix formatting of safety docstrings * only define fold and rfold on not(feature = "nightly") * add missing try_fold implementation on nightly * Use split borrows for locked iteration for PyList Inline ListIterImpl implementations by using split borrows and destructuring let Self { .. } = self destructuring inside BoundListIterator impls. Signed-off-by: Manos Pitsidianakis * use a function to do the split borrow * add changelog entries * fix clippy on limited API and PyPy * use a macro for the split borrow * add a test that mutates the list during a fold * enable next_unchecked on PyPy * fix incorrect docstring for locked_for_each * simplify borrows by adding BoundListIterator::with_critical_section * fix build on GIL-enabled and limited API builds * fix docs build on MSRV --------- Signed-off-by: Manos Pitsidianakis Co-authored-by: Manos Pitsidianakis --- newsfragments/4789.added.md | 3 + newsfragments/4789.changed.md | 2 + src/types/list.rs | 482 +++++++++++++++++++++++++++++++--- 3 files changed, 454 insertions(+), 33 deletions(-) create mode 100644 newsfragments/4789.added.md create mode 100644 newsfragments/4789.changed.md diff --git a/newsfragments/4789.added.md b/newsfragments/4789.added.md new file mode 100644 index 00000000000..fab564a8962 --- /dev/null +++ b/newsfragments/4789.added.md @@ -0,0 +1,3 @@ +* Added `PyList::locked_for_each`, which is equivalent to `PyList::for_each` on + the GIL-enabled build and uses a critical section to lock the list on the + free-threaded build, similar to `PyDict::locked_for_each`. diff --git a/newsfragments/4789.changed.md b/newsfragments/4789.changed.md new file mode 100644 index 00000000000..d20419e8f23 --- /dev/null +++ b/newsfragments/4789.changed.md @@ -0,0 +1,2 @@ +* Operations that process a PyList via an iterator now use a critical section + on the free-threaded build to amortize synchronization cost and prevent race conditions. diff --git a/src/types/list.rs b/src/types/list.rs index 2e124c82400..76da36d00b9 100644 --- a/src/types/list.rs +++ b/src/types/list.rs @@ -179,7 +179,9 @@ pub trait PyListMethods<'py>: crate::sealed::Sealed { /// # Safety /// /// Caller must verify that the index is within the bounds of the list. - #[cfg(not(any(Py_LIMITED_API, Py_GIL_DISABLED)))] + /// On the free-threaded build, caller must verify they have exclusive access to the list + /// via a lock or by holding the innermost critical section on the list. + #[cfg(not(Py_LIMITED_API))] unsafe fn get_item_unchecked(&self, index: usize) -> Bound<'py, PyAny>; /// Takes the slice `self[low:high]` and returns it as a new list. @@ -239,6 +241,17 @@ pub trait PyListMethods<'py>: crate::sealed::Sealed { /// Returns an iterator over this list's items. fn iter(&self) -> BoundListIterator<'py>; + /// Iterates over the contents of this list while holding a critical section on the list. + /// This is useful when the GIL is disabled and the list is shared between threads. + /// It is not guaranteed that the list will not be modified during iteration when the + /// closure calls arbitrary Python code that releases the critical section held by the + /// iterator. Otherwise, the list will not be modified during iteration. + /// + /// This is equivalent to for_each if the GIL is enabled. + fn locked_for_each(&self, closure: F) -> PyResult<()> + where + F: Fn(Bound<'py, PyAny>) -> PyResult<()>; + /// Sorts the list in-place. Equivalent to the Python expression `l.sort()`. fn sort(&self) -> PyResult<()>; @@ -302,7 +315,7 @@ impl<'py> PyListMethods<'py> for Bound<'py, PyList> { /// # Safety /// /// Caller must verify that the index is within the bounds of the list. - #[cfg(not(any(Py_LIMITED_API, Py_GIL_DISABLED)))] + #[cfg(not(Py_LIMITED_API))] unsafe fn get_item_unchecked(&self, index: usize) -> Bound<'py, PyAny> { // PyList_GET_ITEM return borrowed ptr; must make owned for safety (see #890). ffi::PyList_GET_ITEM(self.as_ptr(), index as Py_ssize_t) @@ -440,6 +453,14 @@ impl<'py> PyListMethods<'py> for Bound<'py, PyList> { BoundListIterator::new(self.clone()) } + /// Iterates over a list while holding a critical section, calling a closure on each item + fn locked_for_each(&self, closure: F) -> PyResult<()> + where + F: Fn(Bound<'py, PyAny>) -> PyResult<()>, + { + crate::sync::with_critical_section(self, || self.iter().try_for_each(closure)) + } + /// Sorts the list in-place. Equivalent to the Python expression `l.sort()`. fn sort(&self) -> PyResult<()> { err::error_on_minusone(self.py(), unsafe { ffi::PyList_Sort(self.as_ptr()) }) @@ -462,73 +483,332 @@ impl<'py> PyListMethods<'py> for Bound<'py, PyList> { } } +// New types for type checking when using BoundListIterator associated methods, like +// BoundListIterator::next_unchecked. +struct Index(usize); +struct Length(usize); + /// Used by `PyList::iter()`. pub struct BoundListIterator<'py> { list: Bound<'py, PyList>, - index: usize, - length: usize, + index: Index, + length: Length, } impl<'py> BoundListIterator<'py> { fn new(list: Bound<'py, PyList>) -> Self { - let length: usize = list.len(); - BoundListIterator { + Self { + index: Index(0), + length: Length(list.len()), list, - index: 0, - length, } } - unsafe fn get_item(&self, index: usize) -> Bound<'py, PyAny> { - #[cfg(any(Py_LIMITED_API, PyPy, Py_GIL_DISABLED))] - let item = self.list.get_item(index).expect("list.get failed"); - #[cfg(not(any(Py_LIMITED_API, PyPy, Py_GIL_DISABLED)))] - let item = self.list.get_item_unchecked(index); - item + /// # Safety + /// + /// On the free-threaded build, caller must verify they have exclusive + /// access to the list by holding a lock or by holding the innermost + /// critical section on the list. + #[inline] + #[cfg(not(Py_LIMITED_API))] + #[deny(unsafe_op_in_unsafe_fn)] + unsafe fn next_unchecked( + index: &mut Index, + length: &mut Length, + list: &Bound<'py, PyList>, + ) -> Option> { + let length = length.0.min(list.len()); + let my_index = index.0; + + if index.0 < length { + let item = unsafe { list.get_item_unchecked(my_index) }; + index.0 += 1; + Some(item) + } else { + None + } } -} -impl<'py> Iterator for BoundListIterator<'py> { - type Item = Bound<'py, PyAny>; + #[cfg(Py_LIMITED_API)] + fn next( + index: &mut Index, + length: &mut Length, + list: &Bound<'py, PyList>, + ) -> Option> { + let length = length.0.min(list.len()); + let my_index = index.0; + if index.0 < length { + let item = list.get_item(my_index).expect("get-item failed"); + index.0 += 1; + Some(item) + } else { + None + } + } + + /// # Safety + /// + /// On the free-threaded build, caller must verify they have exclusive + /// access to the list by holding a lock or by holding the innermost + /// critical section on the list. #[inline] - fn next(&mut self) -> Option { - let length = self.length.min(self.list.len()); + #[cfg(not(Py_LIMITED_API))] + #[deny(unsafe_op_in_unsafe_fn)] + unsafe fn next_back_unchecked( + index: &mut Index, + length: &mut Length, + list: &Bound<'py, PyList>, + ) -> Option> { + let current_length = length.0.min(list.len()); + + if index.0 < current_length { + let item = unsafe { list.get_item_unchecked(current_length - 1) }; + length.0 = current_length - 1; + Some(item) + } else { + None + } + } - if self.index < length { - let item = unsafe { self.get_item(self.index) }; - self.index += 1; + #[inline] + #[cfg(Py_LIMITED_API)] + fn next_back( + index: &mut Index, + length: &mut Length, + list: &Bound<'py, PyList>, + ) -> Option> { + let current_length = (length.0).min(list.len()); + + if index.0 < current_length { + let item = list.get_item(current_length - 1).expect("get-item failed"); + length.0 = current_length - 1; Some(item) } else { None } } + #[cfg(not(Py_LIMITED_API))] + fn with_critical_section( + &mut self, + f: impl FnOnce(&mut Index, &mut Length, &Bound<'py, PyList>) -> R, + ) -> R { + let Self { + index, + length, + list, + } = self; + crate::sync::with_critical_section(list, || f(index, length, list)) + } +} + +impl<'py> Iterator for BoundListIterator<'py> { + type Item = Bound<'py, PyAny>; + + #[inline] + fn next(&mut self) -> Option { + #[cfg(not(Py_LIMITED_API))] + { + self.with_critical_section(|index, length, list| unsafe { + Self::next_unchecked(index, length, list) + }) + } + #[cfg(Py_LIMITED_API)] + { + let Self { + index, + length, + list, + } = self; + Self::next(index, length, list) + } + } + #[inline] fn size_hint(&self) -> (usize, Option) { let len = self.len(); (len, Some(len)) } + + #[inline] + #[cfg(all(Py_GIL_DISABLED, not(feature = "nightly")))] + fn fold(mut self, init: B, mut f: F) -> B + where + Self: Sized, + F: FnMut(B, Self::Item) -> B, + { + self.with_critical_section(|index, length, list| { + let mut accum = init; + while let Some(x) = unsafe { Self::next_unchecked(index, length, list) } { + accum = f(accum, x); + } + accum + }) + } + + #[inline] + #[cfg(all(Py_GIL_DISABLED, feature = "nightly"))] + fn try_fold(&mut self, init: B, mut f: F) -> R + where + Self: Sized, + F: FnMut(B, Self::Item) -> R, + R: std::ops::Try, + { + self.with_critical_section(|index, length, list| { + let mut accum = init; + while let Some(x) = unsafe { Self::next_unchecked(index, length, list) } { + accum = f(accum, x)? + } + R::from_output(accum) + }) + } + + #[inline] + #[cfg(all(Py_GIL_DISABLED, not(feature = "nightly")))] + fn all(&mut self, mut f: F) -> bool + where + Self: Sized, + F: FnMut(Self::Item) -> bool, + { + self.with_critical_section(|index, length, list| { + while let Some(x) = unsafe { Self::next_unchecked(index, length, list) } { + if !f(x) { + return false; + } + } + true + }) + } + + #[inline] + #[cfg(all(Py_GIL_DISABLED, not(feature = "nightly")))] + fn any(&mut self, mut f: F) -> bool + where + Self: Sized, + F: FnMut(Self::Item) -> bool, + { + self.with_critical_section(|index, length, list| { + while let Some(x) = unsafe { Self::next_unchecked(index, length, list) } { + if f(x) { + return true; + } + } + false + }) + } + + #[inline] + #[cfg(all(Py_GIL_DISABLED, not(feature = "nightly")))] + fn find

(&mut self, mut predicate: P) -> Option + where + Self: Sized, + P: FnMut(&Self::Item) -> bool, + { + self.with_critical_section(|index, length, list| { + while let Some(x) = unsafe { Self::next_unchecked(index, length, list) } { + if predicate(&x) { + return Some(x); + } + } + None + }) + } + + #[inline] + #[cfg(all(Py_GIL_DISABLED, not(feature = "nightly")))] + fn find_map(&mut self, mut f: F) -> Option + where + Self: Sized, + F: FnMut(Self::Item) -> Option, + { + self.with_critical_section(|index, length, list| { + while let Some(x) = unsafe { Self::next_unchecked(index, length, list) } { + if let found @ Some(_) = f(x) { + return found; + } + } + None + }) + } + + #[inline] + #[cfg(all(Py_GIL_DISABLED, not(feature = "nightly")))] + fn position

(&mut self, mut predicate: P) -> Option + where + Self: Sized, + P: FnMut(Self::Item) -> bool, + { + self.with_critical_section(|index, length, list| { + let mut acc = 0; + while let Some(x) = unsafe { Self::next_unchecked(index, length, list) } { + if predicate(x) { + return Some(acc); + } + acc += 1; + } + None + }) + } } impl DoubleEndedIterator for BoundListIterator<'_> { #[inline] fn next_back(&mut self) -> Option { - let length = self.length.min(self.list.len()); - - if self.index < length { - let item = unsafe { self.get_item(length - 1) }; - self.length = length - 1; - Some(item) - } else { - None + #[cfg(not(Py_LIMITED_API))] + { + self.with_critical_section(|index, length, list| unsafe { + Self::next_back_unchecked(index, length, list) + }) + } + #[cfg(Py_LIMITED_API)] + { + let Self { + index, + length, + list, + } = self; + Self::next_back(index, length, list) } } + + #[inline] + #[cfg(all(Py_GIL_DISABLED, not(feature = "nightly")))] + fn rfold(mut self, init: B, mut f: F) -> B + where + Self: Sized, + F: FnMut(B, Self::Item) -> B, + { + self.with_critical_section(|index, length, list| { + let mut accum = init; + while let Some(x) = unsafe { Self::next_back_unchecked(index, length, list) } { + accum = f(accum, x); + } + accum + }) + } + + #[inline] + #[cfg(all(Py_GIL_DISABLED, feature = "nightly"))] + fn try_rfold(&mut self, init: B, mut f: F) -> R + where + Self: Sized, + F: FnMut(B, Self::Item) -> R, + R: std::ops::Try, + { + self.with_critical_section(|index, length, list| { + let mut accum = init; + while let Some(x) = unsafe { Self::next_back_unchecked(index, length, list) } { + accum = f(accum, x)? + } + R::from_output(accum) + }) + } } impl ExactSizeIterator for BoundListIterator<'_> { fn len(&self) -> usize { - self.length.saturating_sub(self.index) + self.length.0.saturating_sub(self.index.0) } } @@ -558,7 +838,7 @@ mod tests { use crate::types::list::PyListMethods; use crate::types::sequence::PySequenceMethods; use crate::types::{PyList, PyTuple}; - use crate::{ffi, IntoPyObject, Python}; + use crate::{ffi, IntoPyObject, PyResult, Python}; #[test] fn test_new() { @@ -748,6 +1028,142 @@ mod tests { }); } + #[test] + fn test_iter_all() { + Python::with_gil(|py| { + let list = PyList::new(py, [true, true, true]).unwrap(); + assert!(list.iter().all(|x| x.extract::().unwrap())); + + let list = PyList::new(py, [true, false, true]).unwrap(); + assert!(!list.iter().all(|x| x.extract::().unwrap())); + }); + } + + #[test] + fn test_iter_any() { + Python::with_gil(|py| { + let list = PyList::new(py, [true, true, true]).unwrap(); + assert!(list.iter().any(|x| x.extract::().unwrap())); + + let list = PyList::new(py, [true, false, true]).unwrap(); + assert!(list.iter().any(|x| x.extract::().unwrap())); + + let list = PyList::new(py, [false, false, false]).unwrap(); + assert!(!list.iter().any(|x| x.extract::().unwrap())); + }); + } + + #[test] + fn test_iter_find() { + Python::with_gil(|py: Python<'_>| { + let list = PyList::new(py, ["hello", "world"]).unwrap(); + assert_eq!( + Some("world".to_string()), + list.iter() + .find(|v| v.extract::().unwrap() == "world") + .map(|v| v.extract::().unwrap()) + ); + assert_eq!( + None, + list.iter() + .find(|v| v.extract::().unwrap() == "foobar") + .map(|v| v.extract::().unwrap()) + ); + }); + } + + #[test] + fn test_iter_position() { + Python::with_gil(|py: Python<'_>| { + let list = PyList::new(py, ["hello", "world"]).unwrap(); + assert_eq!( + Some(1), + list.iter() + .position(|v| v.extract::().unwrap() == "world") + ); + assert_eq!( + None, + list.iter() + .position(|v| v.extract::().unwrap() == "foobar") + ); + }); + } + + #[test] + fn test_iter_fold() { + Python::with_gil(|py: Python<'_>| { + let list = PyList::new(py, [1, 2, 3]).unwrap(); + let sum = list + .iter() + .fold(0, |acc, v| acc + v.extract::().unwrap()); + assert_eq!(sum, 6); + }); + } + + #[test] + fn test_iter_fold_out_of_bounds() { + Python::with_gil(|py: Python<'_>| { + let list = PyList::new(py, [1, 2, 3]).unwrap(); + let sum = list.iter().fold(0, |_, _| { + // clear the list to create a pathological fold operation + // that mutates the list as it processes it + for _ in 0..3 { + list.del_item(0).unwrap(); + } + -5 + }); + assert_eq!(sum, -5); + assert!(list.len() == 0); + }); + } + + #[test] + fn test_iter_rfold() { + Python::with_gil(|py: Python<'_>| { + let list = PyList::new(py, [1, 2, 3]).unwrap(); + let sum = list + .iter() + .rfold(0, |acc, v| acc + v.extract::().unwrap()); + assert_eq!(sum, 6); + }); + } + + #[test] + fn test_iter_try_fold() { + Python::with_gil(|py: Python<'_>| { + let list = PyList::new(py, [1, 2, 3]).unwrap(); + let sum = list + .iter() + .try_fold(0, |acc, v| PyResult::Ok(acc + v.extract::()?)) + .unwrap(); + assert_eq!(sum, 6); + + let list = PyList::new(py, ["foo", "bar"]).unwrap(); + assert!(list + .iter() + .try_fold(0, |acc, v| PyResult::Ok(acc + v.extract::()?)) + .is_err()); + }); + } + + #[test] + fn test_iter_try_rfold() { + Python::with_gil(|py: Python<'_>| { + let list = PyList::new(py, [1, 2, 3]).unwrap(); + let sum = list + .iter() + .try_rfold(0, |acc, v| PyResult::Ok(acc + v.extract::()?)) + .unwrap(); + assert_eq!(sum, 6); + + let list = PyList::new(py, ["foo", "bar"]).unwrap(); + assert!(list + .iter() + .try_rfold(0, |acc, v| PyResult::Ok(acc + v.extract::()?)) + .is_err()); + }); + } + #[test] fn test_into_iter() { Python::with_gil(|py| { @@ -877,7 +1293,7 @@ mod tests { }); } - #[cfg(not(any(Py_LIMITED_API, PyPy, Py_GIL_DISABLED)))] + #[cfg(not(Py_LIMITED_API))] #[test] fn test_list_get_item_unchecked_sanity() { Python::with_gil(|py| { From 1840bc5ded1ba0eb3f83519b955fa14c1c11ace5 Mon Sep 17 00:00:00 2001 From: Owen Leung Date: Fri, 10 Jan 2025 07:44:59 +0800 Subject: [PATCH 425/495] ci: updates for Rust 1.84 (#4846) * Fix failing ruff fmt test * Add newsfragments * remove changelog * use wasm32-wasip1 in CI * update ui tests * use `wasm32-wasip1` in noxfile * update one more ui test * fix clippy beta * bump `EMSCRIPTEN_VERSION` * emscripten link `sqlite3` * emscipten update node --------- Co-authored-by: Nathan Goldbaum Co-authored-by: Icxolu <10486322+Icxolu@users.noreply.github.com> --- .github/workflows/ci.yml | 4 ++-- emscripten/Makefile | 2 +- noxfile.py | 7 ++++--- pyo3-build-config/src/impl_.rs | 2 +- pyo3-ffi/src/datetime.rs | 2 +- src/types/any.rs | 2 +- src/types/module.rs | 2 +- tests/ui/invalid_cancel_handle.stderr | 4 ++-- tests/ui/invalid_property_args.stderr | 2 +- tests/ui/invalid_pycallargs.stderr | 4 ++++ tests/ui/invalid_pyclass_args.stderr | 8 ++++---- tests/ui/invalid_pyfunctions.stderr | 2 +- tests/ui/invalid_pymethod_receiver.stderr | 2 +- tests/ui/invalid_pymethods.stderr | 2 +- tests/ui/invalid_result_conversion.stderr | 2 +- tests/ui/not_send.stderr | 2 +- tests/ui/not_send2.stderr | 2 +- tests/ui/pyclass_send.stderr | 12 ++++++------ 18 files changed, 34 insertions(+), 29 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f4b13f80dd7..76c5ab06f29 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -115,7 +115,7 @@ jobs: { os: "ubuntu-latest", python-architecture: "x64", - rust-target: "wasm32-wasi", + rust-target: "wasm32-wasip1", }, { os: "windows-latest", @@ -489,7 +489,7 @@ jobs: components: rust-src - uses: actions/setup-node@v4 with: - node-version: 14 + node-version: 18 - run: python -m pip install --upgrade pip && pip install nox - uses: actions/cache@v4 id: cache diff --git a/emscripten/Makefile b/emscripten/Makefile index af224854c26..54094382c1b 100644 --- a/emscripten/Makefile +++ b/emscripten/Makefile @@ -4,7 +4,7 @@ CURDIR=$(abspath .) BUILDROOT ?= $(CURDIR)/builddir PYMAJORMINORMICRO ?= 3.11.0 -EMSCRIPTEN_VERSION=3.1.13 +EMSCRIPTEN_VERSION=3.1.68 export EMSDKDIR = $(BUILDROOT)/emsdk diff --git a/noxfile.py b/noxfile.py index 25cb8d1eb92..ed7759f5f99 100644 --- a/noxfile.py +++ b/noxfile.py @@ -339,6 +339,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=-lsqlite3", "-C link-arg=-lz", "-C link-arg=-lbz2", "-C link-arg=-sALLOW_MEMORY_GROWTH=1", @@ -351,7 +352,7 @@ def test_emscripten(session: nox.Session): session, "bash", "-c", - f"source {info.builddir/'emsdk/emsdk_env.sh'} && cargo test", + f"source {info.builddir / 'emsdk/emsdk_env.sh'} && cargo test", ) @@ -797,7 +798,7 @@ def _get_rust_default_target() -> str: def _get_feature_sets() -> Tuple[Tuple[str, ...], ...]: """Returns feature sets to use for clippy job""" cargo_target = os.getenv("CARGO_BUILD_TARGET", "") - if "wasm32-wasi" not in cargo_target: + if "wasm32-wasip1" not in cargo_target: # multiple-pymethods not supported on wasm return ( ("--no-default-features",), @@ -951,7 +952,7 @@ def set( f"""\ implementation={implementation} version={version} -build_flags={','.join(build_flags)} +build_flags={",".join(build_flags)} suppress_build_script_link_lines=true """ ) diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index 05bb58ac7aa..f130c2d6557 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -2001,7 +2001,7 @@ fn unescape(escaped: &str) -> Vec { } } - bytes.push(unhex(chunk[0]) << 4 | unhex(chunk[1])); + bytes.push((unhex(chunk[0]) << 4) | unhex(chunk[1])); } bytes diff --git a/pyo3-ffi/src/datetime.rs b/pyo3-ffi/src/datetime.rs index e529e0fce50..7f2d7958364 100644 --- a/pyo3-ffi/src/datetime.rs +++ b/pyo3-ffi/src/datetime.rs @@ -119,7 +119,7 @@ pub struct PyDateTime_DateTime { pub unsafe fn PyDateTime_GET_YEAR(o: *mut PyObject) -> c_int { // This should work for Date or DateTime let data = (*(o as *mut PyDateTime_Date)).data; - c_int::from(data[0]) << 8 | c_int::from(data[1]) + (c_int::from(data[0]) << 8) | c_int::from(data[1]) } #[inline] diff --git a/src/types/any.rs b/src/types/any.rs index 1ebc5d40a0b..0725453e569 100644 --- a/src/types/any.rs +++ b/src/types/any.rs @@ -949,7 +949,7 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { } } - inner(self.py(), self.getattr(attr_name).map_err(Into::into)) + inner(self.py(), self.getattr(attr_name)) } fn getattr(&self, attr_name: N) -> PyResult> diff --git a/src/types/module.rs b/src/types/module.rs index fd7299cb084..99dc1be233b 100644 --- a/src/types/module.rs +++ b/src/types/module.rs @@ -443,7 +443,7 @@ impl<'py> PyModuleMethods<'py> for Bound<'py, PyModule> { Err(err) => { if err.is_instance_of::(self.py()) { let l = PyList::empty(self.py()); - self.setattr(__all__, &l).map_err(PyErr::from)?; + self.setattr(__all__, &l)?; Ok(l) } else { Err(err) diff --git a/tests/ui/invalid_cancel_handle.stderr b/tests/ui/invalid_cancel_handle.stderr index bd2b588df32..90a8caa4229 100644 --- a/tests/ui/invalid_cancel_handle.stderr +++ b/tests/ui/invalid_cancel_handle.stderr @@ -42,7 +42,7 @@ error[E0277]: the trait bound `CancelHandle: PyFunctionArgument<'_, '_>` is not --> tests/ui/invalid_cancel_handle.rs:20:50 | 20 | async fn missing_cancel_handle_attribute(_param: pyo3::coroutine::CancelHandle) {} - | ^^^^ the trait `PyClass` is not implemented for `CancelHandle`, which is required by `CancelHandle: PyFunctionArgument<'_, '_>` + | ^^^^ the trait `PyClass` is not implemented for `CancelHandle` | = help: the trait `PyClass` is implemented for `pyo3::coroutine::Coroutine` = note: required for `CancelHandle` to implement `FromPyObject<'_>` @@ -61,7 +61,7 @@ error[E0277]: the trait bound `CancelHandle: PyFunctionArgument<'_, '_>` is not --> tests/ui/invalid_cancel_handle.rs:20:50 | 20 | async fn missing_cancel_handle_attribute(_param: pyo3::coroutine::CancelHandle) {} - | ^^^^ the trait `Clone` is not implemented for `CancelHandle`, which is required by `CancelHandle: PyFunctionArgument<'_, '_>` + | ^^^^ the trait `Clone` is not implemented for `CancelHandle` | = help: the following other types implement trait `PyFunctionArgument<'a, 'py>`: &'a mut pyo3::coroutine::Coroutine diff --git a/tests/ui/invalid_property_args.stderr b/tests/ui/invalid_property_args.stderr index f2fea2a1dd5..5ef01fa210a 100644 --- a/tests/ui/invalid_property_args.stderr +++ b/tests/ui/invalid_property_args.stderr @@ -52,7 +52,7 @@ error[E0277]: `PhantomData` cannot be converted to a Python object 45 | value: ::std::marker::PhantomData, | ^ required by `#[pyo3(get)]` to create a readable property from a field of type `PhantomData` | - = help: the trait `IntoPyObject<'_>` is not implemented for `PhantomData`, which is required by `for<'py> PhantomData: PyO3GetField<'py>` + = help: the trait `IntoPyObject<'_>` is not implemented for `PhantomData` = note: implement `IntoPyObject` for `&PhantomData` or `IntoPyObject + Clone` for `PhantomData` to define the conversion = help: the following other types implement trait `IntoPyObject<'py>`: &&'a T diff --git a/tests/ui/invalid_pycallargs.stderr b/tests/ui/invalid_pycallargs.stderr index 93c0bc19b7f..78867b6855a 100644 --- a/tests/ui/invalid_pycallargs.stderr +++ b/tests/ui/invalid_pycallargs.stderr @@ -27,3 +27,7 @@ note: required by a bound in `call1` | where | A: PyCallArgs<'py>; | ^^^^^^^^^^^^^^^ required by this bound in `PyAnyMethods::call1` +help: use a unary tuple instead + | +6 | any.call1(("foo",)); + | + ++ diff --git a/tests/ui/invalid_pyclass_args.stderr b/tests/ui/invalid_pyclass_args.stderr index 15aa0387cc6..c13edc071d5 100644 --- a/tests/ui/invalid_pyclass_args.stderr +++ b/tests/ui/invalid_pyclass_args.stderr @@ -100,21 +100,21 @@ error: expected one of: `get`, `set`, `name` 85 | #[pyo3(pop)] | ^^^ -error: invalid format string: expected `'}'` but string was terminated +error: invalid format string: expected `}` but string was terminated --> tests/ui/invalid_pyclass_args.rs:105:19 | 105 | #[pyclass(str = "{")] - | -^ expected `'}'` in format string + | -^ expected `}` in format string | | | because of this opening brace | = note: if you intended to print `{`, you can escape it using `{{` -error: invalid format string: expected `'}'`, found `'$'` +error: invalid format string: expected `}`, found `$` --> tests/ui/invalid_pyclass_args.rs:109:19 | 109 | #[pyclass(str = "{$}")] - | -^ expected `'}'` in format string + | -^ expected `}` in format string | | | because of this opening brace | diff --git a/tests/ui/invalid_pyfunctions.stderr b/tests/ui/invalid_pyfunctions.stderr index 271c5a806be..e48c522976a 100644 --- a/tests/ui/invalid_pyfunctions.stderr +++ b/tests/ui/invalid_pyfunctions.stderr @@ -44,7 +44,7 @@ error[E0277]: the trait bound `&str: From tests/ui/invalid_pyfunctions.rs:33:14 | 33 | _string: &str, - | ^ the trait `From>` is not implemented for `&str`, which is required by `BoundRef<'_, '_, pyo3::types::PyModule>: Into<_>` + | ^ the trait `From>` is not implemented for `&str` | = help: the following other types implement trait `From`: `String` implements `From<&String>` diff --git a/tests/ui/invalid_pymethod_receiver.stderr b/tests/ui/invalid_pymethod_receiver.stderr index 9c998403194..7d81c0fae91 100644 --- a/tests/ui/invalid_pymethod_receiver.stderr +++ b/tests/ui/invalid_pymethod_receiver.stderr @@ -2,7 +2,7 @@ error[E0277]: the trait bound `i32: TryFrom>` is not s --> tests/ui/invalid_pymethod_receiver.rs:8:44 | 8 | fn method_with_invalid_self_type(_slf: i32, _py: Python<'_>, _index: u32) {} - | ^^^ the trait `From>` is not implemented for `i32`, which is required by `i32: TryFrom>` + | ^^^ the trait `From>` is not implemented for `i32` | = help: the following other types implement trait `From`: `i32` implements `From` diff --git a/tests/ui/invalid_pymethods.stderr b/tests/ui/invalid_pymethods.stderr index 845b79ed59a..eb8f429c937 100644 --- a/tests/ui/invalid_pymethods.stderr +++ b/tests/ui/invalid_pymethods.stderr @@ -183,7 +183,7 @@ error[E0277]: the trait bound `i32: From>` is not satis --> tests/ui/invalid_pymethods.rs:46:45 | 46 | fn classmethod_wrong_first_argument(_x: i32) -> Self { - | ^^^ the trait `From>` is not implemented for `i32`, which is required by `BoundRef<'_, '_, PyType>: Into<_>` + | ^^^ the trait `From>` is not implemented for `i32` | = help: the following other types implement trait `From`: `i32` implements `From` diff --git a/tests/ui/invalid_result_conversion.stderr b/tests/ui/invalid_result_conversion.stderr index 18667138954..f782e00828f 100644 --- a/tests/ui/invalid_result_conversion.stderr +++ b/tests/ui/invalid_result_conversion.stderr @@ -2,7 +2,7 @@ error[E0277]: the trait bound `PyErr: From` is not satisfied --> tests/ui/invalid_result_conversion.rs:22:25 | 22 | fn should_not_work() -> Result<(), MyError> { - | ^^^^^^ the trait `From` is not implemented for `PyErr`, which is required by `MyError: Into` + | ^^^^^^ the trait `From` is not implemented for `PyErr` | = help: the following other types implement trait `From`: `PyErr` implements `From` diff --git a/tests/ui/not_send.stderr b/tests/ui/not_send.stderr index 8007c2f4e54..5d05b3de059 100644 --- a/tests/ui/not_send.stderr +++ b/tests/ui/not_send.stderr @@ -6,7 +6,7 @@ error[E0277]: `*mut pyo3::Python<'static>` cannot be shared between threads safe | | | required by a bound introduced by this call | - = help: within `pyo3::Python<'_>`, the trait `Sync` is not implemented for `*mut pyo3::Python<'static>`, which is required by `{closure@$DIR/tests/ui/not_send.rs:4:22: 4:24}: Ungil` + = help: within `pyo3::Python<'_>`, the trait `Sync` is not implemented for `*mut pyo3::Python<'static>` note: required because it appears within the type `PhantomData<*mut pyo3::Python<'static>>` --> $RUST/core/src/marker.rs | diff --git a/tests/ui/not_send2.stderr b/tests/ui/not_send2.stderr index 52a4e6a7208..eec2b644e9a 100644 --- a/tests/ui/not_send2.stderr +++ b/tests/ui/not_send2.stderr @@ -9,7 +9,7 @@ error[E0277]: `*mut pyo3::Python<'static>` cannot be shared between threads safe 10 | | }); | |_________^ `*mut pyo3::Python<'static>` cannot be shared between threads safely | - = help: within `pyo3::Bound<'_, PyString>`, the trait `Sync` is not implemented for `*mut pyo3::Python<'static>`, which is required by `{closure@$DIR/tests/ui/not_send2.rs:8:26: 8:28}: Ungil` + = help: within `pyo3::Bound<'_, PyString>`, the trait `Sync` is not implemented for `*mut pyo3::Python<'static>` note: required because it appears within the type `PhantomData<*mut pyo3::Python<'static>>` --> $RUST/core/src/marker.rs | diff --git a/tests/ui/pyclass_send.stderr b/tests/ui/pyclass_send.stderr index 516df060b13..1623a5b2183 100644 --- a/tests/ui/pyclass_send.stderr +++ b/tests/ui/pyclass_send.stderr @@ -4,7 +4,7 @@ error[E0277]: `*mut c_void` cannot be shared between threads safely 5 | struct NotSyncNotSend(*mut c_void); | ^^^^^^^^^^^^^^ `*mut c_void` cannot be shared between threads safely | - = help: within `NotSyncNotSend`, the trait `Sync` is not implemented for `*mut c_void`, which is required by `NotSyncNotSend: Sync` + = help: within `NotSyncNotSend`, the trait `Sync` is not implemented for `*mut c_void` note: required because it appears within the type `NotSyncNotSend` --> tests/ui/pyclass_send.rs:5:8 | @@ -25,7 +25,7 @@ error[E0277]: `*mut c_void` cannot be sent between threads safely 4 | #[pyclass] | ^^^^^^^^^^ `*mut c_void` cannot be sent between threads safely | - = help: within `NotSyncNotSend`, the trait `Send` is not implemented for `*mut c_void`, which is required by `SendablePyClass: pyo3::impl_::pyclass::PyClassThreadChecker` + = help: within `NotSyncNotSend`, the trait `Send` is not implemented for `*mut c_void` = help: the trait `pyo3::impl_::pyclass::PyClassThreadChecker` is implemented for `SendablePyClass` note: required because it appears within the type `NotSyncNotSend` --> tests/ui/pyclass_send.rs:5:8 @@ -46,7 +46,7 @@ error[E0277]: `*mut c_void` cannot be shared between threads safely 8 | struct SendNotSync(*mut c_void); | ^^^^^^^^^^^ `*mut c_void` cannot be shared between threads safely | - = help: within `SendNotSync`, the trait `Sync` is not implemented for `*mut c_void`, which is required by `SendNotSync: Sync` + = help: within `SendNotSync`, the trait `Sync` is not implemented for `*mut c_void` note: required because it appears within the type `SendNotSync` --> tests/ui/pyclass_send.rs:8:8 | @@ -67,7 +67,7 @@ error[E0277]: `*mut c_void` cannot be sent between threads safely 11 | #[pyclass] | ^^^^^^^^^^ `*mut c_void` cannot be sent between threads safely | - = help: within `SyncNotSend`, the trait `Send` is not implemented for `*mut c_void`, which is required by `SendablePyClass: pyo3::impl_::pyclass::PyClassThreadChecker` + = help: within `SyncNotSend`, the trait `Send` is not implemented for `*mut c_void` = help: the trait `pyo3::impl_::pyclass::PyClassThreadChecker` is implemented for `SendablePyClass` note: required because it appears within the type `SyncNotSend` --> tests/ui/pyclass_send.rs:12:8 @@ -88,7 +88,7 @@ error[E0277]: `*mut c_void` cannot be sent between threads safely 4 | #[pyclass] | ^^^^^^^^^^ `*mut c_void` cannot be sent between threads safely | - = help: within `NotSyncNotSend`, the trait `Send` is not implemented for `*mut c_void`, which is required by `NotSyncNotSend: Send` + = help: within `NotSyncNotSend`, the trait `Send` is not implemented for `*mut c_void` note: required because it appears within the type `NotSyncNotSend` --> tests/ui/pyclass_send.rs:5:8 | @@ -107,7 +107,7 @@ error[E0277]: `*mut c_void` cannot be sent between threads safely 11 | #[pyclass] | ^^^^^^^^^^ `*mut c_void` cannot be sent between threads safely | - = help: within `SyncNotSend`, the trait `Send` is not implemented for `*mut c_void`, which is required by `SyncNotSend: Send` + = help: within `SyncNotSend`, the trait `Send` is not implemented for `*mut c_void` note: required because it appears within the type `SyncNotSend` --> tests/ui/pyclass_send.rs:12:8 | From 21132a8e77dbce1597b80f6c0ff6fbcf36c93852 Mon Sep 17 00:00:00 2001 From: Thomas Tanon Date: Fri, 10 Jan 2025 08:01:43 +0100 Subject: [PATCH 426/495] derive(FromPyObject): adds default option (#4829) * derive(FromPyObject): adds default option Takes an optional expression to set a custom value that is not the one from the Default trait * Documentation, testing and hygiene * Support enum variant named fields and cover failures --- guide/src/conversions/traits.md | 42 +++++++++ newsfragments/4829.added.md | 1 + pyo3-macros-backend/src/attributes.rs | 2 + pyo3-macros-backend/src/frompyobject.rs | 66 +++++++++++--- src/tests/hygiene/misc.rs | 2 + tests/test_frompyobject.rs | 114 ++++++++++++++++++++++++ tests/ui/invalid_frompy_derive.rs | 17 ++++ tests/ui/invalid_frompy_derive.stderr | 28 +++++- 8 files changed, 258 insertions(+), 14 deletions(-) create mode 100644 newsfragments/4829.added.md diff --git a/guide/src/conversions/traits.md b/guide/src/conversions/traits.md index c4e8f14866c..1aa445cce41 100644 --- a/guide/src/conversions/traits.md +++ b/guide/src/conversions/traits.md @@ -488,6 +488,48 @@ If the input is neither a string nor an integer, the error message will be: - apply a custom function to convert the field from Python the desired Rust type. - the argument must be the name of the function as a string. - the function signature must be `fn(&Bound) -> PyResult` where `T` is the Rust type of the argument. +- `pyo3(default)`, `pyo3(default = ...)` + - if the argument is set, uses the given default value. + - in this case, the argument must be a Rust expression returning a value of the desired Rust type. + - if the argument is not set, [`Default::default`](https://doc.rust-lang.org/std/default/trait.Default.html#tymethod.default) is used. + - note that the default value is only used if the field is not set. + If the field is set and the conversion function from Python to Rust fails, an exception is raised and the default value is not used. + - this attribute is only supported on named fields. + +For example, the code below applies the given conversion function on the `"value"` dict item to compute its length or fall back to the type default value (0): + +```rust +use pyo3::prelude::*; + +#[derive(FromPyObject)] +struct RustyStruct { + #[pyo3(item("value"), default, from_py_with = "Bound::<'_, PyAny>::len")] + len: usize, + #[pyo3(item)] + other: usize, +} +# +# use pyo3::types::PyDict; +# fn main() -> PyResult<()> { +# Python::with_gil(|py| -> PyResult<()> { +# // Filled case +# let dict = PyDict::new(py); +# dict.set_item("value", (1,)).unwrap(); +# dict.set_item("other", 1).unwrap(); +# let result = dict.extract::()?; +# assert_eq!(result.len, 1); +# assert_eq!(result.other, 1); +# +# // Empty case +# let dict = PyDict::new(py); +# dict.set_item("other", 1).unwrap(); +# let result = dict.extract::()?; +# assert_eq!(result.len, 0); +# assert_eq!(result.other, 1); +# Ok(()) +# }) +# } +``` ### `IntoPyObject` The ['IntoPyObject'] trait defines the to-python conversion for a Rust type. All types in PyO3 implement this trait, diff --git a/newsfragments/4829.added.md b/newsfragments/4829.added.md new file mode 100644 index 00000000000..9400501a799 --- /dev/null +++ b/newsfragments/4829.added.md @@ -0,0 +1 @@ +`derive(FromPyObject)` allow a `default` attribute to set a default value for extracted fields of named structs. The default value is either provided explicitly or fetched via `Default::default()`. \ No newline at end of file diff --git a/pyo3-macros-backend/src/attributes.rs b/pyo3-macros-backend/src/attributes.rs index 6fe75e44302..bd5da377121 100644 --- a/pyo3-macros-backend/src/attributes.rs +++ b/pyo3-macros-backend/src/attributes.rs @@ -351,6 +351,8 @@ impl ToTokens for OptionalKeywordAttribute { pub type FromPyWithAttribute = KeywordAttribute>; +pub type DefaultAttribute = OptionalKeywordAttribute; + /// For specifying the path to the pyo3 crate. pub type CrateAttribute = KeywordAttribute>; diff --git a/pyo3-macros-backend/src/frompyobject.rs b/pyo3-macros-backend/src/frompyobject.rs index 565c54da1f3..b353e2dc16d 100644 --- a/pyo3-macros-backend/src/frompyobject.rs +++ b/pyo3-macros-backend/src/frompyobject.rs @@ -1,7 +1,9 @@ -use crate::attributes::{self, get_pyo3_options, CrateAttribute, FromPyWithAttribute}; +use crate::attributes::{ + self, get_pyo3_options, CrateAttribute, DefaultAttribute, FromPyWithAttribute, +}; use crate::utils::Ctx; use proc_macro2::TokenStream; -use quote::{format_ident, quote}; +use quote::{format_ident, quote, ToTokens}; use syn::{ ext::IdentExt, parenthesized, @@ -90,6 +92,7 @@ struct NamedStructField<'a> { ident: &'a syn::Ident, getter: Option, from_py_with: Option, + default: Option, } struct TupleStructField { @@ -144,6 +147,10 @@ impl<'a> Container<'a> { attrs.getter.is_none(), field.span() => "`getter` is not permitted on tuple struct elements." ); + ensure_spanned!( + attrs.default.is_none(), + field.span() => "`default` is not permitted on tuple struct elements." + ); Ok(TupleStructField { from_py_with: attrs.from_py_with, }) @@ -193,10 +200,15 @@ impl<'a> Container<'a> { ident, getter: attrs.getter, from_py_with: attrs.from_py_with, + default: attrs.default, }) }) .collect::>>()?; - if options.transparent { + if struct_fields.iter().all(|field| field.default.is_some()) { + bail_spanned!( + fields.span() => "cannot derive FromPyObject for structs and variants with only default values" + ) + } else if options.transparent { ensure_spanned!( struct_fields.len() == 1, fields.span() => "transparent structs and variants can only have 1 field" @@ -346,18 +358,33 @@ impl<'a> Container<'a> { quote!(#pyo3_path::types::PyAnyMethods::get_item(obj, #pyo3_path::intern!(obj.py(), #field_name))) } }; - let extractor = match &field.from_py_with { - None => { - quote!(#pyo3_path::impl_::frompyobject::extract_struct_field(&#getter?, #struct_name, #field_name)?) - } - Some(FromPyWithAttribute { - value: expr_path, .. - }) => { - quote! (#pyo3_path::impl_::frompyobject::extract_struct_field_with(#expr_path as fn(_) -> _, &#getter?, #struct_name, #field_name)?) - } + let extractor = if let Some(FromPyWithAttribute { + value: expr_path, .. + }) = &field.from_py_with + { + quote!(#pyo3_path::impl_::frompyobject::extract_struct_field_with(#expr_path as fn(_) -> _, &value, #struct_name, #field_name)?) + } else { + quote!(#pyo3_path::impl_::frompyobject::extract_struct_field(&value, #struct_name, #field_name)?) + }; + let extracted = if let Some(default) = &field.default { + let default_expr = if let Some(default_expr) = &default.value { + default_expr.to_token_stream() + } else { + quote!(::std::default::Default::default()) + }; + quote!(if let ::std::result::Result::Ok(value) = #getter { + #extractor + } else { + #default_expr + }) + } else { + quote!({ + let value = #getter?; + #extractor + }) }; - fields.push(quote!(#ident: #extractor)); + fields.push(quote!(#ident: #extracted)); } quote!(::std::result::Result::Ok(#self_ty{#fields})) @@ -458,6 +485,7 @@ impl ContainerOptions { struct FieldPyO3Attributes { getter: Option, from_py_with: Option, + default: Option, } #[derive(Clone, Debug)] @@ -469,6 +497,7 @@ enum FieldGetter { enum FieldPyO3Attribute { Getter(FieldGetter), FromPyWith(FromPyWithAttribute), + Default(DefaultAttribute), } impl Parse for FieldPyO3Attribute { @@ -512,6 +541,8 @@ impl Parse for FieldPyO3Attribute { } } else if lookahead.peek(attributes::kw::from_py_with) { input.parse().map(FieldPyO3Attribute::FromPyWith) + } else if lookahead.peek(Token![default]) { + input.parse().map(FieldPyO3Attribute::Default) } else { Err(lookahead.error()) } @@ -523,6 +554,7 @@ impl FieldPyO3Attributes { fn from_attrs(attrs: &[Attribute]) -> Result { let mut getter = None; let mut from_py_with = None; + let mut default = None; for attr in attrs { if let Some(pyo3_attrs) = get_pyo3_options(attr)? { @@ -542,6 +574,13 @@ impl FieldPyO3Attributes { ); from_py_with = Some(from_py_with_attr); } + FieldPyO3Attribute::Default(default_attr) => { + ensure_spanned!( + default.is_none(), + attr.span() => "`default` may only be provided once" + ); + default = Some(default_attr); + } } } } @@ -550,6 +589,7 @@ impl FieldPyO3Attributes { Ok(FieldPyO3Attributes { getter, from_py_with, + default, }) } } diff --git a/src/tests/hygiene/misc.rs b/src/tests/hygiene/misc.rs index 6e00167ddb6..a953cea4a24 100644 --- a/src/tests/hygiene/misc.rs +++ b/src/tests/hygiene/misc.rs @@ -12,6 +12,8 @@ struct Derive3 { f: i32, #[pyo3(item(42))] g: i32, + #[pyo3(default)] + h: i32, } // struct case #[derive(crate::FromPyObject)] diff --git a/tests/test_frompyobject.rs b/tests/test_frompyobject.rs index 2192caf1f7c..d72a215814c 100644 --- a/tests/test_frompyobject.rs +++ b/tests/test_frompyobject.rs @@ -686,3 +686,117 @@ fn test_with_keyword_item() { assert_eq!(result, expected); }); } + +#[derive(Debug, FromPyObject, PartialEq, Eq)] +pub struct WithDefaultItem { + #[pyo3(item, default)] + opt: Option, + #[pyo3(item)] + value: usize, +} + +#[test] +fn test_with_default_item() { + Python::with_gil(|py| { + let dict = PyDict::new(py); + dict.set_item("value", 3).unwrap(); + let result = dict.extract::().unwrap(); + let expected = WithDefaultItem { + value: 3, + opt: None, + }; + assert_eq!(result, expected); + }); +} + +#[derive(Debug, FromPyObject, PartialEq, Eq)] +pub struct WithExplicitDefaultItem { + #[pyo3(item, default = 1)] + opt: usize, + #[pyo3(item)] + value: usize, +} + +#[test] +fn test_with_explicit_default_item() { + Python::with_gil(|py| { + let dict = PyDict::new(py); + dict.set_item("value", 3).unwrap(); + let result = dict.extract::().unwrap(); + let expected = WithExplicitDefaultItem { value: 3, opt: 1 }; + assert_eq!(result, expected); + }); +} + +#[derive(Debug, FromPyObject, PartialEq, Eq)] +pub struct WithDefaultItemAndConversionFunction { + #[pyo3(item, default, from_py_with = "Bound::<'_, PyAny>::len")] + opt: usize, + #[pyo3(item)] + value: usize, +} + +#[test] +fn test_with_default_item_and_conversion_function() { + Python::with_gil(|py| { + // Filled case + let dict = PyDict::new(py); + dict.set_item("opt", (1,)).unwrap(); + dict.set_item("value", 3).unwrap(); + let result = dict + .extract::() + .unwrap(); + let expected = WithDefaultItemAndConversionFunction { opt: 1, value: 3 }; + assert_eq!(result, expected); + + // Empty case + let dict = PyDict::new(py); + dict.set_item("value", 3).unwrap(); + let result = dict + .extract::() + .unwrap(); + let expected = WithDefaultItemAndConversionFunction { opt: 0, value: 3 }; + assert_eq!(result, expected); + + // Error case + let dict = PyDict::new(py); + dict.set_item("value", 3).unwrap(); + dict.set_item("opt", 1).unwrap(); + assert!(dict + .extract::() + .is_err()); + }); +} + +#[derive(Debug, FromPyObject, PartialEq, Eq)] +pub enum WithDefaultItemEnum { + #[pyo3(from_item_all)] + Foo { + a: usize, + #[pyo3(default)] + b: usize, + }, + NeverUsedA { + a: usize, + }, +} + +#[test] +fn test_with_default_item_enum() { + Python::with_gil(|py| { + // A and B filled + let dict = PyDict::new(py); + dict.set_item("a", 1).unwrap(); + dict.set_item("b", 2).unwrap(); + let result = dict.extract::().unwrap(); + let expected = WithDefaultItemEnum::Foo { a: 1, b: 2 }; + assert_eq!(result, expected); + + // A filled + let dict = PyDict::new(py); + dict.set_item("a", 1).unwrap(); + let result = dict.extract::().unwrap(); + let expected = WithDefaultItemEnum::Foo { a: 1, b: 0 }; + assert_eq!(result, expected); + }); +} diff --git a/tests/ui/invalid_frompy_derive.rs b/tests/ui/invalid_frompy_derive.rs index f123b149fb8..d3a778e686b 100644 --- a/tests/ui/invalid_frompy_derive.rs +++ b/tests/ui/invalid_frompy_derive.rs @@ -213,4 +213,21 @@ struct FromItemAllConflictAttrWithArgs { field: String, } +#[derive(FromPyObject)] +struct StructWithOnlyDefaultValues { + #[pyo3(default)] + field: String, +} + +#[derive(FromPyObject)] +enum EnumVariantWithOnlyDefaultValues { + Foo { + #[pyo3(default)] + field: String, + }, +} + +#[derive(FromPyObject)] +struct NamedTuplesWithDefaultValues(#[pyo3(default)] String); + fn main() {} diff --git a/tests/ui/invalid_frompy_derive.stderr b/tests/ui/invalid_frompy_derive.stderr index 8ed03caafb4..5b8c1fc718b 100644 --- a/tests/ui/invalid_frompy_derive.stderr +++ b/tests/ui/invalid_frompy_derive.stderr @@ -84,7 +84,7 @@ error: transparent structs and variants can only have 1 field 70 | | }, | |_____^ -error: expected one of: `attribute`, `item`, `from_py_with` +error: expected one of: `attribute`, `item`, `from_py_with`, `default` --> tests/ui/invalid_frompy_derive.rs:76:12 | 76 | #[pyo3(attr)] @@ -223,3 +223,29 @@ error: The struct is already annotated with `from_item_all`, `attribute` is not | 210 | #[pyo3(from_item_all)] | ^^^^^^^^^^^^^ + +error: cannot derive FromPyObject for structs and variants with only default values + --> tests/ui/invalid_frompy_derive.rs:217:36 + | +217 | struct StructWithOnlyDefaultValues { + | ____________________________________^ +218 | | #[pyo3(default)] +219 | | field: String, +220 | | } + | |_^ + +error: cannot derive FromPyObject for structs and variants with only default values + --> tests/ui/invalid_frompy_derive.rs:224:9 + | +224 | Foo { + | _________^ +225 | | #[pyo3(default)] +226 | | field: String, +227 | | }, + | |_____^ + +error: `default` is not permitted on tuple struct elements. + --> tests/ui/invalid_frompy_derive.rs:231:37 + | +231 | struct NamedTuplesWithDefaultValues(#[pyo3(default)] String); + | ^ From b9bab5de71e83092eb5856a976ce4153fc05dbb4 Mon Sep 17 00:00:00 2001 From: matt-codecov <137832199+matt-codecov@users.noreply.github.com> Date: Tue, 14 Jan 2025 23:38:25 -0800 Subject: [PATCH 427/495] docs: add ecosystems/tracing.md to guide (#4713) --- guide/src/SUMMARY.md | 1 + guide/src/ecosystem/tracing.md | 107 +++++++++++++++++++++++++++++++++ 2 files changed, 108 insertions(+) create mode 100644 guide/src/ecosystem/tracing.md diff --git a/guide/src/SUMMARY.md b/guide/src/SUMMARY.md index f8cc899d75c..cf987b72625 100644 --- a/guide/src/SUMMARY.md +++ b/guide/src/SUMMARY.md @@ -35,6 +35,7 @@ - [Supporting multiple Python versions](building-and-distribution/multiple-python-versions.md) - [Useful crates](ecosystem.md) - [Logging](ecosystem/logging.md) + - [Tracing](ecosystem/tracing.md) - [Using `async` and `await`](ecosystem/async-await.md) - [FAQ and troubleshooting](faq.md) diff --git a/guide/src/ecosystem/tracing.md b/guide/src/ecosystem/tracing.md new file mode 100644 index 00000000000..341d0759e96 --- /dev/null +++ b/guide/src/ecosystem/tracing.md @@ -0,0 +1,107 @@ +# Tracing + +Python projects that write extension modules for performance reasons may want to +tap into [Rust's `tracing` ecosystem] to gain insight into the performance of +their extension module. + +This section of the guide describes a few crates that provide ways to do that. +They build on [`tracing_subscriber`][tracing-subscriber] and require code +changes in both Python and Rust to integrate. Note that each extension module +must configure its own `tracing` integration; one extension module will not see +`tracing` data from a different module. + +## `pyo3-tracing-subscriber` ([documentation][pyo3-tracing-subscriber-docs]) + +[`pyo3-tracing-subscriber`][pyo3-tracing-subscriber] provides a way for Python +projects to configure `tracing_subscriber`. It exposes a few +`tracing_subscriber` layers: +- `tracing_subscriber::fmt` for writing human-readable output to file or stdout +- `opentelemetry-stdout` for writing OTLP output to file or stdout +- `opentelemetry-otlp` for writing OTLP output to an OTLP endpoint + +The extension module must call [`pyo3_tracing_subscriber::add_submodule`][add-submodule] +to export the Python classes needed to configure and initialize `tracing`. + +On the Python side, use the `Tracing` context manager to initialize tracing and +run Rust code inside the context manager's block. `Tracing` takes a +`GlobalTracingConfig` instance describing the layers to be used. + +See [the README on crates.io][pyo3-tracing-subscriber] +for example code. + +## `pyo3-python-tracing-subscriber` ([documentation][pyo3-python-tracing-subscriber-docs]) + +The similarly-named [`pyo3-python-tracing-subscriber`][pyo3-python-tracing-subscriber] +implements a shim in Rust that forwards `tracing` data to a `Layer` +implementation defined in and passed in from Python. + +There are many ways an extension module could integrate `pyo3-python-tracing-subscriber` +but a simple one may look something like this: +```rust +#[tracing::instrument] +#[pyfunction] +fn fibonacci(index: usize, use_memoized: bool) -> PyResult { + // ... +} + +#[pyfunction] +pub fn initialize_tracing(py_impl: Bound<'_, PyAny>) { + tracing_subscriber::registry() + .with(pyo3_python_tracing_subscriber::PythonCallbackLayerBridge::new(py_impl)) + .init(); +} +``` +The extension module must provide some way for Python to pass in one or more +Python objects that implement [the `Layer` interface]. Then it should construct +[`pyo3_python_tracing_subscriber::PythonCallbackLayerBridge`][PythonCallbackLayerBridge] +instances with each of those Python objects and initialize `tracing_subscriber` +as shown above. + +The Python objects implement a modified version of the `Layer` interface: +- `on_new_span()` may return some state that will stored inside the Rust span +- other callbacks will be given that state as an additional positional argument + +A dummy `Layer` implementation may look like this: +```python +import rust_extension + +class MyPythonLayer: + def __init__(self): + pass + + # `on_new_span` can return some state + def on_new_span(self, span_attrs: str, span_id: str) -> int: + print(f"[on_new_span]: {span_attrs} | {span_id}") + return random.randint(1, 1000) + + # The state from `on_new_span` is passed back into other trait methods + def on_event(self, event: str, state: int): + print(f"[on_event]: {event} | {state}") + + def on_close(self, span_id: str, state: int): + print(f"[on_close]: {span_id} | {state}") + + def on_record(self, span_id: str, values: str, state: int): + print(f"[on_record]: {span_id} | {values} | {state}") + +def main(): + rust_extension.initialize_tracing(MyPythonLayer()) + + print("10th fibonacci number: ", rust_extension.fibonacci(10, True)) +``` + +`pyo3-python-tracing-subscriber` has [working examples] +showing both the Rust side and the Python side of an integration. + +[pyo3-tracing-subscriber]: https://crates.io/crates/pyo3-tracing-subscriber +[pyo3-tracing-subscriber-docs]: https://docs.rs/pyo3-tracing-subscriber +[add-submodule]: https://docs.rs/pyo3-tracing-subscriber/*/pyo3_tracing_subscriber/fn.add_submodule.html + +[pyo3-python-tracing-subscriber]: https://crates.io/crates/pyo3-python-tracing-subscriber +[pyo3-python-tracing-subscriber-docs]: https://docs.rs/pyo3-python-tracing-subscriber +[PythonCallbackLayerBridge]: https://docs.rs/pyo3-python-tracing-subscriber/*/pyo3_python_tracing_subscriber/struct.PythonCallbackLayerBridge.html +[working examples]: https://github.com/getsentry/pyo3-python-tracing-subscriber/tree/main/demo + +[Rust's `tracing` ecosystem]: https://crates.io/crates/tracing +[tracing-subscriber]: https://docs.rs/tracing-subscriber/*/tracing_subscriber/ +[the `Layer` interface]: https://docs.rs/tracing-subscriber/*/tracing_subscriber/layer/trait.Layer.html From af9f42cbf06bc1dbf44451767a0ebb6df3c84297 Mon Sep 17 00:00:00 2001 From: Kyle Barron Date: Wed, 15 Jan 2025 00:08:52 -0800 Subject: [PATCH 428/495] Add pyo3-bytes to readme (#4854) --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 82b8d390981..54e027571a2 100644 --- a/README.md +++ b/README.md @@ -181,6 +181,7 @@ about this topic. - [pyo3-asyncio](https://github.com/awestlake87/pyo3-asyncio) _Utilities for working with Python's Asyncio library and async functions_ - [rustimport](https://github.com/mityax/rustimport) _Directly import Rust files or crates from Python, without manual compilation step. Provides pyo3 integration by default and generates pyo3 binding code automatically._ - [pyo3-arrow](https://crates.io/crates/pyo3-arrow) _Lightweight [Apache Arrow](https://arrow.apache.org/) integration for pyo3._ +- [pyo3-bytes](https://crates.io/crates/pyo3-bytes) _Integration between [`bytes`](https://crates.io/crates/bytes) and pyo3._ ## Examples From ad5f6d404fcf0849ca03cdac2c498ea18e74a588 Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Wed, 15 Jan 2025 19:31:05 +0100 Subject: [PATCH 429/495] pyo3-ffi: expose `PyObject_Vectorcall(Method)` on the stable abi on 3.12+ (#4853) --- newsfragments/4853.added.md | 1 + pyo3-ffi/src/abstract_.rs | 24 ++++++ pyo3-ffi/src/cpython/abstract_.rs | 30 ++----- src/instance.rs | 44 ++++++++++ src/types/tuple.rs | 137 ++++++++++++++++-------------- 5 files changed, 147 insertions(+), 89 deletions(-) create mode 100644 newsfragments/4853.added.md diff --git a/newsfragments/4853.added.md b/newsfragments/4853.added.md new file mode 100644 index 00000000000..d3df4d219cc --- /dev/null +++ b/newsfragments/4853.added.md @@ -0,0 +1 @@ +pyo3-ffi: expose `PyObject_Vectorcall(Method)` on the stable abi on 3.12+ \ No newline at end of file diff --git a/pyo3-ffi/src/abstract_.rs b/pyo3-ffi/src/abstract_.rs index ce6c9b94735..82eecce05bd 100644 --- a/pyo3-ffi/src/abstract_.rs +++ b/pyo3-ffi/src/abstract_.rs @@ -1,5 +1,7 @@ use crate::object::*; use crate::pyport::Py_ssize_t; +#[cfg(any(Py_3_12, all(Py_3_8, not(Py_LIMITED_API))))] +use libc::size_t; use std::os::raw::{c_char, c_int}; #[inline] @@ -70,6 +72,28 @@ extern "C" { method: *mut PyObject, ... ) -> *mut PyObject; +} +#[cfg(any(Py_3_12, all(Py_3_8, not(Py_LIMITED_API))))] +pub const PY_VECTORCALL_ARGUMENTS_OFFSET: size_t = + 1 << (8 * std::mem::size_of::() as size_t - 1); + +extern "C" { + #[cfg_attr(PyPy, link_name = "PyPyObject_Vectorcall")] + #[cfg(any(Py_3_12, all(Py_3_11, not(Py_LIMITED_API))))] + pub fn PyObject_Vectorcall( + callable: *mut PyObject, + args: *const *mut PyObject, + nargsf: size_t, + kwnames: *mut PyObject, + ) -> *mut PyObject; + + #[cfg(any(Py_3_12, all(Py_3_9, not(any(Py_LIMITED_API, PyPy, GraalPy)))))] + pub fn PyObject_VectorcallMethod( + name: *mut PyObject, + args: *const *mut PyObject, + nargsf: size_t, + kwnames: *mut PyObject, + ) -> *mut PyObject; #[cfg_attr(PyPy, link_name = "PyPyObject_Type")] pub fn PyObject_Type(o: *mut PyObject) -> *mut PyObject; #[cfg_attr(PyPy, link_name = "PyPyObject_Size")] diff --git a/pyo3-ffi/src/cpython/abstract_.rs b/pyo3-ffi/src/cpython/abstract_.rs index 477ad02b747..5c7b16ff0e8 100644 --- a/pyo3-ffi/src/cpython/abstract_.rs +++ b/pyo3-ffi/src/cpython/abstract_.rs @@ -41,8 +41,8 @@ extern "C" { ) -> *mut PyObject; } -#[cfg(Py_3_8)] -pub const PY_VECTORCALL_ARGUMENTS_OFFSET: size_t = +#[cfg(Py_3_8)] // NB exported as public in abstract.rs from 3.12 +const PY_VECTORCALL_ARGUMENTS_OFFSET: size_t = 1 << (8 * std::mem::size_of::() as size_t - 1); #[cfg(Py_3_8)] @@ -91,7 +91,7 @@ pub unsafe fn _PyObject_VectorcallTstate( } } -#[cfg(all(Py_3_8, not(any(PyPy, GraalPy))))] +#[cfg(all(Py_3_8, not(any(PyPy, GraalPy, Py_3_11))))] // exported as a function from 3.11, see abstract.rs #[inline(always)] pub unsafe fn PyObject_Vectorcall( callable: *mut PyObject, @@ -103,16 +103,6 @@ pub unsafe fn PyObject_Vectorcall( } extern "C" { - #[cfg(all(PyPy, Py_3_8))] - #[cfg_attr(not(Py_3_9), link_name = "_PyPyObject_Vectorcall")] - #[cfg_attr(Py_3_9, link_name = "PyPyObject_Vectorcall")] - pub fn PyObject_Vectorcall( - callable: *mut PyObject, - args: *const *mut PyObject, - nargsf: size_t, - kwnames: *mut PyObject, - ) -> *mut PyObject; - #[cfg(Py_3_8)] #[cfg_attr( all(not(any(PyPy, GraalPy)), not(Py_3_9)), @@ -187,23 +177,13 @@ pub unsafe fn PyObject_CallOneArg(func: *mut PyObject, arg: *mut PyObject) -> *m _PyObject_VectorcallTstate(tstate, func, args, nargsf, std::ptr::null_mut()) } -extern "C" { - #[cfg(all(Py_3_9, not(any(PyPy, GraalPy))))] - pub fn PyObject_VectorcallMethod( - name: *mut PyObject, - args: *const *mut PyObject, - nargsf: size_t, - kwnames: *mut PyObject, - ) -> *mut PyObject; -} - #[cfg(all(Py_3_9, not(any(PyPy, GraalPy))))] #[inline(always)] pub unsafe fn PyObject_CallMethodNoArgs( self_: *mut PyObject, name: *mut PyObject, ) -> *mut PyObject { - PyObject_VectorcallMethod( + crate::PyObject_VectorcallMethod( name, &self_, 1 | PY_VECTORCALL_ARGUMENTS_OFFSET, @@ -220,7 +200,7 @@ pub unsafe fn PyObject_CallMethodOneArg( ) -> *mut PyObject { let args = [self_, arg]; assert!(!arg.is_null()); - PyObject_VectorcallMethod( + crate::PyObject_VectorcallMethod( name, args.as_ptr(), 2 | PY_VECTORCALL_ARGUMENTS_OFFSET, diff --git a/src/instance.rs b/src/instance.rs index 840416116f3..08a8779eadc 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -2020,6 +2020,50 @@ mod tests { }) } + #[test] + fn test_call_tuple_ref() { + let assert_repr = |obj: &Bound<'_, PyAny>, expected: &str| { + use crate::prelude::PyStringMethods; + assert_eq!( + obj.repr() + .unwrap() + .to_cow() + .unwrap() + .trim_matches(|c| c == '{' || c == '}'), + expected.trim_matches(|c| c == ',' || c == ' ') + ); + }; + + macro_rules! tuple { + ($py:ident, $($key: literal => $value: literal),+) => { + let ty_obj = $py.get_type::().into_pyobject($py).unwrap(); + assert!(ty_obj.call1(&(($(($key),)+),)).is_err()); + let obj = ty_obj.call1(&(($(($key, i32::from($value)),)+),)).unwrap(); + assert_repr(&obj, concat!($("'", $key, "'", ": ", stringify!($value), ", ",)+)); + assert!(obj.call_method1("update", &(($(($key),)+),)).is_err()); + obj.call_method1("update", &(($((i32::from($value), $key),)+),)).unwrap(); + assert_repr(&obj, concat!( + concat!($("'", $key, "'", ": ", stringify!($value), ", ",)+), + concat!($(stringify!($value), ": ", "'", $key, "'", ", ",)+) + )); + }; + } + + Python::with_gil(|py| { + tuple!(py, "a" => 1); + tuple!(py, "a" => 1, "b" => 2); + tuple!(py, "a" => 1, "b" => 2, "c" => 3); + tuple!(py, "a" => 1, "b" => 2, "c" => 3, "d" => 4); + tuple!(py, "a" => 1, "b" => 2, "c" => 3, "d" => 4, "e" => 5); + tuple!(py, "a" => 1, "b" => 2, "c" => 3, "d" => 4, "e" => 5, "f" => 6); + tuple!(py, "a" => 1, "b" => 2, "c" => 3, "d" => 4, "e" => 5, "f" => 6, "g" => 7); + tuple!(py, "a" => 1, "b" => 2, "c" => 3, "d" => 4, "e" => 5, "f" => 6, "g" => 7, "h" => 8); + tuple!(py, "a" => 1, "b" => 2, "c" => 3, "d" => 4, "e" => 5, "f" => 6, "g" => 7, "h" => 8, "i" => 9); + tuple!(py, "a" => 1, "b" => 2, "c" => 3, "d" => 4, "e" => 5, "f" => 6, "g" => 7, "h" => 8, "i" => 9, "j" => 10, "k" => 11); + tuple!(py, "a" => 1, "b" => 2, "c" => 3, "d" => 4, "e" => 5, "f" => 6, "g" => 7, "h" => 8, "i" => 9, "j" => 10, "k" => 11, "l" => 12); + }) + } + #[test] fn test_call_for_non_existing_method() { Python::with_gil(|py| { diff --git a/src/types/tuple.rs b/src/types/tuple.rs index a5de938140a..8147b872af5 100644 --- a/src/types/tuple.rs +++ b/src/types/tuple.rs @@ -594,7 +594,7 @@ macro_rules! tuple_conversion ({$length:expr,$(($refN:ident, $n:tt, $T:ident)),+ } } - #[cfg(all(Py_3_9, not(any(PyPy, GraalPy, Py_LIMITED_API))))] + #[cfg(all(not(any(PyPy, GraalPy)), any(all(Py_3_9, not(Py_LIMITED_API)), Py_3_12)))] fn call_positional( self, function: Borrowed<'_, 'py, PyAny>, @@ -603,30 +603,32 @@ macro_rules! tuple_conversion ({$length:expr,$(($refN:ident, $n:tt, $T:ident)),+ let py = function.py(); // We need this to drop the arguments correctly. let args_bound = [$(self.$n.into_bound_py_any(py)?,)*]; + + #[cfg(not(Py_LIMITED_API))] if $length == 1 { - unsafe { + return unsafe { ffi::PyObject_CallOneArg( function.as_ptr(), args_bound[0].as_ptr() ) .assume_owned_or_err(py) - } - } else { - // Prepend one null argument for `PY_VECTORCALL_ARGUMENTS_OFFSET`. - let mut args = [std::ptr::null_mut(), $(args_bound[$n].as_ptr()),*]; - unsafe { - ffi::PyObject_Vectorcall( - function.as_ptr(), - args.as_mut_ptr().add(1), - $length + ffi::PY_VECTORCALL_ARGUMENTS_OFFSET, - std::ptr::null_mut(), - ) - .assume_owned_or_err(py) - } + }; + } + + // Prepend one null argument for `PY_VECTORCALL_ARGUMENTS_OFFSET`. + let mut args = [std::ptr::null_mut(), $(args_bound[$n].as_ptr()),*]; + unsafe { + ffi::PyObject_Vectorcall( + function.as_ptr(), + args.as_mut_ptr().add(1), + $length + ffi::PY_VECTORCALL_ARGUMENTS_OFFSET, + std::ptr::null_mut(), + ) + .assume_owned_or_err(py) } } - #[cfg(all(Py_3_9, not(any(PyPy, GraalPy, Py_LIMITED_API))))] + #[cfg(all(not(any(PyPy, GraalPy)), any(all(Py_3_9, not(Py_LIMITED_API)), Py_3_12)))] fn call_method_positional( self, object: Borrowed<'_, 'py, PyAny>, @@ -636,28 +638,31 @@ macro_rules! tuple_conversion ({$length:expr,$(($refN:ident, $n:tt, $T:ident)),+ let py = object.py(); // We need this to drop the arguments correctly. let args_bound = [$(self.$n.into_bound_py_any(py)?,)*]; + + #[cfg(not(Py_LIMITED_API))] if $length == 1 { - unsafe { + return unsafe { ffi::PyObject_CallMethodOneArg( object.as_ptr(), method_name.as_ptr(), args_bound[0].as_ptr(), ) .assume_owned_or_err(py) - } - } else { - let mut args = [object.as_ptr(), $(args_bound[$n].as_ptr()),*]; - unsafe { - ffi::PyObject_VectorcallMethod( - method_name.as_ptr(), - args.as_mut_ptr(), - // +1 for the receiver. - 1 + $length + ffi::PY_VECTORCALL_ARGUMENTS_OFFSET, - std::ptr::null_mut(), - ) - .assume_owned_or_err(py) - } + }; + } + + let mut args = [object.as_ptr(), $(args_bound[$n].as_ptr()),*]; + unsafe { + ffi::PyObject_VectorcallMethod( + method_name.as_ptr(), + args.as_mut_ptr(), + // +1 for the receiver. + 1 + $length + ffi::PY_VECTORCALL_ARGUMENTS_OFFSET, + std::ptr::null_mut(), + ) + .assume_owned_or_err(py) } + } #[cfg(not(all(Py_3_9, not(any(PyPy, GraalPy, Py_LIMITED_API)))))] @@ -670,7 +675,7 @@ macro_rules! tuple_conversion ({$length:expr,$(($refN:ident, $n:tt, $T:ident)),+ self.into_pyobject_or_pyerr(function.py())?.call(function, kwargs, token) } - #[cfg(not(all(Py_3_9, not(any(PyPy, GraalPy, Py_LIMITED_API)))))] + #[cfg(not(all(not(any(PyPy, GraalPy)), any(all(Py_3_9, not(Py_LIMITED_API)), Py_3_12))))] fn call_positional( self, function: Borrowed<'_, 'py, PyAny>, @@ -679,7 +684,7 @@ macro_rules! tuple_conversion ({$length:expr,$(($refN:ident, $n:tt, $T:ident)),+ self.into_pyobject_or_pyerr(function.py())?.call_positional(function, token) } - #[cfg(not(all(Py_3_9, not(any(PyPy, GraalPy, Py_LIMITED_API)))))] + #[cfg(not(all(not(any(PyPy, GraalPy)), any(all(Py_3_9, not(Py_LIMITED_API)), Py_3_12))))] fn call_method_positional( self, object: Borrowed<'_, 'py, PyAny>, @@ -719,7 +724,7 @@ macro_rules! tuple_conversion ({$length:expr,$(($refN:ident, $n:tt, $T:ident)),+ } } - #[cfg(all(Py_3_9, not(any(PyPy, GraalPy, Py_LIMITED_API))))] + #[cfg(all(not(any(PyPy, GraalPy)), any(all(Py_3_9, not(Py_LIMITED_API)), Py_3_12)))] fn call_positional( self, function: Borrowed<'_, 'py, PyAny>, @@ -728,30 +733,32 @@ macro_rules! tuple_conversion ({$length:expr,$(($refN:ident, $n:tt, $T:ident)),+ let py = function.py(); // We need this to drop the arguments correctly. let args_bound = [$(self.$n.into_bound_py_any(py)?,)*]; + + #[cfg(not(Py_LIMITED_API))] if $length == 1 { - unsafe { + return unsafe { ffi::PyObject_CallOneArg( function.as_ptr(), args_bound[0].as_ptr() ) .assume_owned_or_err(py) - } - } else { - // Prepend one null argument for `PY_VECTORCALL_ARGUMENTS_OFFSET`. - let mut args = [std::ptr::null_mut(), $(args_bound[$n].as_ptr()),*]; - unsafe { - ffi::PyObject_Vectorcall( - function.as_ptr(), - args.as_mut_ptr().add(1), - $length + ffi::PY_VECTORCALL_ARGUMENTS_OFFSET, - std::ptr::null_mut(), - ) - .assume_owned_or_err(py) - } + }; + } + + // Prepend one null argument for `PY_VECTORCALL_ARGUMENTS_OFFSET`. + let mut args = [std::ptr::null_mut(), $(args_bound[$n].as_ptr()),*]; + unsafe { + ffi::PyObject_Vectorcall( + function.as_ptr(), + args.as_mut_ptr().add(1), + $length + ffi::PY_VECTORCALL_ARGUMENTS_OFFSET, + std::ptr::null_mut(), + ) + .assume_owned_or_err(py) } } - #[cfg(all(Py_3_9, not(any(PyPy, GraalPy, Py_LIMITED_API))))] + #[cfg(all(not(any(PyPy, GraalPy)), any(all(Py_3_9, not(Py_LIMITED_API)), Py_3_12)))] fn call_method_positional( self, object: Borrowed<'_, 'py, PyAny>, @@ -761,27 +768,29 @@ macro_rules! tuple_conversion ({$length:expr,$(($refN:ident, $n:tt, $T:ident)),+ let py = object.py(); // We need this to drop the arguments correctly. let args_bound = [$(self.$n.into_bound_py_any(py)?,)*]; + + #[cfg(not(Py_LIMITED_API))] if $length == 1 { - unsafe { + return unsafe { ffi::PyObject_CallMethodOneArg( object.as_ptr(), method_name.as_ptr(), args_bound[0].as_ptr(), ) .assume_owned_or_err(py) - } - } else { - let mut args = [object.as_ptr(), $(args_bound[$n].as_ptr()),*]; - unsafe { - ffi::PyObject_VectorcallMethod( - method_name.as_ptr(), - args.as_mut_ptr(), - // +1 for the receiver. - 1 + $length + ffi::PY_VECTORCALL_ARGUMENTS_OFFSET, - std::ptr::null_mut(), - ) - .assume_owned_or_err(py) - } + }; + } + + let mut args = [object.as_ptr(), $(args_bound[$n].as_ptr()),*]; + unsafe { + ffi::PyObject_VectorcallMethod( + method_name.as_ptr(), + args.as_mut_ptr(), + // +1 for the receiver. + 1 + $length + ffi::PY_VECTORCALL_ARGUMENTS_OFFSET, + std::ptr::null_mut(), + ) + .assume_owned_or_err(py) } } @@ -795,7 +804,7 @@ macro_rules! tuple_conversion ({$length:expr,$(($refN:ident, $n:tt, $T:ident)),+ self.into_pyobject_or_pyerr(function.py())?.call(function, kwargs, token) } - #[cfg(not(all(Py_3_9, not(any(PyPy, GraalPy, Py_LIMITED_API)))))] + #[cfg(not(all(not(any(PyPy, GraalPy)), any(all(Py_3_9, not(Py_LIMITED_API)), Py_3_12))))] fn call_positional( self, function: Borrowed<'_, 'py, PyAny>, @@ -804,7 +813,7 @@ macro_rules! tuple_conversion ({$length:expr,$(($refN:ident, $n:tt, $T:ident)),+ self.into_pyobject_or_pyerr(function.py())?.call_positional(function, token) } - #[cfg(not(all(Py_3_9, not(any(PyPy, GraalPy, Py_LIMITED_API)))))] + #[cfg(not(all(not(any(PyPy, GraalPy)), any(all(Py_3_9, not(Py_LIMITED_API)), Py_3_12))))] fn call_method_positional( self, object: Borrowed<'_, 'py, PyAny>, From e7041ba70826f98dcb9f2c88f75db87e9568822c Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers <7943856+bschoenmaeckers@users.noreply.github.com> Date: Mon, 20 Jan 2025 18:02:59 +0100 Subject: [PATCH 430/495] Add jiff into/from python convertions (#4823) * Add jiff into/from python convertions * Remove `IntoPyObject<'py> for Span` * Update guide/src/features.md * fix docs ci --------- Co-authored-by: Icxolu <10486322+Icxolu@users.noreply.github.com> --- .github/workflows/ci.yml | 2 +- Cargo.toml | 1 + guide/src/features.md | 12 + newsfragments/4823.added.md | 1 + noxfile.py | 70 +- src/conversions/chrono.rs | 54 +- src/conversions/jiff.rs | 1338 +++++++++++++++++++++++++++++++++++ src/conversions/mod.rs | 1 + src/types/datetime.rs | 4 +- src/types/datetime_abi3.rs | 51 ++ src/types/mod.rs | 2 + 11 files changed, 1455 insertions(+), 81 deletions(-) create mode 100644 newsfragments/4823.added.md create mode 100644 src/conversions/jiff.rs create mode 100644 src/types/datetime_abi3.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 76c5ab06f29..14531d17144 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -439,7 +439,7 @@ jobs: - uses: dtolnay/rust-toolchain@nightly with: components: rust-src - - run: cargo rustdoc --lib --no-default-features --features full -Zunstable-options --config "build.rustdocflags=[\"--cfg\", \"docsrs\"]" + - run: cargo rustdoc --lib --no-default-features --features full,jiff-01 -Zunstable-options --config "build.rustdocflags=[\"--cfg\", \"docsrs\"]" coverage: if: ${{ github.event_name != 'merge_group' }} diff --git a/Cargo.toml b/Cargo.toml index 18cceffbd0c..d1975f706f1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,6 +39,7 @@ either = { version = "1.9", optional = true } eyre = { version = ">= 0.6.8, < 0.7", optional = true } hashbrown = { version = ">= 0.14.5, < 0.16", optional = true } indexmap = { version = ">= 2.5.0, < 3", optional = true } +jiff-01 = { package = "jiff", version = "0.1.18", optional = true } num-bigint = { version = "0.4.2", optional = true } num-complex = { version = ">= 0.4.6, < 0.5", optional = true } num-rational = {version = "0.4.1", optional = true } diff --git a/guide/src/features.md b/guide/src/features.md index d801e2dd1e4..3c3c0cd15d3 100644 --- a/guide/src/features.md +++ b/guide/src/features.md @@ -151,6 +151,18 @@ Adds a dependency on [hashbrown](https://docs.rs/hashbrown) and enables conversi Adds a dependency on [indexmap](https://docs.rs/indexmap) and enables conversions into its [`IndexMap`](https://docs.rs/indexmap/latest/indexmap/map/struct.IndexMap.html) type. +### `jiff-01` + +Adds a dependency on [jiff@0.1](https://docs.rs/jiff/0.1) and requires MSRV 1.70. Enables a conversion from [jiff](https://docs.rs/jiff)'s types to python: +- [SignedDuration](https://docs.rs/jiff/0.1/jiff/struct.SignedDuration.html) -> [`PyDelta`]({{#PYO3_DOCS_URL}}/pyo3/types/struct.PyDelta.html) +- [TimeZone](https://docs.rs/jiff/0.1/jiff/tz/struct.TimeZone.html) -> [`PyTzInfo`]({{#PYO3_DOCS_URL}}/pyo3/types/struct.PyTzInfo.html) +- [Offset](https://docs.rs/jiff/0.1/jiff/tz/struct.Offset.html) -> [`PyTzInfo`]({{#PYO3_DOCS_URL}}/pyo3/types/struct.PyTzInfo.html) +- [Date](https://docs.rs/jiff/0.1/jiff/civil/struct.Date.html) -> [`PyDate`]({{#PYO3_DOCS_URL}}/pyo3/types/struct.PyDate.html) +- [Time](https://docs.rs/jiff/0.1/jiff/civil/struct.Time.html) -> [`PyTime`]({{#PYO3_DOCS_URL}}/pyo3/types/struct.PyTime.html) +- [DateTime](https://docs.rs/jiff/0.1/jiff/civil/struct.DateTime.html) -> [`PyDateTime`]({{#PYO3_DOCS_URL}}/pyo3/types/struct.PyDateTime.html) +- [Zoned](https://docs.rs/jiff/0.1/jiff/struct.Zoned.html) -> [`PyDateTime`]({{#PYO3_DOCS_URL}}/pyo3/types/struct.PyDateTime.html) +- [Timestamp](https://docs.rs/jiff/0.1/jiff/struct.Timestamp.html) -> [`PyDateTime`]({{#PYO3_DOCS_URL}}/pyo3/types/struct.PyDateTime.html) + ### `num-bigint` Adds a dependency on [num-bigint](https://docs.rs/num-bigint) and enables conversions into its [`BigInt`](https://docs.rs/num-bigint/latest/num_bigint/struct.BigInt.html) and [`BigUint`](https://docs.rs/num-bigint/latest/num_bigint/struct.BigUint.html) types. diff --git a/newsfragments/4823.added.md b/newsfragments/4823.added.md new file mode 100644 index 00000000000..f51227a20b2 --- /dev/null +++ b/newsfragments/4823.added.md @@ -0,0 +1 @@ +Add jiff to/from python conversions. diff --git a/noxfile.py b/noxfile.py index ed7759f5f99..9f94175ab16 100644 --- a/noxfile.py +++ b/noxfile.py @@ -10,7 +10,17 @@ from functools import lru_cache from glob import glob from pathlib import Path -from typing import Any, Callable, Dict, Iterable, Iterator, List, Optional, Tuple +from typing import ( + Any, + Callable, + Dict, + Iterable, + Iterator, + List, + Optional, + Tuple, + Generator, +) import nox import nox.command @@ -55,9 +65,9 @@ def test_rust(session: nox.Session): if not FREE_THREADED_BUILD: _run_cargo_test(session, features="abi3") if "skip-full" not in session.posargs: - _run_cargo_test(session, features="full") + _run_cargo_test(session, features="full jiff-01") if not FREE_THREADED_BUILD: - _run_cargo_test(session, features="abi3 full") + _run_cargo_test(session, features="abi3 full jiff-01") @nox.session(name="test-py", venv_backend="none") @@ -381,6 +391,12 @@ def docs(session: nox.Session) -> None: rustdoc_flags.append(session.env.get("RUSTDOCFLAGS", "")) session.env["RUSTDOCFLAGS"] = " ".join(rustdoc_flags) + features = "full" + + if get_rust_version()[:2] >= (1, 70): + # jiff needs MSRC 1.70+ + features += ",jiff-01" + shutil.rmtree(PYO3_DOCS_TARGET, ignore_errors=True) _run_cargo( session, @@ -388,7 +404,7 @@ def docs(session: nox.Session) -> None: "doc", "--lib", "--no-default-features", - "--features=full", + f"--features={features}", "--no-deps", "--workspace", *cargo_flags, @@ -761,8 +777,8 @@ def update_ui_tests(session: nox.Session): env["TRYBUILD"] = "overwrite" command = ["test", "--test", "test_compile_error"] _run_cargo(session, *command, env=env) - _run_cargo(session, *command, "--features=full", env=env) - _run_cargo(session, *command, "--features=abi3,full", env=env) + _run_cargo(session, *command, "--features=full,jiff-01", env=env) + _run_cargo(session, *command, "--features=abi3,full,jiff-01", env=env) def _build_docs_for_ffi_check(session: nox.Session) -> None: @@ -779,7 +795,7 @@ def _get_rust_info() -> Tuple[str, ...]: return tuple(output.splitlines()) -def _get_rust_version() -> Tuple[int, int, int, List[str]]: +def get_rust_version() -> Tuple[int, int, int, List[str]]: for line in _get_rust_info(): if line.startswith(_RELEASE_LINE_START): version = line[len(_RELEASE_LINE_START) :].strip() @@ -795,30 +811,30 @@ def _get_rust_default_target() -> str: @lru_cache() -def _get_feature_sets() -> Tuple[Tuple[str, ...], ...]: +def _get_feature_sets() -> Generator[Tuple[str, ...], None, None]: """Returns feature sets to use for clippy job""" cargo_target = os.getenv("CARGO_BUILD_TARGET", "") + + yield from ( + ("--no-default-features",), + ( + "--no-default-features", + "--features=abi3", + ), + ) + + features = "full" + if "wasm32-wasip1" not in cargo_target: # multiple-pymethods not supported on wasm - return ( - ("--no-default-features",), - ( - "--no-default-features", - "--features=abi3", - ), - ("--features=full multiple-pymethods",), - ("--features=abi3 full multiple-pymethods",), - ) - else: - return ( - ("--no-default-features",), - ( - "--no-default-features", - "--features=abi3", - ), - ("--features=full",), - ("--features=abi3 full",), - ) + features += ",multiple-pymethods" + + if get_rust_version()[:2] >= (1, 70): + # jiff needs MSRC 1.70+ + features += ",jiff-01" + + yield (f"--features={features}",) + yield (f"--features=abi3,{features}",) _RELEASE_LINE_START = "release: " diff --git a/src/conversions/chrono.rs b/src/conversions/chrono.rs index 04febb43b78..342ed659e22 100644 --- a/src/conversions/chrono.rs +++ b/src/conversions/chrono.rs @@ -44,11 +44,13 @@ use crate::conversion::IntoPyObject; use crate::exceptions::{PyTypeError, PyUserWarning, PyValueError}; #[cfg(Py_LIMITED_API)] -use crate::sync::GILOnceCell; +use crate::intern; use crate::types::any::PyAnyMethods; #[cfg(not(Py_LIMITED_API))] use crate::types::datetime::timezone_from_offset; #[cfg(Py_LIMITED_API)] +use crate::types::datetime_abi3::{check_type, timezone_utc, DatetimeTypes}; +#[cfg(Py_LIMITED_API)] use crate::types::IntoPyDict; use crate::types::PyNone; #[cfg(not(Py_LIMITED_API))] @@ -57,8 +59,6 @@ use crate::types::{ PyTzInfo, PyTzInfoAccess, }; use crate::{ffi, Bound, FromPyObject, IntoPyObjectExt, PyAny, PyErr, PyObject, PyResult, Python}; -#[cfg(Py_LIMITED_API)] -use crate::{intern, DowncastError}; #[allow(deprecated)] use crate::{IntoPy, ToPyObject}; use chrono::offset::{FixedOffset, Utc}; @@ -811,54 +811,6 @@ fn py_time_to_naive_time(py_time: &Bound<'_, PyAny>) -> PyResult { .ok_or_else(|| PyValueError::new_err("invalid or out-of-range time")) } -#[cfg(Py_LIMITED_API)] -fn check_type(value: &Bound<'_, PyAny>, t: &PyObject, type_name: &'static str) -> PyResult<()> { - if !value.is_instance(t.bind(value.py()))? { - return Err(DowncastError::new(value, type_name).into()); - } - Ok(()) -} - -#[cfg(Py_LIMITED_API)] -struct DatetimeTypes { - date: PyObject, - datetime: PyObject, - time: PyObject, - timedelta: PyObject, - timezone: PyObject, - timezone_utc: PyObject, - tzinfo: PyObject, -} - -#[cfg(Py_LIMITED_API)] -impl DatetimeTypes { - fn get(py: Python<'_>) -> &Self { - Self::try_get(py).expect("failed to load datetime module") - } - - fn try_get(py: Python<'_>) -> PyResult<&Self> { - static TYPES: GILOnceCell = GILOnceCell::new(); - TYPES.get_or_try_init(py, || { - let datetime = py.import("datetime")?; - let timezone = datetime.getattr("timezone")?; - Ok::<_, PyErr>(Self { - date: datetime.getattr("date")?.into(), - datetime: datetime.getattr("datetime")?.into(), - time: datetime.getattr("time")?.into(), - timedelta: datetime.getattr("timedelta")?.into(), - timezone_utc: timezone.getattr("utc")?.into(), - timezone: timezone.into(), - tzinfo: datetime.getattr("tzinfo")?.into(), - }) - }) - } -} - -#[cfg(Py_LIMITED_API)] -fn timezone_utc(py: Python<'_>) -> Bound<'_, PyAny> { - DatetimeTypes::get(py).timezone_utc.bind(py).clone() -} - #[cfg(test)] mod tests { use super::*; diff --git a/src/conversions/jiff.rs b/src/conversions/jiff.rs new file mode 100644 index 00000000000..be1a68bea0e --- /dev/null +++ b/src/conversions/jiff.rs @@ -0,0 +1,1338 @@ +#![cfg(feature = "jiff-01")] + +//! Conversions to and from [jiff](https://docs.rs/jiff/)’s `Span`, `SignedDuration`, `TimeZone`, +//! `Offset`, `Date`, `Time`, `DateTime`, `Zoned`, and `Timestamp`. +//! +//! # Setup +//! +//! To use this feature, add this to your **`Cargo.toml`**: +//! +//! ```toml +//! [dependencies] +//! jiff = "0.1" +#![doc = concat!("pyo3 = { version = \"", env!("CARGO_PKG_VERSION"), "\", features = [\"jiff-01\"] }")] +//! ``` +//! +//! Note that you must use compatible versions of jiff and PyO3. +//! The required jiff version may vary based on the version of PyO3. Jiff also requires a MSRV +//! of 1.70. +//! +//! # Example: Convert a `datetime.datetime` to jiff `Zoned` +//! +//! ```rust +//! # #![cfg_attr(windows, allow(unused_imports))] +//! # use jiff_01 as jiff; +//! use jiff::{Zoned, SignedDuration, ToSpan}; +//! use pyo3::{Python, PyResult, IntoPyObject, types::PyAnyMethods}; +//! +//! # #[cfg(windows)] +//! # fn main() -> () {} +//! # #[cfg(not(windows))] +//! fn main() -> PyResult<()> { +//! pyo3::prepare_freethreaded_python(); +//! Python::with_gil(|py| { +//! // Build some jiff values +//! let jiff_zoned = Zoned::now(); +//! let jiff_span = 1.second(); +//! // Convert them to Python +//! let py_datetime = jiff_zoned.into_pyobject(py)?; +//! let py_timedelta = SignedDuration::try_from(jiff_span)?.into_pyobject(py)?; +//! // Do an operation in Python +//! let py_sum = py_datetime.call_method1("__add__", (py_timedelta,))?; +//! // Convert back to Rust +//! let jiff_sum: Zoned = py_sum.extract()?; +//! println!("Zoned: {}", jiff_sum); +//! Ok(()) +//! }) +//! } +//! ``` +use crate::exceptions::{PyTypeError, PyValueError}; +use crate::pybacked::PyBackedStr; +use crate::sync::GILOnceCell; +#[cfg(not(Py_LIMITED_API))] +use crate::types::datetime::timezone_from_offset; +#[cfg(Py_LIMITED_API)] +use crate::types::datetime_abi3::{check_type, timezone_utc, DatetimeTypes}; +#[cfg(Py_LIMITED_API)] +use crate::types::IntoPyDict; +#[cfg(not(Py_LIMITED_API))] +use crate::types::{ + timezone_utc, PyDate, PyDateAccess, PyDateTime, PyDelta, PyDeltaAccess, PyTime, PyTimeAccess, + PyTzInfo, PyTzInfoAccess, +}; +use crate::types::{PyAnyMethods, PyNone, PyType}; +use crate::{intern, Bound, FromPyObject, IntoPyObject, Py, PyAny, PyErr, PyResult, Python}; +use jiff::civil::{Date, DateTime, Time}; +use jiff::tz::{Offset, TimeZone}; +use jiff::{SignedDuration, Span, Timestamp, Zoned}; +#[cfg(feature = "jiff-01")] +use jiff_01 as jiff; + +#[cfg(not(Py_LIMITED_API))] +fn datetime_to_pydatetime<'py>( + py: Python<'py>, + datetime: &DateTime, + fold: bool, + timezone: Option<&TimeZone>, +) -> PyResult> { + PyDateTime::new_with_fold( + py, + datetime.year().into(), + datetime.month().try_into()?, + datetime.day().try_into()?, + datetime.hour().try_into()?, + datetime.minute().try_into()?, + datetime.second().try_into()?, + (datetime.subsec_nanosecond() / 1000).try_into()?, + timezone + .map(|tz| tz.into_pyobject(py)) + .transpose()? + .as_ref(), + fold, + ) +} + +#[cfg(Py_LIMITED_API)] +fn datetime_to_pydatetime<'py>( + py: Python<'py>, + datetime: &DateTime, + fold: bool, + timezone: Option<&TimeZone>, +) -> PyResult> { + DatetimeTypes::try_get(py)?.datetime.bind(py).call( + ( + datetime.year(), + datetime.month(), + datetime.day(), + datetime.hour(), + datetime.minute(), + datetime.second(), + datetime.subsec_nanosecond() / 1000, + timezone, + ), + Some(&[("fold", fold as u8)].into_py_dict(py)?), + ) +} + +#[cfg(not(Py_LIMITED_API))] +fn pytime_to_time(time: &impl PyTimeAccess) -> PyResult

[`ToPyObject`] is a conversion trait that allows various objects to be From 903afcd6f598144cb422ed2b2c6b3f3b1c1c77f0 Mon Sep 17 00:00:00 2001 From: Owen Leung Date: Fri, 7 Feb 2025 19:10:32 +0800 Subject: [PATCH 437/495] Optimize nth and nth_back for BoundListIterator (#4810) * Optimize nth and nth_back for BoundListIterator. Add unit test and benchmarks * Fix fmt and newsfragment CI * Fix clippy and changelog CI * Revise Impl of nth and nth_back. Impl advance_by * Fix failing fmt * Fix failing ruff test * branch out nth, nth_unchecked, nth_back, nth_back_unchecked. * Fix fmt * Revise advance_by impl. add advance_by unittest. * Fix fmt * Fix clippy unused function warning * Set appropriate Py_LIMITED_API flag * Rewrite nth & nth_back using conditional compilation. Rearrange flags for proper compilation * fix fmt * fix failing CI * Impl advance_back_by. Remove cfg flag for with_critical_section * refactor advance_by and advance_back_by. Add back cfg for with_critical_section * Put allow deadcode for with_critical_section * Remove use of get_item. Revise changelog --- newsfragments/4810.added.md | 1 + pyo3-benches/benches/bench_list.rs | 30 ++- src/lib.rs | 2 +- src/types/list.rs | 288 ++++++++++++++++++++++++++++- 4 files changed, 313 insertions(+), 8 deletions(-) create mode 100644 newsfragments/4810.added.md diff --git a/newsfragments/4810.added.md b/newsfragments/4810.added.md new file mode 100644 index 00000000000..00c7c9e1127 --- /dev/null +++ b/newsfragments/4810.added.md @@ -0,0 +1 @@ +Optimizes `nth`, `nth_back`, `advance_by` and `advance_back_by` for `BoundListIterator` \ No newline at end of file diff --git a/pyo3-benches/benches/bench_list.rs b/pyo3-benches/benches/bench_list.rs index cc790db37bf..7a19452455e 100644 --- a/pyo3-benches/benches/bench_list.rs +++ b/pyo3-benches/benches/bench_list.rs @@ -39,7 +39,33 @@ fn list_get_item(b: &mut Bencher<'_>) { }); } -#[cfg(not(any(Py_LIMITED_API, Py_GIL_DISABLED)))] +fn list_nth(b: &mut Bencher<'_>) { + Python::with_gil(|py| { + const LEN: usize = 50; + let list = PyList::new_bound(py, 0..LEN); + let mut sum = 0; + b.iter(|| { + for i in 0..LEN { + sum += list.iter().nth(i).unwrap().extract::().unwrap(); + } + }); + }); +} + +fn list_nth_back(b: &mut Bencher<'_>) { + Python::with_gil(|py| { + const LEN: usize = 50; + let list = PyList::new_bound(py, 0..LEN); + let mut sum = 0; + b.iter(|| { + for i in 0..LEN { + sum += list.iter().nth_back(i).unwrap().extract::().unwrap(); + } + }); + }); +} + +#[cfg(not(Py_LIMITED_API))] fn list_get_item_unchecked(b: &mut Bencher<'_>) { Python::with_gil(|py| { const LEN: usize = 50_000; @@ -66,6 +92,8 @@ fn sequence_from_list(b: &mut Bencher<'_>) { fn criterion_benchmark(c: &mut Criterion) { c.bench_function("iter_list", iter_list); c.bench_function("list_new", list_new); + c.bench_function("list_nth", list_nth); + c.bench_function("list_nth_back", list_nth_back); c.bench_function("list_get_item", list_get_item); #[cfg(not(any(Py_LIMITED_API, Py_GIL_DISABLED)))] c.bench_function("list_get_item_unchecked", list_get_item_unchecked); diff --git a/src/lib.rs b/src/lib.rs index 1ecbdc4f61b..2a0c289204c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,7 @@ #![warn(missing_docs)] #![cfg_attr( feature = "nightly", - feature(auto_traits, negative_impls, try_trait_v2) + feature(auto_traits, negative_impls, try_trait_v2, iter_advance_by) )] #![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] // Deny some lints in doctests. diff --git a/src/types/list.rs b/src/types/list.rs index 76da36d00b9..51af830d6f5 100644 --- a/src/types/list.rs +++ b/src/types/list.rs @@ -1,16 +1,16 @@ -use std::iter::FusedIterator; - use crate::err::{self, PyResult}; use crate::ffi::{self, Py_ssize_t}; use crate::ffi_ptr_ext::FfiPtrExt; use crate::internal_tricks::get_ssize_index; +use crate::types::any::PyAnyMethods; +use crate::types::sequence::PySequenceMethods; use crate::types::{PySequence, PyTuple}; use crate::{ Borrowed, Bound, BoundObject, IntoPyObject, IntoPyObjectExt, PyAny, PyErr, PyObject, Python, }; - -use crate::types::any::PyAnyMethods; -use crate::types::sequence::PySequenceMethods; +use std::iter::FusedIterator; +#[cfg(feature = "nightly")] +use std::num::NonZero; /// Represents a Python `list`. /// @@ -547,6 +547,35 @@ impl<'py> BoundListIterator<'py> { } } + #[inline] + #[cfg(not(feature = "nightly"))] + fn nth( + index: &mut Index, + length: &mut Length, + list: &Bound<'py, PyList>, + n: usize, + ) -> Option> { + let length = length.0.min(list.len()); + let target_index = index.0 + n; + if target_index < length { + let item = { + #[cfg(Py_LIMITED_API)] + { + list.get_item(target_index).expect("get-item failed") + } + + #[cfg(not(Py_LIMITED_API))] + { + unsafe { list.get_item_unchecked(target_index) } + } + }; + index.0 = target_index + 1; + Some(item) + } else { + None + } + } + /// # Safety /// /// On the free-threaded build, caller must verify they have exclusive @@ -589,7 +618,36 @@ impl<'py> BoundListIterator<'py> { } } - #[cfg(not(Py_LIMITED_API))] + #[inline] + #[cfg(not(feature = "nightly"))] + fn nth_back( + index: &mut Index, + length: &mut Length, + list: &Bound<'py, PyList>, + n: usize, + ) -> Option> { + let length_size = length.0.min(list.len()); + if index.0 + n < length_size { + let target_index = length_size - n - 1; + let item = { + #[cfg(not(Py_LIMITED_API))] + { + unsafe { list.get_item_unchecked(target_index) } + } + + #[cfg(Py_LIMITED_API)] + { + list.get_item(target_index).expect("get-item failed") + } + }; + length.0 = target_index; + Some(item) + } else { + None + } + } + + #[allow(dead_code)] fn with_critical_section( &mut self, f: impl FnOnce(&mut Index, &mut Length, &Bound<'py, PyList>) -> R, @@ -625,6 +683,12 @@ impl<'py> Iterator for BoundListIterator<'py> { } } + #[inline] + #[cfg(not(feature = "nightly"))] + fn nth(&mut self, n: usize) -> Option { + self.with_critical_section(|index, length, list| Self::nth(index, length, list, n)) + } + #[inline] fn size_hint(&self) -> (usize, Option) { let len = self.len(); @@ -750,6 +814,32 @@ impl<'py> Iterator for BoundListIterator<'py> { None }) } + + #[inline] + #[cfg(feature = "nightly")] + fn advance_by(&mut self, n: usize) -> Result<(), NonZero> { + self.with_critical_section(|index, length, list| { + let max_len = length.0.min(list.len()); + let currently_at = index.0; + if currently_at >= max_len { + if n == 0 { + return Ok(()); + } else { + return Err(unsafe { NonZero::new_unchecked(n) }); + } + } + + let items_left = max_len - currently_at; + if n <= items_left { + index.0 += n; + Ok(()) + } else { + index.0 = max_len; + let remainder = n - items_left; + Err(unsafe { NonZero::new_unchecked(remainder) }) + } + }) + } } impl DoubleEndedIterator for BoundListIterator<'_> { @@ -772,6 +862,12 @@ impl DoubleEndedIterator for BoundListIterator<'_> { } } + #[inline] + #[cfg(not(feature = "nightly"))] + fn nth_back(&mut self, n: usize) -> Option { + self.with_critical_section(|index, length, list| Self::nth_back(index, length, list, n)) + } + #[inline] #[cfg(all(Py_GIL_DISABLED, not(feature = "nightly")))] fn rfold(mut self, init: B, mut f: F) -> B @@ -804,6 +900,32 @@ impl DoubleEndedIterator for BoundListIterator<'_> { R::from_output(accum) }) } + + #[inline] + #[cfg(feature = "nightly")] + fn advance_back_by(&mut self, n: usize) -> Result<(), NonZero> { + self.with_critical_section(|index, length, list| { + let max_len = length.0.min(list.len()); + let currently_at = index.0; + if currently_at >= max_len { + if n == 0 { + return Ok(()); + } else { + return Err(unsafe { NonZero::new_unchecked(n) }); + } + } + + let items_left = max_len - currently_at; + if n <= items_left { + length.0 = max_len - n; + Ok(()) + } else { + length.0 = max_len; + let remainder = n - items_left; + Err(unsafe { NonZero::new_unchecked(remainder) }) + } + }) + } } impl ExactSizeIterator for BoundListIterator<'_> { @@ -839,6 +961,8 @@ mod tests { use crate::types::sequence::PySequenceMethods; use crate::types::{PyList, PyTuple}; use crate::{ffi, IntoPyObject, PyResult, Python}; + #[cfg(feature = "nightly")] + use std::num::NonZero; #[test] fn test_new() { @@ -1502,4 +1626,156 @@ mod tests { assert!(tuple.eq(tuple_expected).unwrap()); }) } + + #[test] + fn test_iter_nth() { + Python::with_gil(|py| { + let v = vec![6, 7, 8, 9, 10]; + let ob = (&v).into_pyobject(py).unwrap(); + let list = ob.downcast::().unwrap(); + + let mut iter = list.iter(); + iter.next(); + assert_eq!(iter.nth(1).unwrap().extract::().unwrap(), 8); + assert_eq!(iter.nth(1).unwrap().extract::().unwrap(), 10); + assert!(iter.nth(1).is_none()); + + let v: Vec = vec![]; + let ob = (&v).into_pyobject(py).unwrap(); + let list = ob.downcast::().unwrap(); + + let mut iter = list.iter(); + iter.next(); + assert!(iter.nth(1).is_none()); + + let v = vec![1, 2, 3]; + let ob = (&v).into_pyobject(py).unwrap(); + let list = ob.downcast::().unwrap(); + + let mut iter = list.iter(); + assert!(iter.nth(10).is_none()); + + let v = vec![6, 7, 8, 9, 10]; + let ob = (&v).into_pyobject(py).unwrap(); + let list = ob.downcast::().unwrap(); + let mut iter = list.iter(); + assert_eq!(iter.next().unwrap().extract::().unwrap(), 6); + assert_eq!(iter.nth(2).unwrap().extract::().unwrap(), 9); + assert_eq!(iter.next().unwrap().extract::().unwrap(), 10); + + let mut iter = list.iter(); + assert_eq!(iter.nth_back(1).unwrap().extract::().unwrap(), 9); + assert_eq!(iter.nth(2).unwrap().extract::().unwrap(), 8); + assert!(iter.next().is_none()); + }); + } + + #[test] + fn test_iter_nth_back() { + Python::with_gil(|py| { + let v = vec![1, 2, 3, 4, 5]; + let ob = (&v).into_pyobject(py).unwrap(); + let list = ob.downcast::().unwrap(); + + let mut iter = list.iter(); + assert_eq!(iter.nth_back(0).unwrap().extract::().unwrap(), 5); + assert_eq!(iter.nth_back(1).unwrap().extract::().unwrap(), 3); + assert!(iter.nth_back(2).is_none()); + + let v: Vec = vec![]; + let ob = (&v).into_pyobject(py).unwrap(); + let list = ob.downcast::().unwrap(); + + let mut iter = list.iter(); + assert!(iter.nth_back(0).is_none()); + assert!(iter.nth_back(1).is_none()); + + let v = vec![1, 2, 3]; + let ob = (&v).into_pyobject(py).unwrap(); + let list = ob.downcast::().unwrap(); + + let mut iter = list.iter(); + assert!(iter.nth_back(5).is_none()); + + let v = vec![1, 2, 3, 4, 5]; + let ob = (&v).into_pyobject(py).unwrap(); + let list = ob.downcast::().unwrap(); + + let mut iter = list.iter(); + iter.next_back(); // Consume the last element + assert_eq!(iter.nth_back(1).unwrap().extract::().unwrap(), 3); + assert_eq!(iter.next_back().unwrap().extract::().unwrap(), 2); + assert_eq!(iter.nth_back(0).unwrap().extract::().unwrap(), 1); + + let v = vec![1, 2, 3, 4, 5]; + let ob = (&v).into_pyobject(py).unwrap(); + let list = ob.downcast::().unwrap(); + + let mut iter = list.iter(); + assert_eq!(iter.nth_back(1).unwrap().extract::().unwrap(), 4); + assert_eq!(iter.nth_back(2).unwrap().extract::().unwrap(), 1); + + let mut iter2 = list.iter(); + iter2.next_back(); + assert_eq!(iter2.nth_back(1).unwrap().extract::().unwrap(), 3); + assert_eq!(iter2.next_back().unwrap().extract::().unwrap(), 2); + + let mut iter3 = list.iter(); + iter3.nth(1); + assert_eq!(iter3.nth_back(2).unwrap().extract::().unwrap(), 3); + assert!(iter3.nth_back(0).is_none()); + }); + } + + #[cfg(feature = "nightly")] + #[test] + fn test_iter_advance_by() { + Python::with_gil(|py| { + let v = vec![1, 2, 3, 4, 5]; + let ob = (&v).into_pyobject(py).unwrap(); + let list = ob.downcast::().unwrap(); + + let mut iter = list.iter(); + assert_eq!(iter.advance_by(2), Ok(())); + assert_eq!(iter.next().unwrap().extract::().unwrap(), 3); + assert_eq!(iter.advance_by(0), Ok(())); + assert_eq!(iter.advance_by(100), Err(NonZero::new(98).unwrap())); + + let mut iter2 = list.iter(); + assert_eq!(iter2.advance_by(6), Err(NonZero::new(1).unwrap())); + + let mut iter3 = list.iter(); + assert_eq!(iter3.advance_by(5), Ok(())); + + let mut iter4 = list.iter(); + assert_eq!(iter4.advance_by(0), Ok(())); + assert_eq!(iter4.next().unwrap().extract::().unwrap(), 1); + }) + } + + #[cfg(feature = "nightly")] + #[test] + fn test_iter_advance_back_by() { + Python::with_gil(|py| { + let v = vec![1, 2, 3, 4, 5]; + let ob = (&v).into_pyobject(py).unwrap(); + let list = ob.downcast::().unwrap(); + + let mut iter = list.iter(); + assert_eq!(iter.advance_back_by(2), Ok(())); + assert_eq!(iter.next_back().unwrap().extract::().unwrap(), 3); + assert_eq!(iter.advance_back_by(0), Ok(())); + assert_eq!(iter.advance_back_by(100), Err(NonZero::new(98).unwrap())); + + let mut iter2 = list.iter(); + assert_eq!(iter2.advance_back_by(6), Err(NonZero::new(1).unwrap())); + + let mut iter3 = list.iter(); + assert_eq!(iter3.advance_back_by(5), Ok(())); + + let mut iter4 = list.iter(); + assert_eq!(iter4.advance_back_by(0), Ok(())); + assert_eq!(iter4.next_back().unwrap().extract::().unwrap(), 5); + }) + } } From 8ab39d2c1820e67103b18c5e66cddef71049835e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20=C5=A0im=C3=A1=C4=8Dek?= Date: Fri, 7 Feb 2025 12:55:04 +0100 Subject: [PATCH 438/495] Update tested GraalPy version in CI (#4867) * Add GraalPy 24.1 version to CI * Skip failing test on GraalPy --- .github/workflows/build.yml | 4 ++++ .github/workflows/ci.yml | 1 + pytests/tests/test_comparisons.py | 6 ++++++ 3 files changed, 11 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ed24957ad2a..5fb2124836e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -38,6 +38,10 @@ jobs: - name: Install nox run: python -m pip install --upgrade pip && pip install nox + - if: inputs.python-version == 'graalpy24.1' + name: Install GraalPy virtualenv (only GraalPy 24.1) + run: python -m pip install 'git+https://github.com/oracle/graalpython#egg=graalpy_virtualenv_seeder&subdirectory=graalpy_virtualenv_seeder' + - name: Install Rust toolchain uses: dtolnay/rust-toolchain@master with: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 929955c7084..2d7d32e6e67 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -255,6 +255,7 @@ jobs: "pypy3.9", "pypy3.10", "graalpy24.0", + "graalpy24.1", ] platform: [ diff --git a/pytests/tests/test_comparisons.py b/pytests/tests/test_comparisons.py index 50bba81cb1a..fe4d8f31f62 100644 --- a/pytests/tests/test_comparisons.py +++ b/pytests/tests/test_comparisons.py @@ -1,5 +1,6 @@ from typing import Type, Union +import sys import pytest from pyo3_pytests.comparisons import ( Eq, @@ -28,6 +29,11 @@ def __ne__(self, other: Self) -> bool: return NotImplemented +@pytest.mark.skipif( + sys.implementation.name == "graalpy" + and __graalpython__.get_graalvm_version().startswith("24.1"), # noqa: F821 + reason="Bug in GraalPy 24.1", +) @pytest.mark.parametrize( "ty", (Eq, EqDerived, PyEq), ids=("rust", "rust-derived", "python") ) From ded16005789f5aa81ee6fde3dcb6d218a75eecf8 Mon Sep 17 00:00:00 2001 From: Lundy Bernard Date: Mon, 10 Feb 2025 13:43:28 -0800 Subject: [PATCH 439/495] fix typo in class.md (#4901) --- 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 7229330a361..543435a3de9 100644 --- a/guide/src/class.md +++ b/guide/src/class.md @@ -200,7 +200,7 @@ fn my_module(m: &Bound<'_, PyModule>) -> PyResult<()> { ## Bound and interior mutability -Often is useful to turn a `#[pyclass]` type `T` into a Python object and access it from Rust code. The [`Py`] and [`Bound<'py, T>`] smart pointers are the ways to represent a Python object in PyO3's API. More detail can be found about them [in the Python objects](./types.md#pyo3s-smart-pointers) section of the guide. +It is often useful to turn a `#[pyclass]` type `T` into a Python object and access it from Rust code. The [`Py`] and [`Bound<'py, T>`] smart pointers are the ways to represent a Python object in PyO3's API. More detail can be found about them [in the Python objects](./types.md#pyo3s-smart-pointers) section of the guide. Most Python objects do not offer exclusive (`&mut`) access (see the [section on Python's memory model](./python-from-rust.md#pythons-memory-model)). However, Rust structs wrapped as Python objects (called `pyclass` types) often *do* need `&mut` access. Due to the GIL, PyO3 *can* guarantee exclusive access to them. From 2c732a7ab42af4b11c2a9a8da9f838b592712d95 Mon Sep 17 00:00:00 2001 From: Matti Picus Date: Wed, 12 Feb 2025 14:54:24 +1100 Subject: [PATCH 440/495] Add PyPy3.11 (#4760) * allow pypy3.11 * run CI on pypy3.11 * change const declaration * change link name for PyExc_BaseExceptionGroup * PyObject_DelAttr* are inline functions on pypy3.11 * use nightly until official release * DOC: add a news fragment * conditionally compile 'use' statements * fix format * changes from review * pypy 3.11 released * fixes for PyPy * typo * exclude _PyInterpreterFrame on PyPy * more pypy fixes * more excluding _PyFrameEvalFunction on PyPy * fixes for PyPy struct differences * format * fix test --------- Co-authored-by: Icxolu <10486322+Icxolu@users.noreply.github.com> --- .github/workflows/ci.yml | 1 + newsfragments/4760.packaging.md | 1 + noxfile.py | 6 +++--- pyo3-ffi/build.rs | 2 +- pyo3-ffi/src/abstract_.rs | 10 ++++++++-- pyo3-ffi/src/cpython/abstract_.rs | 2 +- pyo3-ffi/src/cpython/genobject.rs | 2 +- pyo3-ffi/src/cpython/mod.rs | 2 +- pyo3-ffi/src/cpython/object.rs | 4 ++-- pyo3-ffi/src/cpython/objimpl.rs | 2 +- pyo3-ffi/src/cpython/pyframe.rs | 2 +- pyo3-ffi/src/cpython/pystate.rs | 6 +++--- pyo3-ffi/src/cpython/unicodeobject.rs | 4 ++-- pyo3-ffi/src/object.rs | 4 ++-- pyo3-ffi/src/pybuffer.rs | 6 +++++- pyo3-ffi/src/pyerrors.rs | 1 + src/ffi/tests.rs | 2 +- 17 files changed, 35 insertions(+), 22 deletions(-) create mode 100644 newsfragments/4760.packaging.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2d7d32e6e67..8d0e09bda0d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -254,6 +254,7 @@ jobs: "3.13", "pypy3.9", "pypy3.10", + "pypy3.11", "graalpy24.0", "graalpy24.1", ] diff --git a/newsfragments/4760.packaging.md b/newsfragments/4760.packaging.md new file mode 100644 index 00000000000..e7fe53099d1 --- /dev/null +++ b/newsfragments/4760.packaging.md @@ -0,0 +1 @@ +add support for PyPy3.11 diff --git a/noxfile.py b/noxfile.py index 9f94175ab16..cadd56a10cb 100644 --- a/noxfile.py +++ b/noxfile.py @@ -42,7 +42,7 @@ PYO3_GUIDE_TARGET = PYO3_TARGET / "guide" PYO3_DOCS_TARGET = PYO3_TARGET / "doc" PY_VERSIONS = ("3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13") -PYPY_VERSIONS = ("3.9", "3.10") +PYPY_VERSIONS = ("3.9", "3.10", "3.11") FREE_THREADED_BUILD = bool(sysconfig.get_config_var("Py_GIL_DISABLED")) @@ -689,8 +689,8 @@ def test_version_limits(session: nox.Session): config_file.set("PyPy", "3.8") _run_cargo(session, "check", env=env, expect_error=True) - assert "3.11" not in PYPY_VERSIONS - config_file.set("PyPy", "3.11") + assert "3.12" not in PYPY_VERSIONS + config_file.set("PyPy", "3.12") _run_cargo(session, "check", env=env, expect_error=True) diff --git a/pyo3-ffi/build.rs b/pyo3-ffi/build.rs index ea023de75fa..096614c7961 100644 --- a/pyo3-ffi/build.rs +++ b/pyo3-ffi/build.rs @@ -25,7 +25,7 @@ const SUPPORTED_VERSIONS_PYPY: SupportedVersions = SupportedVersions { min: PythonVersion { major: 3, minor: 9 }, max: PythonVersion { major: 3, - minor: 10, + minor: 11, }, }; diff --git a/pyo3-ffi/src/abstract_.rs b/pyo3-ffi/src/abstract_.rs index 82eecce05bd..6e0f44ddd6f 100644 --- a/pyo3-ffi/src/abstract_.rs +++ b/pyo3-ffi/src/abstract_.rs @@ -5,13 +5,19 @@ use libc::size_t; use std::os::raw::{c_char, c_int}; #[inline] -#[cfg(all(not(Py_3_13), not(PyPy)))] // CPython exposed as a function in 3.13, in object.h +#[cfg(all( + not(Py_3_13), // CPython exposed as a function in 3.13, in object.h + not(all(PyPy, not(Py_3_11))) // PyPy exposed as a function until PyPy 3.10, macro in 3.11+ +))] pub unsafe fn PyObject_DelAttrString(o: *mut PyObject, attr_name: *const c_char) -> c_int { PyObject_SetAttrString(o, attr_name, std::ptr::null_mut()) } #[inline] -#[cfg(all(not(Py_3_13), not(PyPy)))] // CPython exposed as a function in 3.13, in object.h +#[cfg(all( + not(Py_3_13), // CPython exposed as a function in 3.13, in object.h + not(all(PyPy, not(Py_3_11))) // PyPy exposed as a function until PyPy 3.10, macro in 3.11+ +))] pub unsafe fn PyObject_DelAttr(o: *mut PyObject, attr_name: *mut PyObject) -> c_int { PyObject_SetAttr(o, attr_name, std::ptr::null_mut()) } diff --git a/pyo3-ffi/src/cpython/abstract_.rs b/pyo3-ffi/src/cpython/abstract_.rs index 5c7b16ff0e8..6ada1a754ef 100644 --- a/pyo3-ffi/src/cpython/abstract_.rs +++ b/pyo3-ffi/src/cpython/abstract_.rs @@ -1,5 +1,5 @@ use crate::{PyObject, Py_ssize_t}; -#[cfg(not(all(Py_3_11, GraalPy)))] +#[cfg(any(all(Py_3_8, not(any(PyPy, GraalPy))), not(Py_3_11)))] use std::os::raw::c_char; use std::os::raw::c_int; diff --git a/pyo3-ffi/src/cpython/genobject.rs b/pyo3-ffi/src/cpython/genobject.rs index 4be310a8c88..c9d419e3782 100644 --- a/pyo3-ffi/src/cpython/genobject.rs +++ b/pyo3-ffi/src/cpython/genobject.rs @@ -2,7 +2,7 @@ use crate::object::*; use crate::PyFrameObject; #[cfg(not(any(PyPy, GraalPy)))] use crate::_PyErr_StackItem; -#[cfg(all(Py_3_11, not(GraalPy)))] +#[cfg(all(Py_3_11, not(any(PyPy, GraalPy))))] use std::os::raw::c_char; use std::os::raw::c_int; use std::ptr::addr_of_mut; diff --git a/pyo3-ffi/src/cpython/mod.rs b/pyo3-ffi/src/cpython/mod.rs index fe909f0ceeb..f09d51d0e4e 100644 --- a/pyo3-ffi/src/cpython/mod.rs +++ b/pyo3-ffi/src/cpython/mod.rs @@ -71,7 +71,7 @@ pub use self::object::*; pub use self::objimpl::*; pub use self::pydebug::*; pub use self::pyerrors::*; -#[cfg(Py_3_11)] +#[cfg(all(Py_3_11, not(PyPy)))] pub use self::pyframe::*; #[cfg(all(Py_3_8, not(PyPy)))] pub use self::pylifecycle::*; diff --git a/pyo3-ffi/src/cpython/object.rs b/pyo3-ffi/src/cpython/object.rs index 75eef11aae3..4e6932da789 100644 --- a/pyo3-ffi/src/cpython/object.rs +++ b/pyo3-ffi/src/cpython/object.rs @@ -310,9 +310,9 @@ pub struct PyHeapTypeObject { pub ht_cached_keys: *mut c_void, #[cfg(Py_3_9)] pub ht_module: *mut object::PyObject, - #[cfg(Py_3_11)] + #[cfg(all(Py_3_11, not(PyPy)))] _ht_tpname: *mut c_char, - #[cfg(Py_3_11)] + #[cfg(all(Py_3_11, not(PyPy)))] _spec_cache: _specialization_cache, } diff --git a/pyo3-ffi/src/cpython/objimpl.rs b/pyo3-ffi/src/cpython/objimpl.rs index 98a19abeb81..14f7121a202 100644 --- a/pyo3-ffi/src/cpython/objimpl.rs +++ b/pyo3-ffi/src/cpython/objimpl.rs @@ -1,4 +1,4 @@ -#[cfg(not(all(Py_3_11, GraalPy)))] +#[cfg(not(all(Py_3_11, any(PyPy, GraalPy))))] use libc::size_t; use std::os::raw::c_int; diff --git a/pyo3-ffi/src/cpython/pyframe.rs b/pyo3-ffi/src/cpython/pyframe.rs index d0cfa0a2c6d..5e1e16a7d08 100644 --- a/pyo3-ffi/src/cpython/pyframe.rs +++ b/pyo3-ffi/src/cpython/pyframe.rs @@ -1,2 +1,2 @@ -#[cfg(Py_3_11)] +#[cfg(all(Py_3_11, not(PyPy)))] opaque_struct!(_PyInterpreterFrame); diff --git a/pyo3-ffi/src/cpython/pystate.rs b/pyo3-ffi/src/cpython/pystate.rs index 5481265b55d..650cd6a1f7f 100644 --- a/pyo3-ffi/src/cpython/pystate.rs +++ b/pyo3-ffi/src/cpython/pystate.rs @@ -69,21 +69,21 @@ extern "C" { pub fn PyThreadState_DeleteCurrent(); } -#[cfg(all(Py_3_9, not(Py_3_11)))] +#[cfg(all(Py_3_9, not(any(Py_3_11, PyPy))))] pub type _PyFrameEvalFunction = extern "C" fn( *mut crate::PyThreadState, *mut crate::PyFrameObject, c_int, ) -> *mut crate::object::PyObject; -#[cfg(Py_3_11)] +#[cfg(all(Py_3_11, not(PyPy)))] pub type _PyFrameEvalFunction = extern "C" fn( *mut crate::PyThreadState, *mut crate::_PyInterpreterFrame, c_int, ) -> *mut crate::object::PyObject; -#[cfg(Py_3_9)] +#[cfg(all(Py_3_9, not(PyPy)))] extern "C" { /// Get the frame evaluation function. pub fn _PyInterpreterState_GetEvalFrameFunc( diff --git a/pyo3-ffi/src/cpython/unicodeobject.rs b/pyo3-ffi/src/cpython/unicodeobject.rs index fae626b8d25..3527a5aeadb 100644 --- a/pyo3-ffi/src/cpython/unicodeobject.rs +++ b/pyo3-ffi/src/cpython/unicodeobject.rs @@ -1,4 +1,4 @@ -#[cfg(not(PyPy))] +#[cfg(any(Py_3_11, not(PyPy)))] use crate::Py_hash_t; use crate::{PyObject, Py_UCS1, Py_UCS2, Py_UCS4, Py_ssize_t}; use libc::wchar_t; @@ -251,7 +251,7 @@ impl From for u32 { pub struct PyASCIIObject { pub ob_base: PyObject, pub length: Py_ssize_t, - #[cfg(not(PyPy))] + #[cfg(any(Py_3_11, not(PyPy)))] pub hash: Py_hash_t, /// A bit field with various properties. /// diff --git a/pyo3-ffi/src/object.rs b/pyo3-ffi/src/object.rs index 3f086ac1e92..087cd32920c 100644 --- a/pyo3-ffi/src/object.rs +++ b/pyo3-ffi/src/object.rs @@ -436,7 +436,7 @@ extern "C" { arg2: *const c_char, arg3: *mut PyObject, ) -> c_int; - #[cfg(any(Py_3_13, PyPy))] // CPython defined in 3.12 as an inline function in abstract.h + #[cfg(any(Py_3_13, all(PyPy, not(Py_3_11))))] // CPython defined in 3.12 as an inline function in abstract.h #[cfg_attr(PyPy, link_name = "PyPyObject_DelAttrString")] pub fn PyObject_DelAttrString(arg1: *mut PyObject, arg2: *const c_char) -> c_int; #[cfg_attr(PyPy, link_name = "PyPyObject_HasAttrString")] @@ -460,7 +460,7 @@ extern "C" { #[cfg_attr(PyPy, link_name = "PyPyObject_SetAttr")] pub fn PyObject_SetAttr(arg1: *mut PyObject, arg2: *mut PyObject, arg3: *mut PyObject) -> c_int; - #[cfg(any(Py_3_13, PyPy))] // CPython defined in 3.12 as an inline function in abstract.h + #[cfg(any(Py_3_13, all(PyPy, not(Py_3_11))))] // CPython defined in 3.12 as an inline function in abstract.h #[cfg_attr(PyPy, link_name = "PyPyObject_DelAttr")] pub fn PyObject_DelAttr(arg1: *mut PyObject, arg2: *mut PyObject) -> c_int; #[cfg_attr(PyPy, link_name = "PyPyObject_HasAttr")] diff --git a/pyo3-ffi/src/pybuffer.rs b/pyo3-ffi/src/pybuffer.rs index 50bf4e6109c..de7067599ff 100644 --- a/pyo3-ffi/src/pybuffer.rs +++ b/pyo3-ffi/src/pybuffer.rs @@ -103,7 +103,11 @@ extern "C" { } /// Maximum number of dimensions -pub const PyBUF_MAX_NDIM: c_int = if cfg!(PyPy) { 36 } else { 64 }; +pub const PyBUF_MAX_NDIM: usize = if cfg!(all(PyPy, not(Py_3_11))) { + 36 +} else { + 64 +}; /* Flags for getting buffers */ pub const PyBUF_SIMPLE: c_int = 0; diff --git a/pyo3-ffi/src/pyerrors.rs b/pyo3-ffi/src/pyerrors.rs index 6c9313c4ab0..d341239a07b 100644 --- a/pyo3-ffi/src/pyerrors.rs +++ b/pyo3-ffi/src/pyerrors.rs @@ -116,6 +116,7 @@ extern "C" { #[cfg_attr(PyPy, link_name = "PyPyExc_BaseException")] pub static mut PyExc_BaseException: *mut PyObject; #[cfg(Py_3_11)] + #[cfg_attr(PyPy, link_name = "PyPyExc_BaseExceptionGroup")] pub static mut PyExc_BaseExceptionGroup: *mut PyObject; #[cfg_attr(PyPy, link_name = "PyPyExc_Exception")] pub static mut PyExc_Exception: *mut PyObject; diff --git a/src/ffi/tests.rs b/src/ffi/tests.rs index 3396e409368..b2d9e4d39cd 100644 --- a/src/ffi/tests.rs +++ b/src/ffi/tests.rs @@ -121,7 +121,7 @@ fn ascii_object_bitfield() { let mut o = PyASCIIObject { ob_base, length: 0, - #[cfg(not(PyPy))] + #[cfg(any(Py_3_11, not(PyPy)))] hash: 0, state: 0u32, #[cfg(not(Py_3_12))] From 6028cfc511c1bd0fcc2fe2f010cb89c357cc9caa Mon Sep 17 00:00:00 2001 From: Owen Leung Date: Fri, 14 Feb 2025 02:28:01 +0800 Subject: [PATCH 441/495] Optimize nth and nth_back for BoundTupleIterator (#4897) * Implement nth, nth_back, advance_by and advance_back_by for tuple iterator * Add newsfragment * Fix failing clippy * Fix incorrect advance_back_by logic for tuple and list --- newsfragments/4897.added.md | 1 + pyo3-benches/benches/bench_tuple.rs | 28 ++++ src/types/list.rs | 2 +- src/types/tuple.rs | 211 +++++++++++++++++++++++++++- 4 files changed, 238 insertions(+), 4 deletions(-) create mode 100644 newsfragments/4897.added.md diff --git a/newsfragments/4897.added.md b/newsfragments/4897.added.md new file mode 100644 index 00000000000..cfa23d37673 --- /dev/null +++ b/newsfragments/4897.added.md @@ -0,0 +1 @@ +Optimizes `nth`, `nth_back`, `advance_by` and `advance_back_by` for `BoundTupleIterator` \ No newline at end of file diff --git a/pyo3-benches/benches/bench_tuple.rs b/pyo3-benches/benches/bench_tuple.rs index 72146c80b54..e235567e926 100644 --- a/pyo3-benches/benches/bench_tuple.rs +++ b/pyo3-benches/benches/bench_tuple.rs @@ -132,10 +132,38 @@ fn tuple_into_pyobject(b: &mut Bencher<'_>) { }); } +fn tuple_nth(b: &mut Bencher<'_>) { + Python::with_gil(|py| { + const LEN: usize = 50; + let list = PyTuple::new(py, 0..LEN).unwrap(); + let mut sum = 0; + b.iter(|| { + for i in 0..LEN { + sum += list.iter().nth(i).unwrap().extract::().unwrap(); + } + }); + }); +} + +fn tuple_nth_back(b: &mut Bencher<'_>) { + Python::with_gil(|py| { + const LEN: usize = 50; + let list = PyTuple::new(py, 0..LEN).unwrap(); + let mut sum = 0; + b.iter(|| { + for i in 0..LEN { + sum += list.iter().nth_back(i).unwrap().extract::().unwrap(); + } + }); + }); +} + fn criterion_benchmark(c: &mut Criterion) { c.bench_function("iter_tuple", iter_tuple); c.bench_function("tuple_new", tuple_new); c.bench_function("tuple_get_item", tuple_get_item); + c.bench_function("tuple_nth", tuple_nth); + c.bench_function("tuple_nth_back", tuple_nth_back); #[cfg(not(any(Py_LIMITED_API, PyPy)))] c.bench_function("tuple_get_item_unchecked", tuple_get_item_unchecked); c.bench_function("tuple_get_borrowed_item", tuple_get_borrowed_item); diff --git a/src/types/list.rs b/src/types/list.rs index 51af830d6f5..e16e5009a01 100644 --- a/src/types/list.rs +++ b/src/types/list.rs @@ -920,7 +920,7 @@ impl DoubleEndedIterator for BoundListIterator<'_> { length.0 = max_len - n; Ok(()) } else { - length.0 = max_len; + length.0 = currently_at; let remainder = n - items_left; Err(unsafe { NonZero::new_unchecked(remainder) }) } diff --git a/src/types/tuple.rs b/src/types/tuple.rs index 8147b872af5..1d674f47a79 100644 --- a/src/types/tuple.rs +++ b/src/types/tuple.rs @@ -1,5 +1,3 @@ -use std::iter::FusedIterator; - use crate::ffi::{self, Py_ssize_t}; use crate::ffi_ptr_ext::FfiPtrExt; #[cfg(feature = "experimental-inspect")] @@ -13,6 +11,9 @@ use crate::{ }; #[allow(deprecated)] use crate::{IntoPy, ToPyObject}; +use std::iter::FusedIterator; +#[cfg(feature = "nightly")] +use std::num::NonZero; #[inline] #[track_caller] @@ -375,6 +376,46 @@ impl<'py> Iterator for BoundTupleIterator<'py> { let len = self.len(); (len, Some(len)) } + + #[inline] + #[cfg(not(feature = "nightly"))] + fn nth(&mut self, n: usize) -> Option { + let length = self.length.min(self.tuple.len()); + let target_index = self.index + n; + if target_index < length { + let item = unsafe { + BorrowedTupleIterator::get_item(self.tuple.as_borrowed(), target_index).to_owned() + }; + self.index = target_index + 1; + Some(item) + } else { + None + } + } + + #[inline] + #[cfg(feature = "nightly")] + fn advance_by(&mut self, n: usize) -> Result<(), NonZero> { + let max_len = self.length.min(self.tuple.len()); + let currently_at = self.index; + if currently_at >= max_len { + if n == 0 { + return Ok(()); + } else { + return Err(unsafe { NonZero::new_unchecked(n) }); + } + } + + let items_left = max_len - currently_at; + if n <= items_left { + self.index += n; + Ok(()) + } else { + self.index = max_len; + let remainder = n - items_left; + Err(unsafe { NonZero::new_unchecked(remainder) }) + } + } } impl DoubleEndedIterator for BoundTupleIterator<'_> { @@ -391,6 +432,46 @@ impl DoubleEndedIterator for BoundTupleIterator<'_> { None } } + + #[inline] + #[cfg(not(feature = "nightly"))] + fn nth_back(&mut self, n: usize) -> Option { + let length_size = self.length.min(self.tuple.len()); + if self.index + n < length_size { + let target_index = length_size - n - 1; + let item = unsafe { + BorrowedTupleIterator::get_item(self.tuple.as_borrowed(), target_index).to_owned() + }; + self.length = target_index; + Some(item) + } else { + None + } + } + + #[inline] + #[cfg(feature = "nightly")] + fn advance_back_by(&mut self, n: usize) -> Result<(), NonZero> { + let max_len = self.length.min(self.tuple.len()); + let currently_at = self.index; + if currently_at >= max_len { + if n == 0 { + return Ok(()); + } else { + return Err(unsafe { NonZero::new_unchecked(n) }); + } + } + + let items_left = max_len - currently_at; + if n <= items_left { + self.length = max_len - n; + Ok(()) + } else { + self.length = currently_at; + let remainder = n - items_left; + Err(unsafe { NonZero::new_unchecked(remainder) }) + } + } } impl ExactSizeIterator for BoundTupleIterator<'_> { @@ -979,8 +1060,9 @@ mod tests { use crate::types::{any::PyAnyMethods, tuple::PyTupleMethods, PyList, PyTuple}; use crate::{IntoPyObject, Python}; use std::collections::HashSet; + #[cfg(feature = "nightly")] + use std::num::NonZero; use std::ops::Range; - #[test] fn test_new() { Python::with_gil(|py| { @@ -1523,4 +1605,127 @@ mod tests { } }) } + + #[test] + fn test_bound_tuple_nth() { + Python::with_gil(|py| { + let tuple = PyTuple::new(py, vec![1, 2, 3, 4]).unwrap(); + let mut iter = tuple.iter(); + assert_eq!(iter.nth(1).unwrap().extract::().unwrap(), 2); + assert_eq!(iter.nth(1).unwrap().extract::().unwrap(), 4); + assert!(iter.nth(1).is_none()); + + let tuple = PyTuple::new(py, Vec::::new()).unwrap(); + let mut iter = tuple.iter(); + iter.next(); + assert!(iter.nth(1).is_none()); + + let tuple = PyTuple::new(py, vec![1, 2, 3]).unwrap(); + let mut iter = tuple.iter(); + assert!(iter.nth(10).is_none()); + + let tuple = PyTuple::new(py, vec![6, 7, 8, 9, 10]).unwrap(); + let mut iter = tuple.iter(); + assert_eq!(iter.next().unwrap().extract::().unwrap(), 6); + assert_eq!(iter.nth(2).unwrap().extract::().unwrap(), 9); + assert_eq!(iter.next().unwrap().extract::().unwrap(), 10); + + let mut iter = tuple.iter(); + assert_eq!(iter.nth_back(1).unwrap().extract::().unwrap(), 9); + assert_eq!(iter.nth(2).unwrap().extract::().unwrap(), 8); + assert!(iter.next().is_none()); + }); + } + + #[test] + fn test_bound_tuple_nth_back() { + Python::with_gil(|py| { + let tuple = PyTuple::new(py, vec![1, 2, 3, 4, 5]).unwrap(); + let mut iter = tuple.iter(); + assert_eq!(iter.nth_back(0).unwrap().extract::().unwrap(), 5); + assert_eq!(iter.nth_back(1).unwrap().extract::().unwrap(), 3); + assert!(iter.nth_back(2).is_none()); + + let tuple = PyTuple::new(py, Vec::::new()).unwrap(); + let mut iter = tuple.iter(); + assert!(iter.nth_back(0).is_none()); + assert!(iter.nth_back(1).is_none()); + + let tuple = PyTuple::new(py, vec![1, 2, 3]).unwrap(); + let mut iter = tuple.iter(); + assert!(iter.nth_back(5).is_none()); + + let tuple = PyTuple::new(py, vec![1, 2, 3, 4, 5]).unwrap(); + let mut iter = tuple.iter(); + iter.next_back(); // Consume the last element + assert_eq!(iter.nth_back(1).unwrap().extract::().unwrap(), 3); + assert_eq!(iter.next_back().unwrap().extract::().unwrap(), 2); + assert_eq!(iter.nth_back(0).unwrap().extract::().unwrap(), 1); + + let tuple = PyTuple::new(py, vec![1, 2, 3, 4, 5]).unwrap(); + let mut iter = tuple.iter(); + assert_eq!(iter.nth_back(1).unwrap().extract::().unwrap(), 4); + assert_eq!(iter.nth_back(2).unwrap().extract::().unwrap(), 1); + + let mut iter2 = tuple.iter(); + iter2.next_back(); + assert_eq!(iter2.nth_back(1).unwrap().extract::().unwrap(), 3); + assert_eq!(iter2.next_back().unwrap().extract::().unwrap(), 2); + + let mut iter3 = tuple.iter(); + iter3.nth(1); + assert_eq!(iter3.nth_back(2).unwrap().extract::().unwrap(), 3); + assert!(iter3.nth_back(0).is_none()); + }); + } + + #[cfg(feature = "nightly")] + #[test] + fn test_bound_tuple_advance_by() { + Python::with_gil(|py| { + let tuple = PyTuple::new(py, vec![1, 2, 3, 4, 5]).unwrap(); + let mut iter = tuple.iter(); + + assert_eq!(iter.advance_by(2), Ok(())); + assert_eq!(iter.next().unwrap().extract::().unwrap(), 3); + assert_eq!(iter.advance_by(0), Ok(())); + assert_eq!(iter.advance_by(100), Err(NonZero::new(98).unwrap())); + assert!(iter.next().is_none()); + + let mut iter2 = tuple.iter(); + assert_eq!(iter2.advance_by(6), Err(NonZero::new(1).unwrap())); + + let mut iter3 = tuple.iter(); + assert_eq!(iter3.advance_by(5), Ok(())); + + let mut iter4 = tuple.iter(); + assert_eq!(iter4.advance_by(0), Ok(())); + assert_eq!(iter4.next().unwrap().extract::().unwrap(), 1); + }) + } + + #[cfg(feature = "nightly")] + #[test] + fn test_bound_tuple_advance_back_by() { + Python::with_gil(|py| { + let tuple = PyTuple::new(py, vec![1, 2, 3, 4, 5]).unwrap(); + let mut iter = tuple.iter(); + + assert_eq!(iter.advance_back_by(2), Ok(())); + assert_eq!(iter.next_back().unwrap().extract::().unwrap(), 3); + assert_eq!(iter.advance_back_by(0), Ok(())); + assert_eq!(iter.advance_back_by(100), Err(NonZero::new(98).unwrap())); + assert!(iter.next_back().is_none()); + + let mut iter2 = tuple.iter(); + assert_eq!(iter2.advance_back_by(6), Err(NonZero::new(1).unwrap())); + + let mut iter3 = tuple.iter(); + assert_eq!(iter3.advance_back_by(5), Ok(())); + + let mut iter4 = tuple.iter(); + assert_eq!(iter4.advance_back_by(0), Ok(())); + assert_eq!(iter4.next_back().unwrap().extract::().unwrap(), 5); + }) + } } From 4f1af4d482d24d43dd12ba0ddae0e7ec7250d670 Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Fri, 14 Feb 2025 11:34:35 +0100 Subject: [PATCH 442/495] bump `benchmarks` ci base image (#4912) --- .github/workflows/benches.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/benches.yml b/.github/workflows/benches.yml index 97b882dc858..62c64d9d113 100644 --- a/.github/workflows/benches.yml +++ b/.github/workflows/benches.yml @@ -15,8 +15,7 @@ concurrency: jobs: benchmarks: - # No support for 24.04, see https://github.com/CodSpeedHQ/runner/issues/42 - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 From f3d324583e3763dafd8c7dda5587efa9deac556d Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers <7943856+bschoenmaeckers@users.noreply.github.com> Date: Fri, 14 Feb 2025 18:22:37 +0100 Subject: [PATCH 443/495] Update jiff to v0.2 (#4903) * Update jiff to v0.2 * Update (hopefully) all urls in features.md --- .github/workflows/ci.yml | 2 +- Cargo.toml | 2 +- guide/src/features.md | 22 +++++++++++----------- noxfile.py | 12 ++++++------ src/conversions/jiff.rs | 20 +++++++++++--------- src/types/datetime.rs | 4 ++-- src/types/mod.rs | 2 +- 7 files changed, 33 insertions(+), 31 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8d0e09bda0d..607b8484c6c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -460,7 +460,7 @@ jobs: - uses: dtolnay/rust-toolchain@nightly with: components: rust-src - - run: cargo rustdoc --lib --no-default-features --features full,jiff-01 -Zunstable-options --config "build.rustdocflags=[\"--cfg\", \"docsrs\"]" + - run: cargo rustdoc --lib --no-default-features --features full,jiff-02 -Zunstable-options --config "build.rustdocflags=[\"--cfg\", \"docsrs\"]" coverage: if: ${{ github.event_name != 'merge_group' }} diff --git a/Cargo.toml b/Cargo.toml index eba0b490d40..1ab7dfedf76 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,7 +39,7 @@ either = { version = "1.9", optional = true } eyre = { version = ">= 0.6.8, < 0.7", optional = true } hashbrown = { version = ">= 0.14.5, < 0.16", optional = true } indexmap = { version = ">= 2.5.0, < 3", optional = true } -jiff-01 = { package = "jiff", version = "0.1.18", optional = true } +jiff-02 = { package = "jiff", version = "0.2", optional = true } num-bigint = { version = "0.4.2", optional = true } num-complex = { version = ">= 0.4.6, < 0.5", optional = true } num-rational = {version = "0.4.1", optional = true } diff --git a/guide/src/features.md b/guide/src/features.md index 5b470c891b4..b48c138b287 100644 --- a/guide/src/features.md +++ b/guide/src/features.md @@ -151,17 +151,17 @@ Adds a dependency on [hashbrown](https://docs.rs/hashbrown) and enables conversi Adds a dependency on [indexmap](https://docs.rs/indexmap) and enables conversions into its [`IndexMap`](https://docs.rs/indexmap/latest/indexmap/map/struct.IndexMap.html) type. -### `jiff-01` - -Adds a dependency on [jiff@0.1](https://docs.rs/jiff/0.1) and requires MSRV 1.70. Enables a conversion from [jiff](https://docs.rs/jiff)'s types to python: -- [SignedDuration](https://docs.rs/jiff/0.1/jiff/struct.SignedDuration.html) -> [`PyDelta`]({{#PYO3_DOCS_URL}}/pyo3/types/struct.PyDelta.html) -- [TimeZone](https://docs.rs/jiff/0.1/jiff/tz/struct.TimeZone.html) -> [`PyTzInfo`]({{#PYO3_DOCS_URL}}/pyo3/types/struct.PyTzInfo.html) -- [Offset](https://docs.rs/jiff/0.1/jiff/tz/struct.Offset.html) -> [`PyTzInfo`]({{#PYO3_DOCS_URL}}/pyo3/types/struct.PyTzInfo.html) -- [Date](https://docs.rs/jiff/0.1/jiff/civil/struct.Date.html) -> [`PyDate`]({{#PYO3_DOCS_URL}}/pyo3/types/struct.PyDate.html) -- [Time](https://docs.rs/jiff/0.1/jiff/civil/struct.Time.html) -> [`PyTime`]({{#PYO3_DOCS_URL}}/pyo3/types/struct.PyTime.html) -- [DateTime](https://docs.rs/jiff/0.1/jiff/civil/struct.DateTime.html) -> [`PyDateTime`]({{#PYO3_DOCS_URL}}/pyo3/types/struct.PyDateTime.html) -- [Zoned](https://docs.rs/jiff/0.1/jiff/struct.Zoned.html) -> [`PyDateTime`]({{#PYO3_DOCS_URL}}/pyo3/types/struct.PyDateTime.html) -- [Timestamp](https://docs.rs/jiff/0.1/jiff/struct.Timestamp.html) -> [`PyDateTime`]({{#PYO3_DOCS_URL}}/pyo3/types/struct.PyDateTime.html) +### `jiff-02` + +Adds a dependency on [jiff@0.2](https://docs.rs/jiff/0.2) and requires MSRV 1.70. Enables a conversion from [jiff](https://docs.rs/jiff)'s types to python: +- [SignedDuration](https://docs.rs/jiff/0.2/jiff/struct.SignedDuration.html) -> [`PyDelta`]({{#PYO3_DOCS_URL}}/pyo3/types/struct.PyDelta.html) +- [TimeZone](https://docs.rs/jiff/0.2/jiff/tz/struct.TimeZone.html) -> [`PyTzInfo`]({{#PYO3_DOCS_URL}}/pyo3/types/struct.PyTzInfo.html) +- [Offset](https://docs.rs/jiff/0.2/jiff/tz/struct.Offset.html) -> [`PyTzInfo`]({{#PYO3_DOCS_URL}}/pyo3/types/struct.PyTzInfo.html) +- [Date](https://docs.rs/jiff/0.2/jiff/civil/struct.Date.html) -> [`PyDate`]({{#PYO3_DOCS_URL}}/pyo3/types/struct.PyDate.html) +- [Time](https://docs.rs/jiff/0.2/jiff/civil/struct.Time.html) -> [`PyTime`]({{#PYO3_DOCS_URL}}/pyo3/types/struct.PyTime.html) +- [DateTime](https://docs.rs/jiff/0.2/jiff/civil/struct.DateTime.html) -> [`PyDateTime`]({{#PYO3_DOCS_URL}}/pyo3/types/struct.PyDateTime.html) +- [Zoned](https://docs.rs/jiff/0.2/jiff/struct.Zoned.html) -> [`PyDateTime`]({{#PYO3_DOCS_URL}}/pyo3/types/struct.PyDateTime.html) +- [Timestamp](https://docs.rs/jiff/0.2/jiff/struct.Timestamp.html) -> [`PyDateTime`]({{#PYO3_DOCS_URL}}/pyo3/types/struct.PyDateTime.html) ### `num-bigint` diff --git a/noxfile.py b/noxfile.py index cadd56a10cb..a233f778a59 100644 --- a/noxfile.py +++ b/noxfile.py @@ -65,9 +65,9 @@ def test_rust(session: nox.Session): if not FREE_THREADED_BUILD: _run_cargo_test(session, features="abi3") if "skip-full" not in session.posargs: - _run_cargo_test(session, features="full jiff-01") + _run_cargo_test(session, features="full jiff-02") if not FREE_THREADED_BUILD: - _run_cargo_test(session, features="abi3 full jiff-01") + _run_cargo_test(session, features="abi3 full jiff-02") @nox.session(name="test-py", venv_backend="none") @@ -395,7 +395,7 @@ def docs(session: nox.Session) -> None: if get_rust_version()[:2] >= (1, 70): # jiff needs MSRC 1.70+ - features += ",jiff-01" + features += ",jiff-02" shutil.rmtree(PYO3_DOCS_TARGET, ignore_errors=True) _run_cargo( @@ -777,8 +777,8 @@ def update_ui_tests(session: nox.Session): env["TRYBUILD"] = "overwrite" command = ["test", "--test", "test_compile_error"] _run_cargo(session, *command, env=env) - _run_cargo(session, *command, "--features=full,jiff-01", env=env) - _run_cargo(session, *command, "--features=abi3,full,jiff-01", env=env) + _run_cargo(session, *command, "--features=full,jiff-02", env=env) + _run_cargo(session, *command, "--features=abi3,full,jiff-02", env=env) def _build_docs_for_ffi_check(session: nox.Session) -> None: @@ -831,7 +831,7 @@ def _get_feature_sets() -> Generator[Tuple[str, ...], None, None]: if get_rust_version()[:2] >= (1, 70): # jiff needs MSRC 1.70+ - features += ",jiff-01" + features += ",jiff-02" yield (f"--features={features}",) yield (f"--features=abi3,{features}",) diff --git a/src/conversions/jiff.rs b/src/conversions/jiff.rs index be1a68bea0e..23ffddf99eb 100644 --- a/src/conversions/jiff.rs +++ b/src/conversions/jiff.rs @@ -1,4 +1,4 @@ -#![cfg(feature = "jiff-01")] +#![cfg(feature = "jiff-02")] //! Conversions to and from [jiff](https://docs.rs/jiff/)’s `Span`, `SignedDuration`, `TimeZone`, //! `Offset`, `Date`, `Time`, `DateTime`, `Zoned`, and `Timestamp`. @@ -9,8 +9,8 @@ //! //! ```toml //! [dependencies] -//! jiff = "0.1" -#![doc = concat!("pyo3 = { version = \"", env!("CARGO_PKG_VERSION"), "\", features = [\"jiff-01\"] }")] +//! jiff = "0.2" +#![doc = concat!("pyo3 = { version = \"", env!("CARGO_PKG_VERSION"), "\", features = [\"jiff-02\"] }")] //! ``` //! //! Note that you must use compatible versions of jiff and PyO3. @@ -21,7 +21,7 @@ //! //! ```rust //! # #![cfg_attr(windows, allow(unused_imports))] -//! # use jiff_01 as jiff; +//! # use jiff_02 as jiff; //! use jiff::{Zoned, SignedDuration, ToSpan}; //! use pyo3::{Python, PyResult, IntoPyObject, types::PyAnyMethods}; //! @@ -65,8 +65,8 @@ use crate::{intern, Bound, FromPyObject, IntoPyObject, Py, PyAny, PyErr, PyResul use jiff::civil::{Date, DateTime, Time}; use jiff::tz::{Offset, TimeZone}; use jiff::{SignedDuration, Span, Timestamp, Zoned}; -#[cfg(feature = "jiff-01")] -use jiff_01 as jiff; +#[cfg(feature = "jiff-02")] +use jiff_02 as jiff; #[cfg(not(Py_LIMITED_API))] fn datetime_to_pydatetime<'py>( @@ -1120,6 +1120,7 @@ mod tests { use super::*; use crate::types::IntoPyDict; use jiff::tz::TimeZoneTransition; + use jiff::SpanRelativeTo; use proptest::prelude::*; use std::ffi::CString; @@ -1191,10 +1192,11 @@ mod tests { // python values of durations (from -999999999 to 999999999 days), Python::with_gil(|py| { if let Ok(span) = Span::new().try_days(days) { - let date = Date::new(2025, 1, 1).unwrap(); - let py_delta = span.to_jiff_duration(date).unwrap().into_pyobject(py).unwrap(); + let relative_to = SpanRelativeTo::days_are_24_hours(); + let jiff_duration = span.to_duration(relative_to).unwrap(); + let py_delta = jiff_duration.into_pyobject(py).unwrap(); let roundtripped: Span = py_delta.extract().expect("Round trip"); - assert_eq!(span.compare(roundtripped).unwrap(), Ordering::Equal); + assert_eq!(span.compare((roundtripped, relative_to)).unwrap(), Ordering::Equal); } }) } diff --git a/src/types/datetime.rs b/src/types/datetime.rs index 5a230820bf7..e091ace2591 100644 --- a/src/types/datetime.rs +++ b/src/types/datetime.rs @@ -27,7 +27,7 @@ use crate::types::any::PyAnyMethods; use crate::types::PyTuple; use crate::{Bound, IntoPyObject, PyAny, PyErr, Python}; use std::os::raw::c_int; -#[cfg(any(feature = "chrono", feature = "jiff-01"))] +#[cfg(any(feature = "chrono", feature = "jiff-02"))] use std::ptr; fn ensure_datetime_api(py: Python<'_>) -> PyResult<&'static PyDateTime_CAPI> { @@ -698,7 +698,7 @@ pub fn timezone_utc_bound(py: Python<'_>) -> Bound<'_, PyTzInfo> { /// Equivalent to `datetime.timezone` constructor /// /// Only used internally -#[cfg(any(feature = "chrono", feature = "jiff-01"))] +#[cfg(any(feature = "chrono", feature = "jiff-02"))] pub(crate) fn timezone_from_offset<'py>( offset: &Bound<'py, PyDelta>, ) -> PyResult> { diff --git a/src/types/mod.rs b/src/types/mod.rs index 943a83771f0..8304afedf5e 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -239,7 +239,7 @@ mod code; pub(crate) mod complex; #[cfg(not(Py_LIMITED_API))] pub(crate) mod datetime; -#[cfg(all(Py_LIMITED_API, any(feature = "chrono", feature = "jiff-01")))] +#[cfg(all(Py_LIMITED_API, any(feature = "chrono", feature = "jiff-02")))] pub(crate) mod datetime_abi3; pub(crate) mod dict; mod ellipsis; From 5be233c027208f2c91d929274b5c8322ff6669f1 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Mon, 17 Feb 2025 14:16:45 -0700 Subject: [PATCH 444/495] restore skipped asserts on free-threaded build (#4918) --- tests/test_gc.rs | 26 ++------------------------ 1 file changed, 2 insertions(+), 24 deletions(-) diff --git a/tests/test_gc.rs b/tests/test_gc.rs index 4b293449b36..28d599e6769 100644 --- a/tests/test_gc.rs +++ b/tests/test_gc.rs @@ -632,18 +632,7 @@ fn test_traverse_subclass() { check.assert_not_dropped(); } - #[cfg(not(Py_GIL_DISABLED))] - { - // FIXME: seems like a bug that this is flaky on the free-threaded build - // https://github.com/PyO3/pyo3/issues/4627 - check.assert_drops_with_gc(ptr); - } - - #[cfg(Py_GIL_DISABLED)] - { - // silence unused ptr warning - let _ = ptr; - } + check.assert_drops_with_gc(ptr); }); } @@ -690,18 +679,7 @@ fn test_traverse_subclass_override_clear() { check.assert_not_dropped(); } - #[cfg(not(Py_GIL_DISABLED))] - { - // FIXME: seems like a bug that this is flaky on the free-threaded build - // https://github.com/PyO3/pyo3/issues/4627 - check.assert_drops_with_gc(ptr); - } - - #[cfg(Py_GIL_DISABLED)] - { - // silence unused ptr warning - let _ = ptr; - } + check.assert_drops_with_gc(ptr); }); } From 7e52b6e419cef5531ec3dcd1ad3d7c7134f02ad8 Mon Sep 17 00:00:00 2001 From: Nicolas Avrutin Date: Tue, 18 Feb 2025 15:30:29 -0500 Subject: [PATCH 445/495] Format python traceback in impl Debug for PyErr. (#4900) Fixes #4863. --- newsfragments/4900.changed.md | 1 + src/err/mod.rs | 35 +++++++++---- tests/test_pyerr_debug_unformattable.rs | 67 +++++++++++++++++++++++++ 3 files changed, 94 insertions(+), 9 deletions(-) create mode 100644 newsfragments/4900.changed.md create mode 100644 tests/test_pyerr_debug_unformattable.rs diff --git a/newsfragments/4900.changed.md b/newsfragments/4900.changed.md new file mode 100644 index 00000000000..89bab779af1 --- /dev/null +++ b/newsfragments/4900.changed.md @@ -0,0 +1 @@ +Format python traceback in impl Debug for PyErr. diff --git a/src/err/mod.rs b/src/err/mod.rs index 20108f6e8dc..bd026bab14d 100644 --- a/src/err/mod.rs +++ b/src/err/mod.rs @@ -2,7 +2,10 @@ use crate::instance::Bound; use crate::panic::PanicException; use crate::type_object::PyTypeInfo; use crate::types::any::PyAnyMethods; -use crate::types::{string::PyStringMethods, typeobject::PyTypeMethods, PyTraceback, PyType}; +use crate::types::{ + string::PyStringMethods, traceback::PyTracebackMethods, typeobject::PyTypeMethods, PyTraceback, + PyType, +}; use crate::{ exceptions::{self, PyBaseException}, ffi, @@ -770,7 +773,20 @@ impl std::fmt::Debug for PyErr { f.debug_struct("PyErr") .field("type", &self.get_type(py)) .field("value", self.value(py)) - .field("traceback", &self.traceback(py)) + .field( + "traceback", + &self.traceback(py).map(|tb| match tb.format() { + Ok(s) => s, + Err(err) => { + err.write_unraisable(py, Some(&tb)); + // It would be nice to format what we can of the + // error, but we can't guarantee that the error + // won't have another unformattable traceback inside + // it and we want to avoid an infinite recursion. + format!("", tb) + } + }), + ) .finish() }) } @@ -1058,7 +1074,7 @@ mod tests { // PyErr { // type: , // value: Exception('banana'), - // traceback: Some(\\\", line 1, in \\n\") // } Python::with_gil(|py| { @@ -1070,15 +1086,16 @@ mod tests { assert!(debug_str.starts_with("PyErr { ")); assert!(debug_str.ends_with(" }")); - // strip "PyErr { " and " }" - let mut fields = debug_str["PyErr { ".len()..debug_str.len() - 2].split(", "); + // Strip "PyErr { " and " }". Split into 3 substrings to separate type, + // value, and traceback while not splitting the string within traceback. + let mut fields = debug_str["PyErr { ".len()..debug_str.len() - 2].splitn(3, ", "); assert_eq!(fields.next().unwrap(), "type: "); assert_eq!(fields.next().unwrap(), "value: Exception('banana')"); - - let traceback = fields.next().unwrap(); - assert!(traceback.starts_with("traceback: Some()")); + assert_eq!( + fields.next().unwrap(), + "traceback: Some(\"Traceback (most recent call last):\\n File \\\"\\\", line 1, in \\n\")" + ); assert!(fields.next().is_none()); }); diff --git a/tests/test_pyerr_debug_unformattable.rs b/tests/test_pyerr_debug_unformattable.rs new file mode 100644 index 00000000000..615868207b4 --- /dev/null +++ b/tests/test_pyerr_debug_unformattable.rs @@ -0,0 +1,67 @@ +use pyo3::ffi; +use pyo3::prelude::*; + +// This test mucks around with sys.modules, so run it separately to prevent it +// from potentially corrupting the state of the python interpreter used in other +// tests. + +#[test] +fn err_debug_unformattable() { + // Debug representation should be like the following (without the newlines): + // PyErr { + // type: , + // value: Exception('banana'), + // traceback: Some(\">\") + // } + + Python::with_gil(|py| { + // PyTracebackMethods::format uses io.StringIO. Mock it out to trigger a + // formatting failure: + // TypeError: 'Mock' object cannot be converted to 'PyString' + let err = py + .run( + ffi::c_str!( + r#" +import io, sys, unittest.mock +sys.modules['orig_io'] = sys.modules['io'] +sys.modules['io'] = unittest.mock.Mock() +raise Exception('banana')"# + ), + None, + None, + ) + .expect_err("raising should have given us an error"); + + let debug_str = format!("{:?}", err); + assert!(debug_str.starts_with("PyErr { ")); + assert!(debug_str.ends_with(" }")); + + // Strip "PyErr { " and " }". Split into 3 substrings to separate type, + // value, and traceback while not splitting the string within traceback. + let mut fields = debug_str["PyErr { ".len()..debug_str.len() - 2].splitn(3, ", "); + + assert_eq!(fields.next().unwrap(), "type: "); + assert_eq!(fields.next().unwrap(), "value: Exception('banana')"); + + let traceback = fields.next().unwrap(); + assert!( + traceback.starts_with("traceback: Some(\" Date: Wed, 19 Feb 2025 18:34:55 +0800 Subject: [PATCH 446/495] Add rnet library to examples (#4920) --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 54e027571a2..02f1d56de6d 100644 --- a/README.md +++ b/README.md @@ -226,6 +226,7 @@ about this topic. - [river](https://github.com/online-ml/river) _Online machine learning in python, the computationally heavy statistics algorithms are implemented in Rust._ - [robyn](https://github.com/sparckles/Robyn) A Super Fast Async Python Web Framework with a Rust runtime. - [rust-python-coverage](https://github.com/cjermain/rust-python-coverage) _Example PyO3 project with automated test coverage for Rust and Python._ +- [rnet](https://github.com/0x676e67/rnet) Asynchronous Python HTTP Client with Black Magic - [sail](https://github.com/lakehq/sail) _Unifying stream, batch, and AI workloads with Apache Spark compatibility._ - [tiktoken](https://github.com/openai/tiktoken) _A fast BPE tokeniser for use with OpenAI's models._ - [tokenizers](https://github.com/huggingface/tokenizers/tree/main/bindings/python) _Python bindings to the Hugging Face tokenizers (NLP) written in Rust._ From 03bb5b49b988d5613b0fe7f838e8fa298f0dbd9a Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Wed, 19 Feb 2025 05:49:35 -0700 Subject: [PATCH 447/495] fix multithreaded access to freelist pyclasses (#4902) * make FreeList explicitly a wrapper for *mut PyObject * fix multithreaded access to freelist pyclasses * add changelog entry * use a GILOnceCell to initialize the freelist * respond to code review * skip test on wasm --------- Co-authored-by: David Hewitt --- newsfragments/4902.fixed.md | 1 + pyo3-macros-backend/src/pyclass.rs | 16 +++++------- src/impl_/freelist.rs | 42 +++++++++++++++++------------- src/impl_/pyclass.rs | 15 ++++++++--- tests/test_gc.rs | 29 +++++++++++++++++++++ 5 files changed, 72 insertions(+), 31 deletions(-) create mode 100644 newsfragments/4902.fixed.md diff --git a/newsfragments/4902.fixed.md b/newsfragments/4902.fixed.md new file mode 100644 index 00000000000..e377ab018d7 --- /dev/null +++ b/newsfragments/4902.fixed.md @@ -0,0 +1 @@ +* Fixed thread-unsafe implementation of freelist pyclasses on the free-threaded build. \ No newline at end of file diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index d9fb6652017..de11b0604ad 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -2380,15 +2380,13 @@ impl<'a> PyClassImplsBuilder<'a> { quote! { impl #pyo3_path::impl_::pyclass::PyClassWithFreeList for #cls { #[inline] - fn get_free_list(py: #pyo3_path::Python<'_>) -> &mut #pyo3_path::impl_::freelist::FreeList<*mut #pyo3_path::ffi::PyObject> { - static mut FREELIST: *mut #pyo3_path::impl_::freelist::FreeList<*mut #pyo3_path::ffi::PyObject> = 0 as *mut _; - unsafe { - if FREELIST.is_null() { - FREELIST = ::std::boxed::Box::into_raw(::std::boxed::Box::new( - #pyo3_path::impl_::freelist::FreeList::with_capacity(#freelist))); - } - &mut *FREELIST - } + fn get_free_list(py: #pyo3_path::Python<'_>) -> &'static ::std::sync::Mutex<#pyo3_path::impl_::freelist::PyObjectFreeList> { + static FREELIST: #pyo3_path::sync::GILOnceCell<::std::sync::Mutex<#pyo3_path::impl_::freelist::PyObjectFreeList>> = #pyo3_path::sync::GILOnceCell::new(); + // If there's a race to fill the cell, the object created + // by the losing thread will be deallocated via RAII + &FREELIST.get_or_init(py, || { + ::std::sync::Mutex::new(#pyo3_path::impl_::freelist::PyObjectFreeList::with_capacity(#freelist)) + }) } } } diff --git a/src/impl_/freelist.rs b/src/impl_/freelist.rs index 0e611d421d6..713bb3f6464 100644 --- a/src/impl_/freelist.rs +++ b/src/impl_/freelist.rs @@ -8,31 +8,37 @@ //! //! [1]: https://en.wikipedia.org/wiki/Free_list +use crate::ffi; use std::mem; -/// Represents a slot of a [`FreeList`]. -pub enum Slot { +/// Represents a slot of a [`PyObjectFreeList`]. +enum PyObjectSlot { /// A free slot. Empty, /// An allocated slot. - Filled(T), + Filled(*mut ffi::PyObject), } -/// A free allocation list. +// safety: access is guarded by a per-pyclass mutex +unsafe impl Send for PyObjectSlot {} + +/// A free allocation list for PyObject ffi pointers. /// /// See [the parent module](crate::impl_::freelist) for more details. -pub struct FreeList { - entries: Vec>, +pub struct PyObjectFreeList { + entries: Box<[PyObjectSlot]>, split: usize, capacity: usize, } -impl FreeList { - /// Creates a new `FreeList` instance with specified capacity. - pub fn with_capacity(capacity: usize) -> FreeList { - let entries = (0..capacity).map(|_| Slot::Empty).collect::>(); +impl PyObjectFreeList { + /// Creates a new `PyObjectFreeList` instance with specified capacity. + pub fn with_capacity(capacity: usize) -> PyObjectFreeList { + let entries = (0..capacity) + .map(|_| PyObjectSlot::Empty) + .collect::>(); - FreeList { + PyObjectFreeList { entries, split: 0, capacity, @@ -40,26 +46,26 @@ impl FreeList { } /// Pops the first non empty item. - pub fn pop(&mut self) -> Option { + pub fn pop(&mut self) -> Option<*mut ffi::PyObject> { let idx = self.split; if idx == 0 { None } else { - match mem::replace(&mut self.entries[idx - 1], Slot::Empty) { - Slot::Filled(v) => { + match mem::replace(&mut self.entries[idx - 1], PyObjectSlot::Empty) { + PyObjectSlot::Filled(v) => { self.split = idx - 1; Some(v) } - _ => panic!("FreeList is corrupt"), + _ => panic!("PyObjectFreeList is corrupt"), } } } - /// Inserts a value into the list. Returns `Some(val)` if the `FreeList` is full. - pub fn insert(&mut self, val: T) -> Option { + /// Inserts a value into the list. Returns `Some(val)` if the `PyObjectFreeList` is full. + pub fn insert(&mut self, val: *mut ffi::PyObject) -> Option<*mut ffi::PyObject> { let next = self.split + 1; if next < self.capacity { - self.entries[self.split] = Slot::Filled(val); + self.entries[self.split] = PyObjectSlot::Filled(val); self.split = next; None } else { diff --git a/src/impl_/pyclass.rs b/src/impl_/pyclass.rs index 59bb2b4bd5a..06ec83d6ff2 100644 --- a/src/impl_/pyclass.rs +++ b/src/impl_/pyclass.rs @@ -2,7 +2,7 @@ use crate::{ exceptions::{PyAttributeError, PyNotImplementedError, PyRuntimeError, PyValueError}, ffi, impl_::{ - freelist::FreeList, + freelist::PyObjectFreeList, pycell::{GetBorrowChecker, PyClassMutability, PyClassObjectLayout}, pyclass_init::PyObjectInit, pymethods::{PyGetterDef, PyMethodDefType}, @@ -20,6 +20,7 @@ use std::{ marker::PhantomData, os::raw::{c_int, c_void}, ptr::NonNull, + sync::Mutex, thread, }; @@ -912,7 +913,7 @@ use super::{pycell::PyClassObject, pymethods::BoundRef}; /// Do not implement this trait manually. Instead, use `#[pyclass(freelist = N)]` /// on a Rust struct to implement it. pub trait PyClassWithFreeList: PyClass { - fn get_free_list(py: Python<'_>) -> &mut FreeList<*mut ffi::PyObject>; + fn get_free_list(py: Python<'_>) -> &'static Mutex; } /// Implementation of tp_alloc for `freelist` classes. @@ -933,7 +934,9 @@ pub unsafe extern "C" fn alloc_with_freelist( // If this type is a variable type or the subtype is not equal to this type, we cannot use the // freelist if nitems == 0 && subtype == self_type { - if let Some(obj) = T::get_free_list(py).pop() { + let mut free_list = T::get_free_list(py).lock().unwrap(); + if let Some(obj) = free_list.pop() { + drop(free_list); ffi::PyObject_Init(obj, subtype); return obj as _; } @@ -953,7 +956,11 @@ pub unsafe extern "C" fn free_with_freelist(obj: *mut c_ T::type_object_raw(Python::assume_gil_acquired()), ffi::Py_TYPE(obj) ); - if let Some(obj) = T::get_free_list(Python::assume_gil_acquired()).insert(obj) { + let mut free_list = T::get_free_list(Python::assume_gil_acquired()) + .lock() + .unwrap(); + if let Some(obj) = free_list.insert(obj) { + drop(free_list); let ty = ffi::Py_TYPE(obj); // Deduce appropriate inverse of PyType_GenericAlloc diff --git a/tests/test_gc.rs b/tests/test_gc.rs index 28d599e6769..eb2cb34e1d7 100644 --- a/tests/test_gc.rs +++ b/tests/test_gc.rs @@ -36,6 +36,35 @@ fn class_with_freelist() { }); } +#[pyclass(freelist = 2)] +#[cfg(not(target_arch = "wasm32"))] +struct ClassWithFreelistAndData { + data: Option, +} + +#[cfg(not(target_arch = "wasm32"))] +fn spin_freelist(py: Python<'_>, data: usize) { + for _ in 0..500 { + let inst1 = Py::new(py, ClassWithFreelistAndData { data: Some(data) }).unwrap(); + let inst2 = Py::new(py, ClassWithFreelistAndData { data: Some(data) }).unwrap(); + assert_eq!(inst1.borrow(py).data, Some(data)); + assert_eq!(inst2.borrow(py).data, Some(data)); + } +} + +#[test] +#[cfg(not(target_arch = "wasm32"))] +fn multithreaded_class_with_freelist() { + std::thread::scope(|s| { + s.spawn(|| { + Python::with_gil(|py| spin_freelist(py, 12)); + }); + s.spawn(|| { + Python::with_gil(|py| spin_freelist(py, 0x4d3d3d3)); + }); + }); +} + /// Helper function to create a pair of objects that can be used to test drops; /// the first object is a guard that records when it has been dropped, the second /// object is a check that can be used to assert that the guard has been dropped. From f2f30f3f3ccacd11ed5b5ae99f682dc2a4cc09de Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Wed, 19 Feb 2025 09:24:37 -0700 Subject: [PATCH 448/495] Use a critical section to serialize adding builtins to globals (#4921) * add test that panics because __builtins__ isn't available * use a critical section to serialize adding __builtins__ to __globals__ * add release note * use safe APIs instead of PyDict_Contains --- newsfragments/4921.fixed.md | 1 + src/marker.rs | 93 +++++++++++++++++++++++++++---------- 2 files changed, 70 insertions(+), 24 deletions(-) create mode 100644 newsfragments/4921.fixed.md diff --git a/newsfragments/4921.fixed.md b/newsfragments/4921.fixed.md new file mode 100644 index 00000000000..86a91fd727a --- /dev/null +++ b/newsfragments/4921.fixed.md @@ -0,0 +1 @@ +* Reenabled a workaround for situations where CPython incorrectly does not add `__builtins__` to `__globals__` in code executed by `Python::py_run`. \ No newline at end of file diff --git a/src/marker.rs b/src/marker.rs index 5962b47b60b..f98a725da0e 100644 --- a/src/marker.rs +++ b/src/marker.rs @@ -117,7 +117,6 @@ //! [`Rc`]: std::rc::Rc //! [`Py`]: crate::Py use crate::conversion::IntoPyObject; -#[cfg(any(doc, not(Py_3_10)))] use crate::err::PyErr; use crate::err::{self, PyResult}; use crate::ffi_ptr_ext::FfiPtrExt; @@ -649,30 +648,34 @@ impl<'py> Python<'py> { }; let locals = locals.unwrap_or(globals); - #[cfg(not(Py_3_10))] - { - // If `globals` don't provide `__builtins__`, most of the code will fail if Python - // version is <3.10. That's probably not what user intended, so insert `__builtins__` - // for them. - // - // See also: - // - https://github.com/python/cpython/pull/24564 (the same fix in CPython 3.10) - // - https://github.com/PyO3/pyo3/issues/3370 - let builtins_s = crate::intern!(self, "__builtins__").as_ptr(); - let has_builtins = unsafe { ffi::PyDict_Contains(globals.as_ptr(), builtins_s) }; - if has_builtins == -1 { - return Err(PyErr::fetch(self)); - } - if has_builtins == 0 { - // Inherit current builtins. - let builtins = unsafe { ffi::PyEval_GetBuiltins() }; - - // `PyDict_SetItem` doesn't take ownership of `builtins`, but `PyEval_GetBuiltins` - // seems to return a borrowed reference, so no leak here. - if unsafe { ffi::PyDict_SetItem(globals.as_ptr(), builtins_s, builtins) } == -1 { - return Err(PyErr::fetch(self)); + // If `globals` don't provide `__builtins__`, most of the code will fail if Python + // version is <3.10. That's probably not what user intended, so insert `__builtins__` + // for them. + // + // See also: + // - https://github.com/python/cpython/pull/24564 (the same fix in CPython 3.10) + // - https://github.com/PyO3/pyo3/issues/3370 + let builtins_s = crate::intern!(self, "__builtins__"); + let has_builtins = globals.contains(builtins_s)?; + if !has_builtins { + crate::sync::with_critical_section(globals, || { + // check if another thread set __builtins__ while this thread was blocked on the critical section + let has_builtins = globals.contains(builtins_s)?; + if !has_builtins { + // Inherit current builtins. + let builtins = unsafe { ffi::PyEval_GetBuiltins() }; + + // `PyDict_SetItem` doesn't take ownership of `builtins`, but `PyEval_GetBuiltins` + // seems to return a borrowed reference, so no leak here. + if unsafe { + ffi::PyDict_SetItem(globals.as_ptr(), builtins_s.as_ptr(), builtins) + } == -1 + { + return Err(PyErr::fetch(self)); + } } - } + Ok(()) + })?; } let code_obj = unsafe { @@ -1018,4 +1021,46 @@ mod tests { assert!(matches!(namespace.get_item("__builtins__"), Ok(Some(..)))); }) } + + #[cfg(feature = "macros")] + #[test] + fn test_py_run_inserts_globals_2() { + #[crate::pyclass(crate = "crate")] + #[derive(Clone)] + struct CodeRunner { + code: CString, + } + + impl CodeRunner { + fn reproducer(&mut self, py: Python<'_>) -> PyResult<()> { + let variables = PyDict::new(py); + variables.set_item("cls", Py::new(py, self.clone())?)?; + + py.run(self.code.as_c_str(), Some(&variables), None)?; + Ok(()) + } + } + + #[crate::pymethods(crate = "crate")] + impl CodeRunner { + fn func(&mut self, py: Python<'_>) -> PyResult<()> { + py.import("math")?; + Ok(()) + } + } + + let mut runner = CodeRunner { + code: CString::new( + r#" +cls.func() +"# + .to_string(), + ) + .unwrap(), + }; + + Python::with_gil(|py| { + runner.reproducer(py).unwrap(); + }); + } } From da3e37e335bfe108662c96330330a6dd4c53a181 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers <7943856+bschoenmaeckers@users.noreply.github.com> Date: Wed, 19 Feb 2025 18:06:39 +0100 Subject: [PATCH 449/495] Optimize `Iterator::last()` for `PyList` & `PyTuple` and `Iterator::count()` for `PyDict`, `PyList`, `PyTuple` & `PySet` (#4878) * Optimize `Iterator::last()` for `PyList` & `PyTuple` * Optimize `Iterator::count()` for `PyDict`, `PyList`, `PyTuple` & `PySet` --- newsfragments/4878.added.md | 2 ++ src/types/dict.rs | 24 ++++++++++++++++++ src/types/frozenset.rs | 16 ++++++++++++ src/types/list.rs | 33 +++++++++++++++++++++++++ src/types/set.rs | 16 ++++++++++++ src/types/tuple.rs | 49 +++++++++++++++++++++++++++++++++++++ 6 files changed, 140 insertions(+) create mode 100644 newsfragments/4878.added.md diff --git a/newsfragments/4878.added.md b/newsfragments/4878.added.md new file mode 100644 index 00000000000..0130b2b805b --- /dev/null +++ b/newsfragments/4878.added.md @@ -0,0 +1,2 @@ +- Optimizes `last` for `BoundListIterator`, `BoundTupleIterator` and `BorrowedTupleIterator` +- Optimizes `Iterator::count()` for `PyDict`, `PyList`, `PyTuple` & `PySet` \ No newline at end of file diff --git a/src/types/dict.rs b/src/types/dict.rs index 0d2e6ff335f..3f5118aab6b 100644 --- a/src/types/dict.rs +++ b/src/types/dict.rs @@ -534,6 +534,14 @@ impl<'py> Iterator for BoundDictIterator<'py> { (len, Some(len)) } + #[inline] + fn count(self) -> usize + where + Self: Sized, + { + self.len() + } + #[inline] #[cfg(Py_GIL_DISABLED)] fn fold(mut self, init: B, mut f: F) -> B @@ -736,6 +744,14 @@ mod borrowed_iter { let len = self.len(); (len, Some(len)) } + + #[inline] + fn count(self) -> usize + where + Self: Sized, + { + self.len() + } } impl ExactSizeIterator for BorrowedDictIter<'_, '_> { @@ -1657,4 +1673,12 @@ mod tests { .is_err()); }); } + + #[test] + fn test_iter_count() { + Python::with_gil(|py| { + let dict = [(1, 1), (2, 2), (3, 3)].into_py_dict(py).unwrap(); + assert_eq!(dict.iter().count(), 3); + }) + } } diff --git a/src/types/frozenset.rs b/src/types/frozenset.rs index 954c49b5902..df22560cd8e 100644 --- a/src/types/frozenset.rs +++ b/src/types/frozenset.rs @@ -237,6 +237,14 @@ impl<'py> Iterator for BoundFrozenSetIterator<'py> { fn size_hint(&self) -> (usize, Option) { (self.remaining, Some(self.remaining)) } + + #[inline] + fn count(self) -> usize + where + Self: Sized, + { + self.len() + } } impl ExactSizeIterator for BoundFrozenSetIterator<'_> { @@ -358,4 +366,12 @@ mod tests { assert!(!set.contains(3).unwrap()); }); } + + #[test] + fn test_iter_count() { + Python::with_gil(|py| { + let set = PyFrozenSet::new(py, vec![1, 2, 3]).unwrap(); + assert_eq!(set.iter().count(), 3); + }) + } } diff --git a/src/types/list.rs b/src/types/list.rs index e16e5009a01..ead22315f05 100644 --- a/src/types/list.rs +++ b/src/types/list.rs @@ -695,6 +695,22 @@ impl<'py> Iterator for BoundListIterator<'py> { (len, Some(len)) } + #[inline] + fn count(self) -> usize + where + Self: Sized, + { + self.len() + } + + #[inline] + fn last(mut self) -> Option + where + Self: Sized, + { + self.next_back() + } + #[inline] #[cfg(all(Py_GIL_DISABLED, not(feature = "nightly")))] fn fold(mut self, init: B, mut f: F) -> B @@ -1778,4 +1794,21 @@ mod tests { assert_eq!(iter4.next_back().unwrap().extract::().unwrap(), 5); }) } + + #[test] + fn test_iter_last() { + Python::with_gil(|py| { + let list = PyList::new(py, vec![1, 2, 3]).unwrap(); + let last = list.iter().last(); + assert_eq!(last.unwrap().extract::().unwrap(), 3); + }) + } + + #[test] + fn test_iter_count() { + Python::with_gil(|py| { + let list = PyList::new(py, vec![1, 2, 3]).unwrap(); + assert_eq!(list.iter().count(), 3); + }) + } } diff --git a/src/types/set.rs b/src/types/set.rs index 60aa9428562..ddaddbfe5ed 100644 --- a/src/types/set.rs +++ b/src/types/set.rs @@ -267,6 +267,14 @@ impl<'py> Iterator for BoundSetIterator<'py> { fn size_hint(&self) -> (usize, Option) { (self.remaining, Some(self.remaining)) } + + #[inline] + fn count(self) -> usize + where + Self: Sized, + { + self.len() + } } impl ExactSizeIterator for BoundSetIterator<'_> { @@ -479,4 +487,12 @@ mod tests { assert_eq!(iter.size_hint(), (0, Some(0))); }); } + + #[test] + fn test_iter_count() { + Python::with_gil(|py| { + let set = PySet::new(py, vec![1, 2, 3]).unwrap(); + assert_eq!(set.iter().count(), 3); + }) + } } diff --git a/src/types/tuple.rs b/src/types/tuple.rs index 1d674f47a79..81a7ad911e9 100644 --- a/src/types/tuple.rs +++ b/src/types/tuple.rs @@ -377,6 +377,22 @@ impl<'py> Iterator for BoundTupleIterator<'py> { (len, Some(len)) } + #[inline] + fn count(self) -> usize + where + Self: Sized, + { + self.len() + } + + #[inline] + fn last(mut self) -> Option + where + Self: Sized, + { + self.next_back() + } + #[inline] #[cfg(not(feature = "nightly"))] fn nth(&mut self, n: usize) -> Option { @@ -548,6 +564,22 @@ impl<'a, 'py> Iterator for BorrowedTupleIterator<'a, 'py> { let len = self.len(); (len, Some(len)) } + + #[inline] + fn count(self) -> usize + where + Self: Sized, + { + self.len() + } + + #[inline] + fn last(mut self) -> Option + where + Self: Sized, + { + self.next_back() + } } impl DoubleEndedIterator for BorrowedTupleIterator<'_, '_> { @@ -1728,4 +1760,21 @@ mod tests { assert_eq!(iter4.next_back().unwrap().extract::().unwrap(), 5); }) } + + #[test] + fn test_iter_last() { + Python::with_gil(|py| { + let tuple = PyTuple::new(py, vec![1, 2, 3]).unwrap(); + let last = tuple.iter().last(); + assert_eq!(last.unwrap().extract::().unwrap(), 3); + }) + } + + #[test] + fn test_iter_count() { + Python::with_gil(|py| { + let tuple = PyTuple::new(py, vec![1, 2, 3]).unwrap(); + assert_eq!(tuple.iter().count(), 3); + }) + } } From 73ce4030ef09df7c799996253a332e9d402e2885 Mon Sep 17 00:00:00 2001 From: Ivan Carvalho <8753214+IvanIsCoding@users.noreply.github.com> Date: Wed, 19 Feb 2025 14:34:59 -0500 Subject: [PATCH 450/495] Add support for type hinting objects (#4917) * Add support for types.GenericAlias * Remove wrong cfg that made tests fail * Add changelog entry * Address first batch of PR comments * Move Python version check to inside FFI module * Gate imports to make Clippy happy * pub use does not need to be gated * Revert "pub use does not need to be gated" it makes clippy fail This reverts commit 3eaf0a1563385c47252a63a50743cce9b2f76fa2. * Handle errors in PyGenericAlias::new * Use PyResultExt trait * Use PyPy_GenericAlias --- newsfragments/4917.added.md | 2 + pyo3-ffi/src/genericaliasobject.rs | 12 +++++ pyo3-ffi/src/lib.rs | 4 +- src/types/genericalias.rs | 72 ++++++++++++++++++++++++++++++ src/types/mod.rs | 4 ++ 5 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 newsfragments/4917.added.md create mode 100644 pyo3-ffi/src/genericaliasobject.rs create mode 100644 src/types/genericalias.rs diff --git a/newsfragments/4917.added.md b/newsfragments/4917.added.md new file mode 100644 index 00000000000..4cc65a8f404 --- /dev/null +++ b/newsfragments/4917.added.md @@ -0,0 +1,2 @@ +Added support for creating [types.GenericAlias](https://docs.python.org/3/library/types.html#types.GenericAlias) +objects in PyO3 with `pyo3::types::PyGenericAlias`. \ No newline at end of file diff --git a/pyo3-ffi/src/genericaliasobject.rs b/pyo3-ffi/src/genericaliasobject.rs new file mode 100644 index 00000000000..7979d7d863e --- /dev/null +++ b/pyo3-ffi/src/genericaliasobject.rs @@ -0,0 +1,12 @@ +#[cfg(Py_3_9)] +use crate::object::{PyObject, PyTypeObject}; + +#[cfg_attr(windows, link(name = "pythonXY"))] +extern "C" { + #[cfg(Py_3_9)] + #[cfg_attr(PyPy, link_name = "PyPy_GenericAlias")] + pub fn Py_GenericAlias(origin: *mut PyObject, args: *mut PyObject) -> *mut PyObject; + + #[cfg(Py_3_9)] + pub static mut Py_GenericAliasType: PyTypeObject; +} diff --git a/pyo3-ffi/src/lib.rs b/pyo3-ffi/src/lib.rs index 7bdba1173d6..bb0d0ad040b 100644 --- a/pyo3-ffi/src/lib.rs +++ b/pyo3-ffi/src/lib.rs @@ -410,6 +410,8 @@ pub use self::enumobject::*; pub use self::fileobject::*; pub use self::fileutils::*; pub use self::floatobject::*; +#[cfg(Py_3_9)] +pub use self::genericaliasobject::*; pub use self::import::*; pub use self::intrcheck::*; pub use self::iterobject::*; @@ -479,7 +481,7 @@ mod fileobject; mod fileutils; mod floatobject; // skipped empty frameobject.h -// skipped genericaliasobject.h +mod genericaliasobject; mod import; // skipped interpreteridobject.h mod intrcheck; diff --git a/src/types/genericalias.rs b/src/types/genericalias.rs new file mode 100644 index 00000000000..48cdcc17154 --- /dev/null +++ b/src/types/genericalias.rs @@ -0,0 +1,72 @@ +use crate::err::PyResult; +use crate::ffi_ptr_ext::FfiPtrExt; +use crate::py_result_ext::PyResultExt; +use crate::{ffi, Bound, PyAny, Python}; + +/// Represents a Python [`types.GenericAlias`](https://docs.python.org/3/library/types.html#types.GenericAlias) object. +/// +/// Values of this type are accessed via PyO3's smart pointers, e.g. as +/// [`Py`][crate::Py] or [`Bound<'py, PyGenericAlias>`][Bound]. +/// +/// This type is particularly convenient for users implementing +/// [`__class_getitem__`](https://docs.python.org/3/reference/datamodel.html#object.__class_getitem__) +/// for PyO3 classes to allow runtime parameterization. +#[repr(transparent)] +pub struct PyGenericAlias(PyAny); + +pyobject_native_type!( + PyGenericAlias, + ffi::PyDictObject, + pyobject_native_static_type_object!(ffi::Py_GenericAliasType) +); + +impl PyGenericAlias { + /// Creates a new Python GenericAlias object. + /// + /// origin should be a non-parameterized generic class. + /// args should be a tuple (possibly of length 1) of types which parameterize origin. + pub fn new<'py>( + py: Python<'py>, + origin: &Bound<'py, PyAny>, + args: &Bound<'py, PyAny>, + ) -> PyResult> { + unsafe { + ffi::Py_GenericAlias(origin.as_ptr(), args.as_ptr()) + .assume_owned_or_err(py) + .downcast_into_unchecked() + } + } +} + +#[cfg(test)] +mod tests { + use crate::instance::BoundObject; + use crate::types::any::PyAnyMethods; + use crate::{ffi, Python}; + + use super::PyGenericAlias; + + // Tests that PyGenericAlias::new is identical to types.GenericAlias + // created from Python. + #[test] + fn equivalency_test() { + Python::with_gil(|py| { + let list_int = py + .eval(ffi::c_str!("list[int]"), None, None) + .unwrap() + .into_bound(); + + let cls = py + .eval(ffi::c_str!("list"), None, None) + .unwrap() + .into_bound(); + let key = py + .eval(ffi::c_str!("(int,)"), None, None) + .unwrap() + .into_bound(); + let generic_alias = PyGenericAlias::new(py, &cls, &key).unwrap(); + + assert!(generic_alias.eq(list_int).unwrap()); + }) + } +} diff --git a/src/types/mod.rs b/src/types/mod.rs index 8304afedf5e..aa2ef0d07d3 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -25,6 +25,8 @@ pub use self::frozenset::{PyFrozenSet, PyFrozenSetBuilder, PyFrozenSetMethods}; pub use self::function::PyCFunction; #[cfg(all(not(Py_LIMITED_API), not(all(PyPy, not(Py_3_8)))))] pub use self::function::PyFunction; +#[cfg(Py_3_9)] +pub use self::genericalias::PyGenericAlias; pub use self::iterator::PyIterator; pub use self::list::{PyList, PyListMethods}; pub use self::mapping::{PyMapping, PyMappingMethods}; @@ -248,6 +250,8 @@ pub(crate) mod float; mod frame; pub(crate) mod frozenset; mod function; +#[cfg(Py_3_9)] +pub(crate) mod genericalias; pub(crate) mod iterator; pub(crate) mod list; pub(crate) mod mapping; From 241080aeba3e97f29ac2617cd89c0e26563d6703 Mon Sep 17 00:00:00 2001 From: Bruno Kolenbrander <59372212+mejrs@users.noreply.github.com> Date: Thu, 20 Feb 2025 20:06:06 +0100 Subject: [PATCH 451/495] Improve diagnostic for invalid function passed to from_py_with (#4840) --- pyo3-macros-backend/src/frompyobject.rs | 79 +++++++++++++-------- pyo3-macros-backend/src/params.rs | 10 ++- pyo3-macros-backend/src/pymethod.rs | 14 ++-- tests/ui/invalid_argument_attributes.rs | 7 ++ tests/ui/invalid_argument_attributes.stderr | 11 +++ 5 files changed, 83 insertions(+), 38 deletions(-) diff --git a/pyo3-macros-backend/src/frompyobject.rs b/pyo3-macros-backend/src/frompyobject.rs index b353e2dc16d..0ffd13bdae8 100644 --- a/pyo3-macros-backend/src/frompyobject.rs +++ b/pyo3-macros-backend/src/frompyobject.rs @@ -3,7 +3,7 @@ use crate::attributes::{ }; use crate::utils::Ctx; use proc_macro2::TokenStream; -use quote::{format_ident, quote, ToTokens}; +use quote::{format_ident, quote, quote_spanned, ToTokens}; use syn::{ ext::IdentExt, parenthesized, @@ -276,31 +276,40 @@ impl<'a> Container<'a> { let struct_name = self.name(); if let Some(ident) = field_ident { let field_name = ident.to_string(); - match from_py_with { - None => quote! { + if let Some(FromPyWithAttribute { + kw, + value: expr_path, + }) = from_py_with + { + let extractor = quote_spanned! { kw.span => + { let from_py_with: fn(_) -> _ = #expr_path; from_py_with } + }; + quote! { Ok(#self_ty { - #ident: #pyo3_path::impl_::frompyobject::extract_struct_field(obj, #struct_name, #field_name)? + #ident: #pyo3_path::impl_::frompyobject::extract_struct_field_with(#extractor, obj, #struct_name, #field_name)? }) - }, - Some(FromPyWithAttribute { - value: expr_path, .. - }) => quote! { + } + } else { + quote! { Ok(#self_ty { - #ident: #pyo3_path::impl_::frompyobject::extract_struct_field_with(#expr_path as fn(_) -> _, obj, #struct_name, #field_name)? + #ident: #pyo3_path::impl_::frompyobject::extract_struct_field(obj, #struct_name, #field_name)? }) - }, + } + } + } else if let Some(FromPyWithAttribute { + kw, + value: expr_path, + }) = from_py_with + { + let extractor = quote_spanned! { kw.span => + { let from_py_with: fn(_) -> _ = #expr_path; from_py_with } + }; + quote! { + #pyo3_path::impl_::frompyobject::extract_tuple_struct_field_with(#extractor, obj, #struct_name, 0).map(#self_ty) } } else { - match from_py_with { - None => quote! { - #pyo3_path::impl_::frompyobject::extract_tuple_struct_field(obj, #struct_name, 0).map(#self_ty) - }, - - Some(FromPyWithAttribute { - value: expr_path, .. - }) => quote! { - #pyo3_path::impl_::frompyobject::extract_tuple_struct_field_with(#expr_path as fn(_) -> _, obj, #struct_name, 0).map(#self_ty) - }, + quote! { + #pyo3_path::impl_::frompyobject::extract_tuple_struct_field(obj, #struct_name, 0).map(#self_ty) } } } @@ -313,16 +322,20 @@ impl<'a> Container<'a> { .map(|i| format_ident!("arg{}", i)) .collect(); let fields = struct_fields.iter().zip(&field_idents).enumerate().map(|(index, (field, ident))| { - match &field.from_py_with { - None => quote!( + if let Some(FromPyWithAttribute { + kw, + value: expr_path, .. + }) = &field.from_py_with { + let extractor = quote_spanned! { kw.span => + { let from_py_with: fn(_) -> _ = #expr_path; from_py_with } + }; + quote! { + #pyo3_path::impl_::frompyobject::extract_tuple_struct_field_with(#extractor, &#ident, #struct_name, #index)? + } + } else { + quote!{ #pyo3_path::impl_::frompyobject::extract_tuple_struct_field(&#ident, #struct_name, #index)? - ), - Some(FromPyWithAttribute { - value: expr_path, .. - }) => quote! ( - #pyo3_path::impl_::frompyobject::extract_tuple_struct_field_with(#expr_path as fn(_) -> _, &#ident, #struct_name, #index)? - ), - } + }} }); quote!( @@ -359,10 +372,14 @@ impl<'a> Container<'a> { } }; let extractor = if let Some(FromPyWithAttribute { - value: expr_path, .. + kw, + value: expr_path, }) = &field.from_py_with { - quote!(#pyo3_path::impl_::frompyobject::extract_struct_field_with(#expr_path as fn(_) -> _, &value, #struct_name, #field_name)?) + let extractor = quote_spanned! { kw.span => + { let from_py_with: fn(_) -> _ = #expr_path; from_py_with } + }; + quote! (#pyo3_path::impl_::frompyobject::extract_struct_field_with(#extractor, &#getter?, #struct_name, #field_name)?) } else { quote!(#pyo3_path::impl_::frompyobject::extract_struct_field(&value, #struct_name, #field_name)?) }; diff --git a/pyo3-macros-backend/src/params.rs b/pyo3-macros-backend/src/params.rs index f967149c725..9425b8d32b6 100644 --- a/pyo3-macros-backend/src/params.rs +++ b/pyo3-macros-backend/src/params.rs @@ -1,5 +1,6 @@ use crate::utils::Ctx; use crate::{ + attributes::FromPyWithAttribute, method::{FnArg, FnSpec, RegularArg}, pyfunction::FunctionSignature, quotes::some_wrap, @@ -248,13 +249,16 @@ pub(crate) fn impl_regular_arg_param( default = default.map(|tokens| some_wrap(tokens, ctx)); } - if arg.from_py_with.is_some() { + if let Some(FromPyWithAttribute { kw, .. }) = arg.from_py_with { + let extractor = quote_spanned! { kw.span => + { let from_py_with: fn(_) -> _ = #from_py_with; from_py_with } + }; if let Some(default) = default { quote_arg_span! { #pyo3_path::impl_::extract_argument::from_py_with_with_default( #arg_value, #name_str, - #from_py_with as fn(_) -> _, + #extractor, #[allow(clippy::redundant_closure)] { || #default @@ -267,7 +271,7 @@ pub(crate) fn impl_regular_arg_param( #pyo3_path::impl_::extract_argument::from_py_with( #unwrap, #name_str, - #from_py_with as fn(_) -> _, + #extractor, )? } } diff --git a/pyo3-macros-backend/src/pymethod.rs b/pyo3-macros-backend/src/pymethod.rs index c21f6d4556e..825a4addfd3 100644 --- a/pyo3-macros-backend/src/pymethod.rs +++ b/pyo3-macros-backend/src/pymethod.rs @@ -1,7 +1,7 @@ use std::borrow::Cow; use std::ffi::CString; -use crate::attributes::{NameAttribute, RenamingRule}; +use crate::attributes::{FromPyWithAttribute, NameAttribute, RenamingRule}; use crate::method::{CallingConvention, ExtractErrorMode, PyArg}; use crate::params::{impl_regular_arg_param, Holders}; use crate::utils::PythonDoc; @@ -1179,14 +1179,20 @@ fn extract_object( let Ctx { pyo3_path, .. } = ctx; let name = arg.name().unraw().to_string(); - let extract = if let Some(from_py_with) = - arg.from_py_with().map(|from_py_with| &from_py_with.value) + let extract = if let Some(FromPyWithAttribute { + kw, + value: extractor, + }) = arg.from_py_with() { + let extractor = quote_spanned! { kw.span => + { let from_py_with: fn(_) -> _ = #extractor; from_py_with } + }; + quote! { #pyo3_path::impl_::extract_argument::from_py_with( unsafe { #pyo3_path::impl_::pymethods::BoundRef::ref_from_ptr(py, &#source_ptr).0 }, #name, - #from_py_with as fn(_) -> _, + #extractor, ) } } else { diff --git a/tests/ui/invalid_argument_attributes.rs b/tests/ui/invalid_argument_attributes.rs index 6797642d77b..819d6709ef8 100644 --- a/tests/ui/invalid_argument_attributes.rs +++ b/tests/ui/invalid_argument_attributes.rs @@ -15,4 +15,11 @@ fn from_py_with_value_not_a_string(#[pyo3(from_py_with = func)] _param: String) #[pyfunction] fn from_py_with_repeated(#[pyo3(from_py_with = "func", from_py_with = "func")] _param: String) {} +fn bytes_from_py(bytes: &Bound<'_, pyo3::types::PyBytes>) -> Vec { + bytes.as_bytes().to_vec() +} + +#[pyfunction] +fn f(#[pyo3(from_py_with = "bytes_from_py")] _bytes: Vec) {} + fn main() {} diff --git a/tests/ui/invalid_argument_attributes.stderr b/tests/ui/invalid_argument_attributes.stderr index e6c42f82a87..6679dd635f1 100644 --- a/tests/ui/invalid_argument_attributes.stderr +++ b/tests/ui/invalid_argument_attributes.stderr @@ -27,3 +27,14 @@ error: `from_py_with` may only be specified once per argument | 16 | fn from_py_with_repeated(#[pyo3(from_py_with = "func", from_py_with = "func")] _param: String) {} | ^^^^^^^^^^^^ + +error[E0308]: mismatched types + --> tests/ui/invalid_argument_attributes.rs:23:13 + | +22 | #[pyfunction] + | ------------- here the type of `from_py_with` is inferred to be `fn(&pyo3::Bound<'_, PyBytes>) -> Vec` +23 | fn f(#[pyo3(from_py_with = "bytes_from_py")] _bytes: Vec) {} + | ^^^^^^^^^^^^ expected `PyAny`, found `PyBytes` + | + = note: expected fn pointer `fn(&pyo3::Bound<'_, PyAny>) -> Result<_, PyErr>` + found fn pointer `fn(&pyo3::Bound<'_, PyBytes>) -> Vec` From c9ee46fd953435dd91cf764f5b46b1753aa66aa2 Mon Sep 17 00:00:00 2001 From: Peter Hall Date: Sun, 23 Feb 2025 18:54:20 +0000 Subject: [PATCH 452/495] docs: Removed references to unmaintained pyo3-asyncio crate (#4910) * Removed references to unmaintained pyo3-asyncio crate * Applied CR suggestions --- README.md | 2 +- guide/src/ecosystem/async-await.md | 548 +---------------------------- pytests/src/awaitable.rs | 2 +- 3 files changed, 6 insertions(+), 546 deletions(-) diff --git a/README.md b/README.md index 02f1d56de6d..a607cdaae94 100644 --- a/README.md +++ b/README.md @@ -178,7 +178,7 @@ about this topic. - [dict-derive](https://github.com/gperinazzo/dict-derive) _Derive FromPyObject to automatically transform Python dicts into Rust structs_ - [pyo3-log](https://github.com/vorner/pyo3-log) _Bridge from Rust to Python logging_ - [pythonize](https://github.com/davidhewitt/pythonize) _Serde serializer for converting Rust objects to JSON-compatible Python objects_ -- [pyo3-asyncio](https://github.com/awestlake87/pyo3-asyncio) _Utilities for working with Python's Asyncio library and async functions_ +- [pyo3-async-runtimes](https://github.com/PyO3/pyo3-async-runtimes) _Utilities for interoperability with Python's Asyncio library and Rust's async runtimes._ - [rustimport](https://github.com/mityax/rustimport) _Directly import Rust files or crates from Python, without manual compilation step. Provides pyo3 integration by default and generates pyo3 binding code automatically._ - [pyo3-arrow](https://crates.io/crates/pyo3-arrow) _Lightweight [Apache Arrow](https://arrow.apache.org/) integration for pyo3._ - [pyo3-bytes](https://crates.io/crates/pyo3-bytes) _Integration between [`bytes`](https://crates.io/crates/bytes) and pyo3._ diff --git a/guide/src/ecosystem/async-await.md b/guide/src/ecosystem/async-await.md index 0128efbc8a3..9da906edeb9 100644 --- a/guide/src/ecosystem/async-await.md +++ b/guide/src/ecosystem/async-await.md @@ -3,553 +3,13 @@ *`async`/`await` support is currently being integrated in PyO3. See the [dedicated documentation](../async-await.md)* If you are working with a Python library that makes use of async functions or wish to provide -Python bindings for an async Rust library, [`pyo3-asyncio`](https://github.com/awestlake87/pyo3-asyncio) +Python bindings for an async Rust library, [`pyo3-async-runtimes`](https://github.com/PyO3/pyo3-async-runtimes) likely has the tools you need. It provides conversions between async functions in both Python and Rust and was designed with first-class support for popular Rust runtimes such as [`tokio`](https://tokio.rs/) and [`async-std`](https://async.rs/). In addition, all async Python -code runs on the default `asyncio` event loop, so `pyo3-asyncio` should work just fine with existing +code runs on the default `asyncio` event loop, so `pyo3-async-runtimes` should work just fine with existing Python libraries. -In the following sections, we'll give a general overview of `pyo3-asyncio` explaining how to call -async Python functions with PyO3, how to call async Rust functions from Python, and how to configure -your codebase to manage the runtimes of both. - -## Quickstart - -Here are some examples to get you started right away! A more detailed breakdown -of the concepts in these examples can be found in the following sections. - -### Rust Applications -Here we initialize the runtime, import Python's `asyncio` library and run the given future to completion using Python's default `EventLoop` and `async-std`. Inside the future, we convert `asyncio` sleep into a Rust future and await it. - - -```toml -# Cargo.toml dependencies -[dependencies] -pyo3 = { version = "0.14" } -pyo3-asyncio = { version = "0.14", features = ["attributes", "async-std-runtime"] } -async-std = "1.9" -``` - -```rust -//! main.rs - -use pyo3::prelude::*; - -#[pyo3_asyncio::async_std::main] -async fn main() -> PyResult<()> { - let fut = Python::with_gil(|py| { - let asyncio = py.import("asyncio")?; - // convert asyncio.sleep into a Rust Future - pyo3_asyncio::async_std::into_future(asyncio.call_method1("sleep", (1.into_py(py),))?) - })?; - - fut.await?; - - Ok(()) -} -``` - -The same application can be written to use `tokio` instead using the `#[pyo3_asyncio::tokio::main]` -attribute. - -```toml -# Cargo.toml dependencies -[dependencies] -pyo3 = { version = "0.14" } -pyo3-asyncio = { version = "0.14", features = ["attributes", "tokio-runtime"] } -tokio = "1.4" -``` - -```rust -//! main.rs - -use pyo3::prelude::*; - -#[pyo3_asyncio::tokio::main] -async fn main() -> PyResult<()> { - let fut = Python::with_gil(|py| { - let asyncio = py.import("asyncio")?; - // convert asyncio.sleep into a Rust Future - pyo3_asyncio::tokio::into_future(asyncio.call_method1("sleep", (1.into_py(py),))?) - })?; - - fut.await?; - - Ok(()) -} -``` - -More details on the usage of this library can be found in the [API docs](https://awestlake87.github.io/pyo3-asyncio/master/doc) and the primer below. - -### PyO3 Native Rust Modules - -PyO3 Asyncio can also be used to write native modules with async functions. - -Add the `[lib]` section to `Cargo.toml` to make your library a `cdylib` that Python can import. -```toml -[lib] -name = "my_async_module" -crate-type = ["cdylib"] -``` - -Make your project depend on `pyo3` with the `extension-module` feature enabled and select your -`pyo3-asyncio` runtime: - -For `async-std`: -```toml -[dependencies] -pyo3 = { version = "0.14", features = ["extension-module"] } -pyo3-asyncio = { version = "0.14", features = ["async-std-runtime"] } -async-std = "1.9" -``` - -For `tokio`: -```toml -[dependencies] -pyo3 = { version = "0.14", features = ["extension-module"] } -pyo3-asyncio = { version = "0.14", features = ["tokio-runtime"] } -tokio = "1.4" -``` - -Export an async function that makes use of `async-std`: - -```rust -//! lib.rs - -use pyo3::{prelude::*, wrap_pyfunction}; - -#[pyfunction] -fn rust_sleep(py: Python<'_>) -> PyResult<&Bound<'_, PyAny>> { - pyo3_asyncio::async_std::future_into_py(py, async { - async_std::task::sleep(std::time::Duration::from_secs(1)).await; - Ok(Python::with_gil(|py| py.None())) - }) -} - -#[pymodule] -fn my_async_module(m: &Bound<'_, PyModule>) -> PyResult<()> { - m.add_function(wrap_pyfunction!(rust_sleep, m)?) -} -``` - -If you want to use `tokio` instead, here's what your module should look like: - -```rust -//! lib.rs - -use pyo3::{prelude::*, wrap_pyfunction}; - -#[pyfunction] -fn rust_sleep(py: Python<'_>) -> PyResult<&Bound<'_, PyAny>>> { - pyo3_asyncio::tokio::future_into_py(py, async { - tokio::time::sleep(std::time::Duration::from_secs(1)).await; - Ok(Python::with_gil(|py| py.None())) - }) -} - -#[pymodule] -fn my_async_module(m: &Bound<'_, PyModule>) -> PyResult<()> { - m.add_function(wrap_pyfunction!(rust_sleep, m)?) -} -``` - -You can build your module with maturin (see the [Using Rust in Python](https://pyo3.rs/main/#using-rust-from-python) section in the PyO3 guide for setup instructions). After that you should be able to run the Python REPL to try it out. - -```bash -maturin develop && python3 -🔗 Found pyo3 bindings -🐍 Found CPython 3.8 at python3 - Finished dev [unoptimized + debuginfo] target(s) in 0.04s -Python 3.8.5 (default, Jan 27 2021, 15:41:15) -[GCC 9.3.0] on linux -Type "help", "copyright", "credits" or "license" for more information. ->>> import asyncio ->>> ->>> from my_async_module import rust_sleep ->>> ->>> async def main(): ->>> await rust_sleep() ->>> ->>> # should sleep for 1s ->>> asyncio.run(main()) ->>> -``` - -## Awaiting an Async Python Function in Rust - -Let's take a look at a dead simple async Python function: - -```python -# Sleep for 1 second -async def py_sleep(): - await asyncio.sleep(1) -``` - -**Async functions in Python are simply functions that return a `coroutine` object**. For our purposes, -we really don't need to know much about these `coroutine` objects. The key factor here is that calling -an `async` function is _just like calling a regular function_, the only difference is that we have -to do something special with the object that it returns. - -Normally in Python, that something special is the `await` keyword, but in order to await this -coroutine in Rust, we first need to convert it into Rust's version of a `coroutine`: a `Future`. -That's where `pyo3-asyncio` comes in. -[`pyo3_asyncio::async_std::into_future`](https://docs.rs/pyo3-asyncio/latest/pyo3_asyncio/async_std/fn.into_future.html) -performs this conversion for us. - -The following example uses `into_future` to call the `py_sleep` function shown above and then await the -coroutine object returned from the call: - -```rust -use pyo3::prelude::*; - -#[pyo3_asyncio::tokio::main] -async fn main() -> PyResult<()> { - let future = Python::with_gil(|py| -> PyResult<_> { - // import the module containing the py_sleep function - let example = py.import("example")?; - - // calling the py_sleep method like a normal function - // returns a coroutine - let coroutine = example.call_method0("py_sleep")?; - - // convert the coroutine into a Rust future using the - // tokio runtime - pyo3_asyncio::tokio::into_future(coroutine) - })?; - - // await the future - future.await?; - - Ok(()) -} -``` - -Alternatively, the below example shows how to write a `#[pyfunction]` which uses `into_future` to receive and await -a coroutine argument: - -```rust -#[pyfunction] -fn await_coro(coro: &Bound<'_, PyAny>>) -> PyResult<()> { - // convert the coroutine into a Rust future using the - // async_std runtime - let f = pyo3_asyncio::async_std::into_future(coro)?; - - pyo3_asyncio::async_std::run_until_complete(coro.py(), async move { - // await the future - f.await?; - Ok(()) - }) -} -``` - -This could be called from Python as: - -```python -import asyncio - -async def py_sleep(): - asyncio.sleep(1) - -await_coro(py_sleep()) -``` - -If you wanted to pass a callable function to the `#[pyfunction]` instead, (i.e. the last line becomes `await_coro(py_sleep))`, then the above example needs to be tweaked to first call the callable to get the coroutine: - -```rust -#[pyfunction] -fn await_coro(callable: &Bound<'_, PyAny>>) -> PyResult<()> { - // get the coroutine by calling the callable - let coro = callable.call0()?; - - // convert the coroutine into a Rust future using the - // async_std runtime - let f = pyo3_asyncio::async_std::into_future(coro)?; - - pyo3_asyncio::async_std::run_until_complete(coro.py(), async move { - // await the future - f.await?; - Ok(()) - }) -} -``` - -This can be particularly helpful where you need to repeatedly create and await a coroutine. Trying to await the same coroutine multiple times will raise an error: - -```python -RuntimeError: cannot reuse already awaited coroutine -``` - -> If you're interested in learning more about `coroutines` and `awaitables` in general, check out the -> [Python 3 `asyncio` docs](https://docs.python.org/3/library/asyncio-task.html) for more information. - -## Awaiting a Rust Future in Python - -Here we have the same async function as before written in Rust using the -[`async-std`](https://async.rs/) runtime: - -```rust -/// Sleep for 1 second -async fn rust_sleep() { - async_std::task::sleep(std::time::Duration::from_secs(1)).await; -} -``` - -Similar to Python, Rust's async functions also return a special object called a -`Future`: - -```rust -let future = rust_sleep(); -``` - -We can convert this `Future` object into Python to make it `awaitable`. This tells Python that you -can use the `await` keyword with it. In order to do this, we'll call -[`pyo3_asyncio::async_std::future_into_py`](https://docs.rs/pyo3-asyncio/latest/pyo3_asyncio/async_std/fn.future_into_py.html): - -```rust -use pyo3::prelude::*; - -async fn rust_sleep() { - async_std::task::sleep(std::time::Duration::from_secs(1)).await; -} - -#[pyfunction] -fn call_rust_sleep(py: Python<'_>) -> PyResult<&Bound<'_, PyAny>>> { - pyo3_asyncio::async_std::future_into_py(py, async move { - rust_sleep().await; - Ok(Python::with_gil(|py| py.None())) - }) -} -``` - -In Python, we can call this pyo3 function just like any other async function: - -```python -from example import call_rust_sleep - -async def rust_sleep(): - await call_rust_sleep() -``` - -## Managing Event Loops - -Python's event loop requires some special treatment, especially regarding the main thread. Some of -Python's `asyncio` features, like proper signal handling, require control over the main thread, which -doesn't always play well with Rust. - -Luckily, Rust's event loops are pretty flexible and don't _need_ control over the main thread, so in -`pyo3-asyncio`, we decided the best way to handle Rust/Python interop was to just surrender the main -thread to Python and run Rust's event loops in the background. Unfortunately, since most event loop -implementations _prefer_ control over the main thread, this can still make some things awkward. - -### PyO3 Asyncio Initialization - -Because Python needs to control the main thread, we can't use the convenient proc macros from Rust -runtimes to handle the `main` function or `#[test]` functions. Instead, the initialization for PyO3 has to be done from the `main` function and the main -thread must block on [`pyo3_asyncio::async_std::run_until_complete`](https://docs.rs/pyo3-asyncio/latest/pyo3_asyncio/async_std/fn.run_until_complete.html). - -Because we have to block on one of those functions, we can't use [`#[async_std::main]`](https://docs.rs/async-std/latest/async_std/attr.main.html) or [`#[tokio::main]`](https://docs.rs/tokio/1.1.0/tokio/attr.main.html) -since it's not a good idea to make long blocking calls during an async function. - -> Internally, these `#[main]` proc macros are expanded to something like this: -> ```rust -> fn main() { -> // your async main fn -> async fn _main_impl() { /* ... */ } -> Runtime::new().block_on(_main_impl()); -> } -> ``` -> Making a long blocking call inside the `Future` that's being driven by `block_on` prevents that -> thread from doing anything else and can spell trouble for some runtimes (also this will actually -> deadlock a single-threaded runtime!). Many runtimes have some sort of `spawn_blocking` mechanism -> that can avoid this problem, but again that's not something we can use here since we need it to -> block on the _main_ thread. - -For this reason, `pyo3-asyncio` provides its own set of proc macros to provide you with this -initialization. These macros are intended to mirror the initialization of `async-std` and `tokio` -while also satisfying the Python runtime's needs. - -Here's a full example of PyO3 initialization with the `async-std` runtime: -```rust -use pyo3::prelude::*; - -#[pyo3_asyncio::async_std::main] -async fn main() -> PyResult<()> { - // PyO3 is initialized - Ready to go - - let fut = Python::with_gil(|py| -> PyResult<_> { - let asyncio = py.import("asyncio")?; - - // convert asyncio.sleep into a Rust Future - pyo3_asyncio::async_std::into_future( - asyncio.call_method1("sleep", (1.into_py(py),))? - ) - })?; - - fut.await?; - - Ok(()) -} -``` - -### A Note About `asyncio.run` - -In Python 3.7+, the recommended way to run a top-level coroutine with `asyncio` -is with `asyncio.run`. In `v0.13` we recommended against using this function due to initialization issues, but in `v0.14` it's perfectly valid to use this function... with a caveat. - -Since our Rust <--> Python conversions require a reference to the Python event loop, this poses a problem. Imagine we have a PyO3 Asyncio module that defines -a `rust_sleep` function like in previous examples. You might rightfully assume that you can call pass this directly into `asyncio.run` like this: - -```python -import asyncio - -from my_async_module import rust_sleep - -asyncio.run(rust_sleep()) -``` - -You might be surprised to find out that this throws an error: -```bash -Traceback (most recent call last): - File "example.py", line 5, in - asyncio.run(rust_sleep()) -RuntimeError: no running event loop -``` - -What's happening here is that we are calling `rust_sleep` _before_ the future is -actually running on the event loop created by `asyncio.run`. This is counter-intuitive, but expected behaviour, and unfortunately there doesn't seem to be a good way of solving this problem within PyO3 Asyncio itself. - -However, we can make this example work with a simple workaround: - -```python -import asyncio - -from my_async_module import rust_sleep - -# Calling main will just construct the coroutine that later calls rust_sleep. -# - This ensures that rust_sleep will be called when the event loop is running, -# not before. -async def main(): - await rust_sleep() - -# Run the main() coroutine at the top-level instead -asyncio.run(main()) -``` - -### Non-standard Python Event Loops - -Python allows you to use alternatives to the default `asyncio` event loop. One -popular alternative is `uvloop`. In `v0.13` using non-standard event loops was -a bit of an ordeal, but in `v0.14` it's trivial. - -#### Using `uvloop` in a PyO3 Asyncio Native Extensions - -```toml -# Cargo.toml - -[lib] -name = "my_async_module" -crate-type = ["cdylib"] - -[dependencies] -pyo3 = { version = "0.14", features = ["extension-module"] } -pyo3-asyncio = { version = "0.14", features = ["tokio-runtime"] } -async-std = "1.9" -tokio = "1.4" -``` - -```rust -//! lib.rs - -use pyo3::{prelude::*, wrap_pyfunction}; - -#[pyfunction] -fn rust_sleep(py: Python<'_>) -> PyResult<&Bound<'_, PyAny>>> { - pyo3_asyncio::tokio::future_into_py(py, async { - tokio::time::sleep(std::time::Duration::from_secs(1)).await; - Ok(Python::with_gil(|py| py.None())) - }) -} - -#[pymodule] -fn my_async_module(m: &Bound<'_, PyModule>) -> PyResult<()> { - m.add_function(wrap_pyfunction!(rust_sleep, m)?)?; - - Ok(()) -} -``` - -```bash -$ maturin develop && python3 -🔗 Found pyo3 bindings -🐍 Found CPython 3.8 at python3 - Finished dev [unoptimized + debuginfo] target(s) in 0.04s -Python 3.8.8 (default, Apr 13 2021, 19:58:26) -[GCC 7.3.0] :: Anaconda, Inc. on linux -Type "help", "copyright", "credits" or "license" for more information. ->>> import asyncio ->>> import uvloop ->>> ->>> import my_async_module ->>> ->>> uvloop.install() ->>> ->>> async def main(): -... await my_async_module.rust_sleep() -... ->>> asyncio.run(main()) ->>> -``` - -#### Using `uvloop` in Rust Applications - -Using `uvloop` in Rust applications is a bit trickier, but it's still possible -with relatively few modifications. - -Unfortunately, we can't make use of the `#[pyo3_asyncio::::main]` attribute with non-standard event loops. This is because the `#[pyo3_asyncio::::main]` proc macro has to interact with the Python -event loop before we can install the `uvloop` policy. - -```toml -[dependencies] -async-std = "1.9" -pyo3 = "0.14" -pyo3-asyncio = { version = "0.14", features = ["async-std-runtime"] } -``` - -```rust -//! main.rs - -use pyo3::{prelude::*, types::PyType}; - -fn main() -> PyResult<()> { - pyo3::prepare_freethreaded_python(); - - Python::with_gil(|py| { - let uvloop = py.import("uvloop")?; - uvloop.call_method0("install")?; - - // store a reference for the assertion - let uvloop = PyObject::from(uvloop); - - pyo3_asyncio::async_std::run(py, async move { - // verify that we are on a uvloop.Loop - Python::with_gil(|py| -> PyResult<()> { - assert!(pyo3_asyncio::async_std::get_current_loop(py)?.is_instance( - uvloop - .as_ref(py) - .getattr("Loop")? - )?); - Ok(()) - })?; - - async_std::task::sleep(std::time::Duration::from_secs(1)).await; - - Ok(()) - }) - }) -} -``` - ## Additional Information -- Managing event loop references can be tricky with pyo3-asyncio. See [Event Loop References](https://docs.rs/pyo3-asyncio/#event-loop-references) in the API docs to get a better intuition for how event loop references are managed in this library. -- Testing pyo3-asyncio libraries and applications requires a custom test harness since Python requires control over the main thread. You can find a testing guide in the [API docs for the `testing` module](https://docs.rs/pyo3-asyncio/latest/pyo3_asyncio/testing) +- Managing event loop references can be tricky with `pyo3-async-runtimes`. See [Event Loop References](https://docs.rs/pyo3-async-runtimes/#event-loop-references-and-contextvars) in the API docs to get a better intuition for how event loop references are managed in this library. +- Testing `pyo3-async-runtimes` libraries and applications requires a custom test harness since Python requires control over the main thread. You can find a testing guide in the [API docs for the `testing` module](https://docs.rs/pyo3-async-runtimes/latest/pyo3_async_runtimes/testing) diff --git a/pytests/src/awaitable.rs b/pytests/src/awaitable.rs index fb04c33ed05..01a93c70a0d 100644 --- a/pytests/src/awaitable.rs +++ b/pytests/src/awaitable.rs @@ -2,7 +2,7 @@ //! awaitable protocol. //! //! Both IterAwaitable and FutureAwaitable will return a value immediately -//! when awaited, see guide examples related to pyo3-asyncio for ways +//! when awaited, see guide examples related to pyo3-async-runtimes for ways //! to suspend tasks and await results. use pyo3::exceptions::PyStopIteration; From b07871d962d56e66243d2c52a700e978545ff417 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Tue, 25 Feb 2025 16:07:59 +0000 Subject: [PATCH 453/495] docs: add release notes from 0.23.4 and 0.23.5 (#4935) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * release: 0.23.4 * release: 0.23.5 (#4929) release: 0.23.5 --------- Co-authored-by: Icxolu <10486322+Icxolu@users.noreply.github.com> Co-authored-by: David Hewitt --------- Co-authored-by: Michał Górny Co-authored-by: Icxolu <10486322+Icxolu@users.noreply.github.com> --- CHANGELOG.md | 42 ++++++++++++++++++- Cargo.toml | 8 ++-- README.md | 4 +- 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/4760.packaging.md | 1 - newsfragments/4766.fixed.md | 1 - newsfragments/4788.fixed.md | 4 -- newsfragments/4789.added.md | 3 -- newsfragments/4789.changed.md | 2 - newsfragments/4790.fixed.md | 1 - newsfragments/4791.fixed.md | 1 - newsfragments/4794.fixed.md | 1 - newsfragments/4800.fixed.md | 1 - newsfragments/4802.fixed.md | 1 - newsfragments/4806.fixed.md | 1 - newsfragments/4808.fixed.md | 1 - newsfragments/4814.fixed.md | 1 - newsfragments/4832.fixed.md | 1 - newsfragments/4833.added.md | 1 - newsfragments/4879.fixed.md | 1 - newsfragments/4902.fixed.md | 1 - newsfragments/4921.fixed.md | 1 - pyo3-build-config/Cargo.toml | 2 +- pyo3-ffi/Cargo.toml | 4 +- pyo3-ffi/README.md | 4 +- pyo3-macros-backend/Cargo.toml | 6 +-- pyo3-macros/Cargo.toml | 4 +- pyproject.toml | 2 +- tests/ui/reject_generics.stderr | 4 +- 33 files changed, 65 insertions(+), 49 deletions(-) delete mode 100644 newsfragments/4760.packaging.md delete mode 100644 newsfragments/4766.fixed.md delete mode 100644 newsfragments/4788.fixed.md delete mode 100644 newsfragments/4789.added.md delete mode 100644 newsfragments/4789.changed.md delete mode 100644 newsfragments/4790.fixed.md delete mode 100644 newsfragments/4791.fixed.md delete mode 100644 newsfragments/4794.fixed.md delete mode 100644 newsfragments/4800.fixed.md delete mode 100644 newsfragments/4802.fixed.md delete mode 100644 newsfragments/4806.fixed.md delete mode 100644 newsfragments/4808.fixed.md delete mode 100644 newsfragments/4814.fixed.md delete mode 100644 newsfragments/4832.fixed.md delete mode 100644 newsfragments/4833.added.md delete mode 100644 newsfragments/4879.fixed.md delete mode 100644 newsfragments/4902.fixed.md delete mode 100644 newsfragments/4921.fixed.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 668fbd23d43..b08db4d36d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,44 @@ To see unreleased changes, please see the [CHANGELOG on the main branch guide](h +## [0.23.5] - 2025-02-22 +### Packaging + +- Add support for PyPy3.11 [#4760](https://github.com/PyO3/pyo3/pull/4760) + +### Fixed + +- Fix thread-unsafe implementation of freelist pyclasses on the free-threaded build. [#4902](https://github.com/PyO3/pyo3/pull/4902) +- Re-enable a workaround for situations where CPython incorrectly does not add `__builtins__` to `__globals__` in code executed by `Python::py_run` (was removed in PyO3 0.23.0). [#4921](https://github.com/PyO3/pyo3/pull/4921) + +## [0.23.4] - 2025-01-10 + +### Added + +- Add `PyList::locked_for_each`, which uses a critical section to lock the list on the free-threaded build. [#4789](https://github.com/PyO3/pyo3/pull/4789) +- Add `pyo3_build_config::add_python_framework_link_args` build script API to set rpath when using macOS system Python. [#4833](https://github.com/PyO3/pyo3/pull/4833) + +### Changed + +- Use `datetime.fold` to distinguish ambiguous datetimes when converting to and from `chrono::DateTime` (rather than erroring). [#4791](https://github.com/PyO3/pyo3/pull/4791) +- Optimize PyList iteration on the free-threaded build. [#4789](https://github.com/PyO3/pyo3/pull/4789) + +### Fixed + +- Fix unnecessary internal `py.allow_threads` GIL-switch when attempting to access contents of a `PyErr` which originated from Python (could lead to unintended deadlocks). [#4766](https://github.com/PyO3/pyo3/pull/4766) +- Fix thread-unsafe access of dict internals in `BoundDictIterator` on the free-threaded build. [#4788](https://github.com/PyO3/pyo3/pull/4788) +* Fix unnecessary critical sections in `BoundDictIterator` on the free-threaded build. [#4788](https://github.com/PyO3/pyo3/pull/4788) +- Fix time-of-check to time-of-use issues with list iteration on the free-threaded build. [#4789](https://github.com/PyO3/pyo3/pull/4789) +- Fix `chrono::DateTime` to-Python conversion when `Tz` is `chrono_tz::Tz`. [#4790](https://github.com/PyO3/pyo3/pull/4790) +- Fix `#[pyclass]` not being able to be named `Probe`. [#4794](https://github.com/PyO3/pyo3/pull/4794) +- Fix not treating cross-compilation from x64 to aarch64 on Windows as a cross-compile. [#4800](https://github.com/PyO3/pyo3/pull/4800) +- Fix missing struct fields on GraalPy when subclassing builtin classes. [#4802](https://github.com/PyO3/pyo3/pull/4802) +- Fix generating import lib for PyPy when `abi3` feature is enabled. [#4806](https://github.com/PyO3/pyo3/pull/4806) +- Fix generating import lib for python3.13t when `abi3` feature is enabled. [#4808](https://github.com/PyO3/pyo3/pull/4808) +- Fix compile failure for raw identifiers like `r#box` in `derive(FromPyObject)`. [#4814](https://github.com/PyO3/pyo3/pull/4814) +- Fix compile failure for `#[pyclass]` enum variants with more than 12 fields. [#4832](https://github.com/PyO3/pyo3/pull/4832) + + ## [0.23.3] - 2024-12-03 ### Packaging @@ -2026,7 +2064,9 @@ Yanked - Initial release -[Unreleased]: https://github.com/pyo3/pyo3/compare/v0.23.3...HEAD +[Unreleased]: https://github.com/pyo3/pyo3/compare/v0.23.5...HEAD +[0.23.5]: https://github.com/pyo3/pyo3/compare/v0.23.4...v0.23.5 +[0.23.4]: https://github.com/pyo3/pyo3/compare/v0.23.3...v0.23.4 [0.23.3]: https://github.com/pyo3/pyo3/compare/v0.23.2...v0.23.3 [0.23.2]: https://github.com/pyo3/pyo3/compare/v0.23.1...v0.23.2 [0.23.1]: https://github.com/pyo3/pyo3/compare/v0.23.0...v0.23.1 diff --git a/Cargo.toml b/Cargo.toml index 1ab7dfedf76..3b4fa81e56b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3" -version = "0.23.3" +version = "0.23.5" description = "Bindings to Python interpreter" authors = ["PyO3 Project and Contributors "] readme = "README.md" @@ -21,10 +21,10 @@ memoffset = "0.9" once_cell = "1.13" # ffi bindings to the python interpreter, split into a separate crate so they can be used independently -pyo3-ffi = { path = "pyo3-ffi", version = "=0.23.3" } +pyo3-ffi = { path = "pyo3-ffi", version = "=0.23.5" } # support crates for macros feature -pyo3-macros = { path = "pyo3-macros", version = "=0.23.3", optional = true } +pyo3-macros = { path = "pyo3-macros", version = "=0.23.5", optional = true } indoc = { version = "2.0.1", optional = true } unindent = { version = "0.2.1", optional = true } @@ -68,7 +68,7 @@ static_assertions = "1.1.0" uuid = {version = "1.10.0", features = ["v4"] } [build-dependencies] -pyo3-build-config = { path = "pyo3-build-config", version = "=0.23.3", features = ["resolve-config"] } +pyo3-build-config = { path = "pyo3-build-config", version = "=0.23.5", features = ["resolve-config"] } [features] default = ["macros"] diff --git a/README.md b/README.md index a607cdaae94..5e7a14d8297 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,7 @@ name = "string_sum" crate-type = ["cdylib"] [dependencies] -pyo3 = { version = "0.23.3", features = ["extension-module"] } +pyo3 = { version = "0.23.5", features = ["extension-module"] } ``` **`src/lib.rs`** @@ -140,7 +140,7 @@ Start a new project with `cargo new` and add `pyo3` to the `Cargo.toml` like th ```toml [dependencies.pyo3] -version = "0.23.3" +version = "0.23.5" features = ["auto-initialize"] ``` diff --git a/examples/decorator/.template/pre-script.rhai b/examples/decorator/.template/pre-script.rhai index aa83bdd81b7..c403a167400 100644 --- a/examples/decorator/.template/pre-script.rhai +++ b/examples/decorator/.template/pre-script.rhai @@ -1,4 +1,4 @@ -variable::set("PYO3_VERSION", "0.23.3"); +variable::set("PYO3_VERSION", "0.23.5"); 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 aa83bdd81b7..c403a167400 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.23.3"); +variable::set("PYO3_VERSION", "0.23.5"); 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 dd76eff3b7b..f958e1da13e 100644 --- a/examples/plugin/.template/pre-script.rhai +++ b/examples/plugin/.template/pre-script.rhai @@ -1,4 +1,4 @@ -variable::set("PYO3_VERSION", "0.23.3"); +variable::set("PYO3_VERSION", "0.23.5"); 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 461282c2832..3e9f2a4a04d 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.23.3"); +variable::set("PYO3_VERSION", "0.23.5"); 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 aa83bdd81b7..c403a167400 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.23.3"); +variable::set("PYO3_VERSION", "0.23.5"); file::rename(".template/Cargo.toml", "Cargo.toml"); file::rename(".template/pyproject.toml", "pyproject.toml"); file::delete(".template"); diff --git a/newsfragments/4760.packaging.md b/newsfragments/4760.packaging.md deleted file mode 100644 index e7fe53099d1..00000000000 --- a/newsfragments/4760.packaging.md +++ /dev/null @@ -1 +0,0 @@ -add support for PyPy3.11 diff --git a/newsfragments/4766.fixed.md b/newsfragments/4766.fixed.md deleted file mode 100644 index 3f69e5d5f63..00000000000 --- a/newsfragments/4766.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Fix unnecessary internal `py.allow_threads` GIL-switch when attempting to access contents of a `PyErr` which originated from Python (could lead to unintended deadlocks). diff --git a/newsfragments/4788.fixed.md b/newsfragments/4788.fixed.md deleted file mode 100644 index 804cd60fd3d..00000000000 --- a/newsfragments/4788.fixed.md +++ /dev/null @@ -1,4 +0,0 @@ -* Fixed thread-unsafe access of dict internals in BoundDictIterator on the - free-threaded build. -* Avoided creating unnecessary critical sections in BoundDictIterator - implementation on the free-threaded build. diff --git a/newsfragments/4789.added.md b/newsfragments/4789.added.md deleted file mode 100644 index fab564a8962..00000000000 --- a/newsfragments/4789.added.md +++ /dev/null @@ -1,3 +0,0 @@ -* Added `PyList::locked_for_each`, which is equivalent to `PyList::for_each` on - the GIL-enabled build and uses a critical section to lock the list on the - free-threaded build, similar to `PyDict::locked_for_each`. diff --git a/newsfragments/4789.changed.md b/newsfragments/4789.changed.md deleted file mode 100644 index d20419e8f23..00000000000 --- a/newsfragments/4789.changed.md +++ /dev/null @@ -1,2 +0,0 @@ -* Operations that process a PyList via an iterator now use a critical section - on the free-threaded build to amortize synchronization cost and prevent race conditions. diff --git a/newsfragments/4790.fixed.md b/newsfragments/4790.fixed.md deleted file mode 100644 index 9b5e1bf60f1..00000000000 --- a/newsfragments/4790.fixed.md +++ /dev/null @@ -1 +0,0 @@ -fix chrono::DateTime intoPyObject conversion when `Tz` is `chrono_tz::Tz` diff --git a/newsfragments/4791.fixed.md b/newsfragments/4791.fixed.md deleted file mode 100644 index 2aab452eb51..00000000000 --- a/newsfragments/4791.fixed.md +++ /dev/null @@ -1 +0,0 @@ -use `datetime.fold` to distinguish ambiguous datetimes when converting to and from `chrono::DateTime` diff --git a/newsfragments/4794.fixed.md b/newsfragments/4794.fixed.md deleted file mode 100644 index 9076234a5b3..00000000000 --- a/newsfragments/4794.fixed.md +++ /dev/null @@ -1 +0,0 @@ -fix `#[pyclass]` could not be named `Probe` \ No newline at end of file diff --git a/newsfragments/4800.fixed.md b/newsfragments/4800.fixed.md deleted file mode 100644 index 615e622a963..00000000000 --- a/newsfragments/4800.fixed.md +++ /dev/null @@ -1 +0,0 @@ -fix: cross-compilation compatibility checks for Windows diff --git a/newsfragments/4802.fixed.md b/newsfragments/4802.fixed.md deleted file mode 100644 index 55d79c71734..00000000000 --- a/newsfragments/4802.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Fixed missing struct fields on GraalPy when subclassing builtin classes diff --git a/newsfragments/4806.fixed.md b/newsfragments/4806.fixed.md deleted file mode 100644 index b5f3d8a554d..00000000000 --- a/newsfragments/4806.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Fix generating import lib for PyPy when `abi3` feature is enabled. diff --git a/newsfragments/4808.fixed.md b/newsfragments/4808.fixed.md deleted file mode 100644 index 2e7c3a8a23c..00000000000 --- a/newsfragments/4808.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Fix generating import lib for python3.13t when `abi3` feature is enabled. diff --git a/newsfragments/4814.fixed.md b/newsfragments/4814.fixed.md deleted file mode 100644 index 6634efc2b9f..00000000000 --- a/newsfragments/4814.fixed.md +++ /dev/null @@ -1 +0,0 @@ -`derive(FromPyObject)` support raw identifiers like `r#box`. \ No newline at end of file diff --git a/newsfragments/4832.fixed.md b/newsfragments/4832.fixed.md deleted file mode 100644 index 13df6deae57..00000000000 --- a/newsfragments/4832.fixed.md +++ /dev/null @@ -1 +0,0 @@ -`#[pyclass]` complex enums support more than 12 variant fields. \ No newline at end of file diff --git a/newsfragments/4833.added.md b/newsfragments/4833.added.md deleted file mode 100644 index 4e1e0005305..00000000000 --- a/newsfragments/4833.added.md +++ /dev/null @@ -1 +0,0 @@ -Add `pyo3_build_config::add_python_framework_link_args` build script API to set rpath when using macOS system Python. diff --git a/newsfragments/4879.fixed.md b/newsfragments/4879.fixed.md deleted file mode 100644 index 2ad659b0786..00000000000 --- a/newsfragments/4879.fixed.md +++ /dev/null @@ -1 +0,0 @@ - * fixed spurious `test_double` failures. diff --git a/newsfragments/4902.fixed.md b/newsfragments/4902.fixed.md deleted file mode 100644 index e377ab018d7..00000000000 --- a/newsfragments/4902.fixed.md +++ /dev/null @@ -1 +0,0 @@ -* Fixed thread-unsafe implementation of freelist pyclasses on the free-threaded build. \ No newline at end of file diff --git a/newsfragments/4921.fixed.md b/newsfragments/4921.fixed.md deleted file mode 100644 index 86a91fd727a..00000000000 --- a/newsfragments/4921.fixed.md +++ /dev/null @@ -1 +0,0 @@ -* Reenabled a workaround for situations where CPython incorrectly does not add `__builtins__` to `__globals__` in code executed by `Python::py_run`. \ No newline at end of file diff --git a/pyo3-build-config/Cargo.toml b/pyo3-build-config/Cargo.toml index 2378d30757f..2eb0750cc0d 100644 --- a/pyo3-build-config/Cargo.toml +++ b/pyo3-build-config/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3-build-config" -version = "0.23.3" +version = "0.23.5" 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 ddbb489e2b9..3dd5a711eb6 100644 --- a/pyo3-ffi/Cargo.toml +++ b/pyo3-ffi/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3-ffi" -version = "0.23.3" +version = "0.23.5" description = "Python-API bindings for the PyO3 ecosystem" authors = ["PyO3 Project and Contributors "] keywords = ["pyo3", "python", "cpython", "ffi"] @@ -42,7 +42,7 @@ generate-import-lib = ["pyo3-build-config/python3-dll-a"] paste = "1" [build-dependencies] -pyo3-build-config = { path = "../pyo3-build-config", version = "=0.23.3", features = ["resolve-config"] } +pyo3-build-config = { path = "../pyo3-build-config", version = "=0.23.5", features = ["resolve-config"] } [lints] workspace = true diff --git a/pyo3-ffi/README.md b/pyo3-ffi/README.md index 5ae73122501..8224217c4e7 100644 --- a/pyo3-ffi/README.md +++ b/pyo3-ffi/README.md @@ -41,13 +41,13 @@ name = "string_sum" crate-type = ["cdylib"] [dependencies.pyo3-ffi] -version = "0.23.3" +version = "0.23.5" features = ["extension-module"] [build-dependencies] # This is only necessary if you need to configure your build based on # the Python version or the compile-time configuration for the interpreter. -pyo3_build_config = "0.23.3" +pyo3_build_config = "0.23.5" ``` If you need to use conditional compilation based on Python version or how diff --git a/pyo3-macros-backend/Cargo.toml b/pyo3-macros-backend/Cargo.toml index 38ec968d71d..fced6a5d287 100644 --- a/pyo3-macros-backend/Cargo.toml +++ b/pyo3-macros-backend/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3-macros-backend" -version = "0.23.3" +version = "0.23.5" description = "Code generation for PyO3 package" authors = ["PyO3 Project and Contributors "] keywords = ["pyo3", "python", "cpython", "ffi"] @@ -17,7 +17,7 @@ rust-version = "1.63" [dependencies] heck = "0.5" proc-macro2 = { version = "1.0.60", default-features = false } -pyo3-build-config = { path = "../pyo3-build-config", version = "=0.23.3", features = ["resolve-config"] } +pyo3-build-config = { path = "../pyo3-build-config", version = "=0.23.5", features = ["resolve-config"] } quote = { version = "1", default-features = false } [dependencies.syn] @@ -26,7 +26,7 @@ default-features = false features = ["derive", "parsing", "printing", "clone-impls", "full", "extra-traits"] [build-dependencies] -pyo3-build-config = { path = "../pyo3-build-config", version = "=0.23.3" } +pyo3-build-config = { path = "../pyo3-build-config", version = "=0.23.5" } [lints] workspace = true diff --git a/pyo3-macros/Cargo.toml b/pyo3-macros/Cargo.toml index a44758b37f5..3de7a556b0b 100644 --- a/pyo3-macros/Cargo.toml +++ b/pyo3-macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3-macros" -version = "0.23.3" +version = "0.23.5" description = "Proc macros for PyO3 package" authors = ["PyO3 Project and Contributors "] keywords = ["pyo3", "python", "cpython", "ffi"] @@ -22,7 +22,7 @@ experimental-async = ["pyo3-macros-backend/experimental-async"] proc-macro2 = { version = "1.0.60", default-features = false } quote = "1" syn = { version = "2", features = ["full", "extra-traits"] } -pyo3-macros-backend = { path = "../pyo3-macros-backend", version = "=0.23.3" } +pyo3-macros-backend = { path = "../pyo3-macros-backend", version = "=0.23.5" } [lints] workspace = true diff --git a/pyproject.toml b/pyproject.toml index 771e6f8a045..48a5e8a9747 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ [tool.towncrier] filename = "CHANGELOG.md" -version = "0.23.3" +version = "0.23.5" start_string = "\n" template = ".towncrier.template.md" title_format = "## [{version}] - {project_date}" diff --git a/tests/ui/reject_generics.stderr b/tests/ui/reject_generics.stderr index 47999f36275..850387aadd4 100644 --- a/tests/ui/reject_generics.stderr +++ b/tests/ui/reject_generics.stderr @@ -1,10 +1,10 @@ -error: #[pyclass] cannot have generic parameters. For an explanation, see https://pyo3.rs/v0.23.3/class.html#no-generic-parameters +error: #[pyclass] cannot have generic parameters. For an explanation, see https://pyo3.rs/v0.23.5/class.html#no-generic-parameters --> tests/ui/reject_generics.rs:4:25 | 4 | struct ClassWithGenerics { | ^ -error: #[pyclass] cannot have lifetime parameters. For an explanation, see https://pyo3.rs/v0.23.3/class.html#no-lifetime-parameters +error: #[pyclass] cannot have lifetime parameters. For an explanation, see https://pyo3.rs/v0.23.5/class.html#no-lifetime-parameters --> tests/ui/reject_generics.rs:9:27 | 9 | struct ClassWithLifetimes<'a> { From ffd7276b5616716db36516aea25d028391b382d6 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers <7943856+bschoenmaeckers@users.noreply.github.com> Date: Wed, 26 Feb 2025 18:10:56 +0100 Subject: [PATCH 454/495] Convert `PathBuf` & `Path` into python `pathlib.Path` instead of `PyString` (#4925) * Convert `PathBuf` into python `pathlib.Path` * Make sure `ToPyObject` & `IntoPy` convert to PyString * Add from pystring test --- newsfragments/4925.changed.md | 1 + pytests/tests/test_path.py | 8 +-- src/conversions/std/path.rs | 96 ++++++++++++++++++++++++----------- tests/test_datetime_import.rs | 2 +- 4 files changed, 71 insertions(+), 36 deletions(-) create mode 100644 newsfragments/4925.changed.md diff --git a/newsfragments/4925.changed.md b/newsfragments/4925.changed.md new file mode 100644 index 00000000000..1501e375c63 --- /dev/null +++ b/newsfragments/4925.changed.md @@ -0,0 +1 @@ +Convert `PathBuf` & `Path` into python `pathlib.Path` instead of `PyString` \ No newline at end of file diff --git a/pytests/tests/test_path.py b/pytests/tests/test_path.py index 21240187356..d1d6eb83924 100644 --- a/pytests/tests/test_path.py +++ b/pytests/tests/test_path.py @@ -7,21 +7,21 @@ def test_make_path(): p = rpath.make_path() - assert p == "/root" + assert p == pathlib.Path("/root") def test_take_pathbuf(): p = "/root" - assert rpath.take_pathbuf(p) == p + assert rpath.take_pathbuf(p) == pathlib.Path(p) def test_take_pathlib(): p = pathlib.Path("/root") - assert rpath.take_pathbuf(p) == str(p) + assert rpath.take_pathbuf(p) == p def test_take_pathlike(): - assert rpath.take_pathbuf(PathLike("/root")) == "/root" + assert rpath.take_pathbuf(PathLike("/root")) == pathlib.Path("/root") def test_take_invalid_pathlike(): diff --git a/src/conversions/std/path.rs b/src/conversions/std/path.rs index dc528ee3595..25765a8084e 100644 --- a/src/conversions/std/path.rs +++ b/src/conversions/std/path.rs @@ -1,13 +1,12 @@ use crate::conversion::IntoPyObject; use crate::ffi_ptr_ext::FfiPtrExt; use crate::instance::Bound; +use crate::sync::GILOnceCell; use crate::types::any::PyAnyMethods; -use crate::types::PyString; -use crate::{ffi, FromPyObject, PyAny, PyObject, PyResult, Python}; +use crate::{ffi, FromPyObject, IntoPyObjectExt, PyAny, PyErr, PyObject, PyResult, Python}; #[allow(deprecated)] use crate::{IntoPy, ToPyObject}; use std::borrow::Cow; -use std::convert::Infallible; use std::ffi::OsString; use std::path::{Path, PathBuf}; @@ -15,7 +14,7 @@ use std::path::{Path, PathBuf}; impl ToPyObject for Path { #[inline] fn to_object(&self, py: Python<'_>) -> PyObject { - self.into_pyobject(py).unwrap().into_any().unbind() + self.as_os_str().into_py_any(py).unwrap() } } @@ -33,25 +32,28 @@ impl FromPyObject<'_> for PathBuf { impl IntoPy for &Path { #[inline] fn into_py(self, py: Python<'_>) -> PyObject { - self.into_pyobject(py).unwrap().into_any().unbind() + self.to_object(py) } } impl<'py> IntoPyObject<'py> for &Path { - type Target = PyString; + type Target = PyAny; type Output = Bound<'py, Self::Target>; - type Error = Infallible; + type Error = PyErr; #[inline] fn into_pyobject(self, py: Python<'py>) -> Result { - self.as_os_str().into_pyobject(py) + static PY_PATH: GILOnceCell = GILOnceCell::new(); + PY_PATH + .import(py, "pathlib", "Path")? + .call((self.as_os_str(),), None) } } impl<'py> IntoPyObject<'py> for &&Path { - type Target = PyString; + type Target = PyAny; type Output = Bound<'py, Self::Target>; - type Error = Infallible; + type Error = PyErr; #[inline] fn into_pyobject(self, py: Python<'py>) -> Result { @@ -63,7 +65,7 @@ impl<'py> IntoPyObject<'py> for &&Path { impl ToPyObject for Cow<'_, Path> { #[inline] fn to_object(&self, py: Python<'_>) -> PyObject { - self.into_pyobject(py).unwrap().into_any().unbind() + (**self).to_object(py) } } @@ -71,29 +73,29 @@ impl ToPyObject for Cow<'_, Path> { impl IntoPy for Cow<'_, Path> { #[inline] fn into_py(self, py: Python<'_>) -> PyObject { - self.into_pyobject(py).unwrap().into_any().unbind() + (*self).to_object(py) } } impl<'py> IntoPyObject<'py> for Cow<'_, Path> { - type Target = PyString; + type Target = PyAny; type Output = Bound<'py, Self::Target>; - type Error = Infallible; + type Error = PyErr; #[inline] fn into_pyobject(self, py: Python<'py>) -> Result { - self.as_os_str().into_pyobject(py) + (*self).into_pyobject(py) } } impl<'py> IntoPyObject<'py> for &Cow<'_, Path> { - type Target = PyString; + type Target = PyAny; type Output = Bound<'py, Self::Target>; - type Error = Infallible; + type Error = PyErr; #[inline] fn into_pyobject(self, py: Python<'py>) -> Result { - self.as_os_str().into_pyobject(py) + (&**self).into_pyobject(py) } } @@ -101,7 +103,7 @@ impl<'py> IntoPyObject<'py> for &Cow<'_, Path> { impl ToPyObject for PathBuf { #[inline] fn to_object(&self, py: Python<'_>) -> PyObject { - self.into_pyobject(py).unwrap().into_any().unbind() + (**self).to_object(py) } } @@ -109,18 +111,18 @@ impl ToPyObject for PathBuf { impl IntoPy for PathBuf { #[inline] fn into_py(self, py: Python<'_>) -> PyObject { - self.into_pyobject(py).unwrap().into_any().unbind() + (*self).to_object(py) } } impl<'py> IntoPyObject<'py> for PathBuf { - type Target = PyString; + type Target = PyAny; type Output = Bound<'py, Self::Target>; - type Error = Infallible; + type Error = PyErr; #[inline] fn into_pyobject(self, py: Python<'py>) -> Result { - self.as_os_str().into_pyobject(py) + (&self).into_pyobject(py) } } @@ -128,25 +130,25 @@ impl<'py> IntoPyObject<'py> for PathBuf { impl IntoPy for &PathBuf { #[inline] fn into_py(self, py: Python<'_>) -> PyObject { - self.into_pyobject(py).unwrap().into_any().unbind() + (**self).to_object(py) } } impl<'py> IntoPyObject<'py> for &PathBuf { - type Target = PyString; + type Target = PyAny; type Output = Bound<'py, Self::Target>; - type Error = Infallible; + type Error = PyErr; #[inline] fn into_pyobject(self, py: Python<'py>) -> Result { - self.as_os_str().into_pyobject(py) + (&**self).into_pyobject(py) } } #[cfg(test)] mod tests { use crate::types::{PyAnyMethods, PyString, PyStringMethods}; - use crate::{BoundObject, IntoPyObject, Python}; + use crate::{IntoPyObject, IntoPyObjectExt, PyObject, Python}; use std::borrow::Cow; use std::fmt::Debug; use std::path::{Path, PathBuf}; @@ -180,10 +182,42 @@ mod tests { T: IntoPyObject<'py> + AsRef + Debug + Clone, T::Error: Debug, { - let pyobject = obj.clone().into_pyobject(py).unwrap().into_any(); - let pystring = pyobject.as_borrowed().downcast::().unwrap(); + let pyobject = obj.clone().into_bound_py_any(py).unwrap(); + let roundtripped_obj: PathBuf = pyobject.extract().unwrap(); + assert_eq!(obj.as_ref(), roundtripped_obj.as_path()); + } + let path = Path::new("Hello\0\n🐍"); + test_roundtrip::<&Path>(py, path); + test_roundtrip::>(py, Cow::Borrowed(path)); + test_roundtrip::>(py, Cow::Owned(path.to_path_buf())); + test_roundtrip::(py, path.to_path_buf()); + }); + } + + #[test] + fn test_from_pystring() { + Python::with_gil(|py| { + let path = "Hello\0\n🐍"; + let pystring = PyString::new(py, path); + let roundtrip: PathBuf = pystring.extract().unwrap(); + assert_eq!(roundtrip, Path::new(path)); + }); + } + + #[test] + #[allow(deprecated)] + fn test_intopy_string() { + use crate::IntoPy; + + Python::with_gil(|py| { + fn test_roundtrip(py: Python<'_>, obj: T) + where + T: IntoPy + AsRef + Debug + Clone, + { + let pyobject = obj.clone().into_py(py).into_bound(py); + let pystring = pyobject.downcast_exact::().unwrap(); assert_eq!(pystring.to_string_lossy(), obj.as_ref().to_string_lossy()); - let roundtripped_obj: PathBuf = pystring.extract().unwrap(); + let roundtripped_obj: PathBuf = pyobject.extract().unwrap(); assert_eq!(obj.as_ref(), roundtripped_obj.as_path()); } let path = Path::new("Hello\0\n🐍"); diff --git a/tests/test_datetime_import.rs b/tests/test_datetime_import.rs index cac4908bba6..295f7142c9d 100644 --- a/tests/test_datetime_import.rs +++ b/tests/test_datetime_import.rs @@ -18,7 +18,7 @@ fn test_bad_datetime_module_panic() { let sys = py.import("sys").unwrap(); sys.getattr("path") .unwrap() - .call_method1("insert", (0, tmpdir.path())) + .call_method1("insert", (0, tmpdir.path().as_os_str())) .unwrap(); // This should panic because the "datetime" module is empty From fe2d7f81d27705777925a46908090d62d5e7b7c2 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Wed, 26 Feb 2025 11:44:23 -0700 Subject: [PATCH 455/495] fix broken link in docs (#4940) --- guide/src/conversions/traits.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/guide/src/conversions/traits.md b/guide/src/conversions/traits.md index d6d29490dee..a06ddcc09a8 100644 --- a/guide/src/conversions/traits.md +++ b/guide/src/conversions/traits.md @@ -532,7 +532,7 @@ struct RustyStruct { ``` ### `IntoPyObject` -The ['IntoPyObject'] trait defines the to-python conversion for a Rust type. All types in PyO3 implement this trait, +The [`IntoPyObject`] trait defines the to-python conversion for a Rust type. All types in PyO3 implement this trait, as does a `#[pyclass]` which doesn't use `extends`. This trait defines a single method, `into_pyobject()`, which returns a [`Result`] with `Ok` and `Err` types depending on the input value. For convenience, there is a companion [`IntoPyObjectExt`] trait which adds methods such as `into_py_any()` which converts the `Ok` and `Err` types to commonly used types (in the case of `into_py_any()`, `Py` and `PyErr` respectively). From 1ced0a352dc9602402eb51f1f90f9adbe1b5c378 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Thu, 27 Feb 2025 14:34:25 -0700 Subject: [PATCH 456/495] Reduce runtime of slow test (#4946) --- src/types/mappingproxy.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/types/mappingproxy.rs b/src/types/mappingproxy.rs index fc28687c561..5a0b1537cb0 100644 --- a/src/types/mappingproxy.rs +++ b/src/types/mappingproxy.rs @@ -539,7 +539,7 @@ mod tests { #[test] fn iter_mappingproxy_nosegv() { Python::with_gil(|py| { - const LEN: usize = 10_000_000; + const LEN: usize = 1_000; let items = (0..LEN as u64).map(|i| (i, i * 2)); let dict = items.clone().into_py_dict(py).unwrap(); @@ -551,7 +551,7 @@ mod tests { let i: u64 = k.extract().unwrap(); sum += i; } - assert_eq!(sum, 49_999_995_000_000); + assert_eq!(sum, 499_500); }) } } From 1e2aae30811a585bc5edb8e80f22d2b2ef59e51f Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Thu, 27 Feb 2025 15:07:38 -0700 Subject: [PATCH 457/495] Avoid data races in BorrowFlag (#4948) * Avoid data races in BorrowFlag * add changelog entry --- newsfragments/4948.fixed.md | 1 + src/pycell/impl_.rs | 15 +++++++-------- 2 files changed, 8 insertions(+), 8 deletions(-) create mode 100644 newsfragments/4948.fixed.md diff --git a/newsfragments/4948.fixed.md b/newsfragments/4948.fixed.md new file mode 100644 index 00000000000..eca5be21e3b --- /dev/null +++ b/newsfragments/4948.fixed.md @@ -0,0 +1 @@ +* Fixed a thread safety issue in the runtime borrow checker used by mutable pyclass instances on the free-threaded build. \ No newline at end of file diff --git a/src/pycell/impl_.rs b/src/pycell/impl_.rs index 1b0724d8481..1b1344f8aa8 100644 --- a/src/pycell/impl_.rs +++ b/src/pycell/impl_.rs @@ -60,6 +60,7 @@ impl BorrowFlag { pub(crate) const UNUSED: usize = 0; const HAS_MUTABLE_BORROW: usize = usize::MAX; fn increment(&self) -> Result<(), PyBorrowError> { + // relaxed is OK because we will read the value again in the compare_exchange let mut value = self.0.load(Ordering::Relaxed); loop { if value == BorrowFlag::HAS_MUTABLE_BORROW { @@ -70,13 +71,13 @@ impl BorrowFlag { // last atomic load value, value + 1, - Ordering::Relaxed, + // reading the value is happens-after a previous write + // writing the new value is happens-after the previous read + Ordering::AcqRel, + // relaxed is OK here because we're going to try to read again Ordering::Relaxed, ) { Ok(..) => { - // value has been successfully incremented, we need an acquire fence - // so that data this borrow flag protects can be read safely in this thread - std::sync::atomic::fence(Ordering::Acquire); break Ok(()); } Err(changed_value) => { @@ -87,10 +88,8 @@ impl BorrowFlag { } } fn decrement(&self) { - // impossible to get into a bad state from here so relaxed - // ordering is fine, the decrement only needs to eventually - // be visible - self.0.fetch_sub(1, Ordering::Relaxed); + // relaxed load is OK but decrements must happen-before the next read + self.0.fetch_sub(1, Ordering::Release); } } From 8567b6e863a6427f6e7ac8dece0483f50d286d18 Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Thu, 27 Feb 2025 23:10:52 +0100 Subject: [PATCH 458/495] added `#[pyo3(rename_all = "...")]` container attribute for `#[derive(FromPyObject)]` (#4941) --- guide/src/conversions/traits.md | 4 ++ newsfragments/4941.added.md | 1 + pyo3-macros-backend/src/frompyobject.rs | 58 +++++++++++++++-- tests/test_frompyobject.rs | 85 +++++++++++++++++++++++++ tests/ui/invalid_frompy_derive.rs | 29 +++++++++ tests/ui/invalid_frompy_derive.stderr | 32 +++++++++- 6 files changed, 201 insertions(+), 8 deletions(-) create mode 100644 newsfragments/4941.added.md diff --git a/guide/src/conversions/traits.md b/guide/src/conversions/traits.md index a06ddcc09a8..00e2bd6bdcb 100644 --- a/guide/src/conversions/traits.md +++ b/guide/src/conversions/traits.md @@ -476,6 +476,10 @@ If the input is neither a string nor an integer, the error message will be: - changes the name of the failed variant in the generated error message in case of failure. - e.g. `pyo3("int")` reports the variant's type as `int`. - only supported for enum variants +- `pyo3(rename_all = "...")` + - renames all attributes/item keys according to the specified renaming rule + - Possible values are: "camelCase", "kebab-case", "lowercase", "PascalCase", "SCREAMING-KEBAB-CASE", "SCREAMING_SNAKE_CASE", "snake_case", "UPPERCASE". + - fields with an explicit renaming via `attribute(...)`/`item(...)` are not affected #### `#[derive(FromPyObject)]` Field Attributes - `pyo3(attribute)`, `pyo3(attribute("name"))` diff --git a/newsfragments/4941.added.md b/newsfragments/4941.added.md new file mode 100644 index 00000000000..7aca45df073 --- /dev/null +++ b/newsfragments/4941.added.md @@ -0,0 +1 @@ +add `#[pyo3(rename_all = "...")]` for `#[derive(FromPyObject)]` \ No newline at end of file diff --git a/pyo3-macros-backend/src/frompyobject.rs b/pyo3-macros-backend/src/frompyobject.rs index 0ffd13bdae8..ac7dbea681f 100644 --- a/pyo3-macros-backend/src/frompyobject.rs +++ b/pyo3-macros-backend/src/frompyobject.rs @@ -1,7 +1,8 @@ use crate::attributes::{ self, get_pyo3_options, CrateAttribute, DefaultAttribute, FromPyWithAttribute, + RenameAllAttribute, RenamingRule, }; -use crate::utils::Ctx; +use crate::utils::{self, Ctx}; use proc_macro2::TokenStream; use quote::{format_ident, quote, quote_spanned, ToTokens}; use syn::{ @@ -25,7 +26,7 @@ impl<'a> Enum<'a> { /// /// `data_enum` is the `syn` representation of the input enum, `ident` is the /// `Identifier` of the enum. - fn new(data_enum: &'a DataEnum, ident: &'a Ident) -> Result { + fn new(data_enum: &'a DataEnum, ident: &'a Ident, options: ContainerOptions) -> Result { ensure_spanned!( !data_enum.variants.is_empty(), ident.span() => "cannot derive FromPyObject for empty enum" @@ -34,9 +35,21 @@ impl<'a> Enum<'a> { .variants .iter() .map(|variant| { - let attrs = ContainerOptions::from_attrs(&variant.attrs)?; + let mut variant_options = ContainerOptions::from_attrs(&variant.attrs)?; + if let Some(rename_all) = &options.rename_all { + ensure_spanned!( + variant_options.rename_all.is_none(), + variant_options.rename_all.span() => "Useless variant `rename_all` - enum is already annotated with `rename_all" + ); + variant_options.rename_all = Some(rename_all.clone()); + + } let var_ident = &variant.ident; - Container::new(&variant.fields, parse_quote!(#ident::#var_ident), attrs) + Container::new( + &variant.fields, + parse_quote!(#ident::#var_ident), + variant_options, + ) }) .collect::>>()?; @@ -129,6 +142,7 @@ struct Container<'a> { path: syn::Path, ty: ContainerType<'a>, err_name: String, + rename_rule: Option, } impl<'a> Container<'a> { @@ -138,6 +152,10 @@ impl<'a> Container<'a> { fn new(fields: &'a Fields, path: syn::Path, options: ContainerOptions) -> Result { let style = match fields { Fields::Unnamed(unnamed) if !unnamed.unnamed.is_empty() => { + ensure_spanned!( + options.rename_all.is_none(), + options.rename_all.span() => "`rename_all` is useless on tuple structs and variants." + ); let mut tuple_fields = unnamed .unnamed .iter() @@ -213,6 +231,10 @@ impl<'a> Container<'a> { struct_fields.len() == 1, fields.span() => "transparent structs and variants can only have 1 field" ); + ensure_spanned!( + options.rename_all.is_none(), + options.rename_all.span() => "`rename_all` is not permitted on `transparent` structs and variants" + ); let field = struct_fields.pop().unwrap(); ensure_spanned!( field.getter.is_none(), @@ -236,6 +258,7 @@ impl<'a> Container<'a> { path, ty: style, err_name, + rename_rule: options.rename_all.map(|v| v.value.rule), }; Ok(v) } @@ -359,7 +382,11 @@ impl<'a> Container<'a> { quote!(#pyo3_path::types::PyAnyMethods::getattr(obj, #pyo3_path::intern!(obj.py(), #name))) } FieldGetter::GetAttr(None) => { - quote!(#pyo3_path::types::PyAnyMethods::getattr(obj, #pyo3_path::intern!(obj.py(), #field_name))) + let name = self + .rename_rule + .map(|rule| utils::apply_renaming_rule(rule, &field_name)); + let name = name.as_deref().unwrap_or(&field_name); + quote!(#pyo3_path::types::PyAnyMethods::getattr(obj, #pyo3_path::intern!(obj.py(), #name))) } FieldGetter::GetItem(Some(syn::Lit::Str(key))) => { quote!(#pyo3_path::types::PyAnyMethods::get_item(obj, #pyo3_path::intern!(obj.py(), #key))) @@ -368,7 +395,11 @@ impl<'a> Container<'a> { quote!(#pyo3_path::types::PyAnyMethods::get_item(obj, #key)) } FieldGetter::GetItem(None) => { - quote!(#pyo3_path::types::PyAnyMethods::get_item(obj, #pyo3_path::intern!(obj.py(), #field_name))) + let name = self + .rename_rule + .map(|rule| utils::apply_renaming_rule(rule, &field_name)); + let name = name.as_deref().unwrap_or(&field_name); + quote!(#pyo3_path::types::PyAnyMethods::get_item(obj, #pyo3_path::intern!(obj.py(), #name))) } }; let extractor = if let Some(FromPyWithAttribute { @@ -418,6 +449,8 @@ struct ContainerOptions { annotation: Option, /// Change the path for the pyo3 crate krate: Option, + /// Converts the field idents according to the [RenamingRule] before extraction + rename_all: Option, } /// Attributes for deriving FromPyObject scoped on containers. @@ -430,6 +463,8 @@ enum ContainerPyO3Attribute { ErrorAnnotation(LitStr), /// Change the path for the pyo3 crate Crate(CrateAttribute), + /// Converts the field idents according to the [RenamingRule] before extraction + RenameAll(RenameAllAttribute), } impl Parse for ContainerPyO3Attribute { @@ -447,6 +482,8 @@ impl Parse for ContainerPyO3Attribute { input.parse().map(ContainerPyO3Attribute::ErrorAnnotation) } else if lookahead.peek(Token![crate]) { input.parse().map(ContainerPyO3Attribute::Crate) + } else if lookahead.peek(attributes::kw::rename_all) { + input.parse().map(ContainerPyO3Attribute::RenameAll) } else { Err(lookahead.error()) } @@ -489,6 +526,13 @@ impl ContainerOptions { ); options.krate = Some(path); } + ContainerPyO3Attribute::RenameAll(rename_all) => { + ensure_spanned!( + options.rename_all.is_none(), + rename_all.span() => "`rename_all` may only be provided once" + ); + options.rename_all = Some(rename_all); + } } } } @@ -658,7 +702,7 @@ pub fn build_derive_from_pyobject(tokens: &DeriveInput) -> Result { bail_spanned!(tokens.span() => "`transparent` or `annotation` is not supported \ at top level for enums"); } - let en = Enum::new(en, &tokens.ident)?; + let en = Enum::new(en, &tokens.ident, options)?; en.build(ctx) } syn::Data::Struct(st) => { diff --git a/tests/test_frompyobject.rs b/tests/test_frompyobject.rs index d72a215814c..ad0a2d4f3d9 100644 --- a/tests/test_frompyobject.rs +++ b/tests/test_frompyobject.rs @@ -331,6 +331,91 @@ fn test_transparent_tuple_error_message() { }); } +#[pyclass] +struct RenameAllCls {} + +#[pymethods] +impl RenameAllCls { + #[getter] + #[pyo3(name = "someField")] + fn some_field(&self) -> &'static str { + "Foo" + } + + #[getter] + #[pyo3(name = "customNumber")] + fn custom_number(&self) -> i32 { + 42 + } + + fn __getitem__(&self, key: &str) -> PyResult { + match key { + "otherField" => Ok(42.0), + _ => Err(pyo3::exceptions::PyKeyError::new_err("foo")), + } + } +} + +#[test] +fn test_struct_rename_all() { + #[derive(FromPyObject)] + #[pyo3(rename_all = "camelCase")] + struct RenameAll { + some_field: String, + #[pyo3(item)] + other_field: f32, + #[pyo3(attribute("customNumber"))] + custom_name: i32, + } + + Python::with_gil(|py| { + let RenameAll { + some_field, + other_field, + custom_name, + } = RenameAllCls {} + .into_pyobject(py) + .unwrap() + .extract() + .unwrap(); + + assert_eq!(some_field, "Foo"); + assert_eq!(other_field, 42.0); + assert_eq!(custom_name, 42); + }); +} + +#[test] +fn test_enum_rename_all() { + #[derive(FromPyObject)] + #[pyo3(rename_all = "camelCase")] + enum RenameAll { + Foo { + some_field: String, + #[pyo3(item)] + other_field: f32, + #[pyo3(attribute("customNumber"))] + custom_name: i32, + }, + } + + Python::with_gil(|py| { + let RenameAll::Foo { + some_field, + other_field, + custom_name, + } = RenameAllCls {} + .into_pyobject(py) + .unwrap() + .extract() + .unwrap(); + + assert_eq!(some_field, "Foo"); + assert_eq!(other_field, 42.0); + assert_eq!(custom_name, 42); + }); +} + #[derive(Debug, FromPyObject)] pub enum Foo<'py> { TupleVar(usize, String), diff --git a/tests/ui/invalid_frompy_derive.rs b/tests/ui/invalid_frompy_derive.rs index d3a778e686b..08d7a41b392 100644 --- a/tests/ui/invalid_frompy_derive.rs +++ b/tests/ui/invalid_frompy_derive.rs @@ -230,4 +230,33 @@ enum EnumVariantWithOnlyDefaultValues { #[derive(FromPyObject)] struct NamedTuplesWithDefaultValues(#[pyo3(default)] String); +#[derive(FromPyObject)] +#[pyo3(rename_all = "camelCase", rename_all = "kebab-case")] +struct MultipleRenames { + snake_case: String, +} + +#[derive(FromPyObject)] +#[pyo3(rename_all = "camelCase")] +struct RenameAllTuple(String); + +#[derive(FromPyObject)] +enum RenameAllEnum { + #[pyo3(rename_all = "camelCase")] + Tuple(String), +} + +#[derive(FromPyObject)] +#[pyo3(transparent, rename_all = "camelCase")] +struct RenameAllTransparent { + inner: String, +} + +#[derive(FromPyObject)] +#[pyo3(rename_all = "camelCase")] +enum UselessRenameAllEnum { + #[pyo3(rename_all = "camelCase")] + Tuple { inner_field: String }, +} + fn main() {} diff --git a/tests/ui/invalid_frompy_derive.stderr b/tests/ui/invalid_frompy_derive.stderr index 5b8c1fc718b..59bc69ffaaf 100644 --- a/tests/ui/invalid_frompy_derive.stderr +++ b/tests/ui/invalid_frompy_derive.stderr @@ -132,7 +132,7 @@ error: only one of `attribute` or `item` can be provided 118 | #[pyo3(item, attribute)] | ^ -error: expected one of: `transparent`, `from_item_all`, `annotation`, `crate` +error: expected one of: `transparent`, `from_item_all`, `annotation`, `crate`, `rename_all` --> tests/ui/invalid_frompy_derive.rs:123:8 | 123 | #[pyo3(unknown = "should not work")] @@ -249,3 +249,33 @@ error: `default` is not permitted on tuple struct elements. | 231 | struct NamedTuplesWithDefaultValues(#[pyo3(default)] String); | ^ + +error: `rename_all` may only be provided once + --> tests/ui/invalid_frompy_derive.rs:234:34 + | +234 | #[pyo3(rename_all = "camelCase", rename_all = "kebab-case")] + | ^^^^^^^^^^ + +error: `rename_all` is useless on tuple structs and variants. + --> tests/ui/invalid_frompy_derive.rs:240:8 + | +240 | #[pyo3(rename_all = "camelCase")] + | ^^^^^^^^^^ + +error: `rename_all` is useless on tuple structs and variants. + --> tests/ui/invalid_frompy_derive.rs:245:12 + | +245 | #[pyo3(rename_all = "camelCase")] + | ^^^^^^^^^^ + +error: `rename_all` is not permitted on `transparent` structs and variants + --> tests/ui/invalid_frompy_derive.rs:250:21 + | +250 | #[pyo3(transparent, rename_all = "camelCase")] + | ^^^^^^^^^^ + +error: Useless variant `rename_all` - enum is already annotated with `rename_all + --> tests/ui/invalid_frompy_derive.rs:258:12 + | +258 | #[pyo3(rename_all = "camelCase")] + | ^^^^^^^^^^ From 72f9e75e1bb4e348342b4619dc30862ca5f1c43d Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Thu, 27 Feb 2025 16:52:02 -0700 Subject: [PATCH 459/495] add a MutexExt trait to avoid deadlocks using Mutex::lock() (#4934) * add a MutexExt trait to avoid deadlocks using Mutex::lock() * add changelog entry * apply review suggestions * replace ThreadStateGuard with SuspendGIL * use try_lock before attempting a blocking lock * collapse unnecessary match Co-authored-by: David Hewitt --------- Co-authored-by: David Hewitt --- guide/src/class/thread-safety.md | 4 +- guide/src/free-threading.md | 15 ++-- newsfragments/4934.added.md | 1 + src/sealed.rs | 1 + src/sync.rs | 148 ++++++++++++++++++++++++++----- 5 files changed, 135 insertions(+), 34 deletions(-) create mode 100644 newsfragments/4934.added.md diff --git a/guide/src/class/thread-safety.md b/guide/src/class/thread-safety.md index 841ee6ae2db..55c2a3caca8 100644 --- a/guide/src/class/thread-safety.md +++ b/guide/src/class/thread-safety.md @@ -101,8 +101,10 @@ impl MyClass { } ``` +If you need to lock around state stored in the Python interpreter or otherwise call into the Python C API while a lock is held, you might find the `MutexExt` trait useful. It provides a `lock_py_attached` method for `std::sync::Mutex` that avoids deadlocks with the GIL or other global synchronization events in the interpreter. + ### Wrapping unsynchronized data In some cases, the data structures stored within a `#[pyclass]` may themselves not be thread-safe. Rust will therefore not implement `Send` and `Sync` on the `#[pyclass]` type. -To achieve thread-safety, a manual `Send` and `Sync` implementation is required which is `unsafe` and should only be done following careful review of the soundness of the implementation. Doing this for PyO3 types is no different than for any other Rust code, [the Rustonomicon](https://doc.rust-lang.org/nomicon/send-and-sync.html) has a great discussion on this. +To achieve thread-safety, a manual `Send` and `Sync` implementation is required which is `unsafe` and should only be done following careful review of the soundness of the implementation. Doing this for PyO3 types is no different than for any other Rust code, [the Rustonomicon](https://doc.rust-lang.org/nomicon/send-and-sync.html) has a great discussion on this. \ No newline at end of file diff --git a/guide/src/free-threading.md b/guide/src/free-threading.md index 8ee9a2e100e..ffb95d240a1 100644 --- a/guide/src/free-threading.md +++ b/guide/src/free-threading.md @@ -387,18 +387,15 @@ Python::with_gil(|py| { # } ``` -If you are executing arbitrary Python code while holding the lock, then you will -need to use conditional compilation to use [`GILProtected`] on GIL-enabled Python -builds and mutexes otherwise. If your use of [`GILProtected`] does not guard the -execution of arbitrary Python code or use of the CPython C API, then conditional -compilation is likely unnecessary since [`GILProtected`] was not needed in the -first place and instead Rust mutexes or atomics should be preferred. Python 3.13 -introduces [`PyMutex`](https://docs.python.org/3/c-api/init.html#c.PyMutex), -which releases the GIL while the waiting for the lock, so that is another option -if you only need to support newer Python versions. +If you are executing arbitrary Python code while holding the lock, then you +should import the [`MutexExt`] trait and use the `lock_py_attached` method +instead of `lock`. This ensures that global synchronization events started by +the Python runtime can proceed, avoiding possible deadlocks with the +interpreter. [`GILOnceCell`]: {{#PYO3_DOCS_URL}}/pyo3/sync/struct.GILOnceCell.html [`GILProtected`]: https://docs.rs/pyo3/0.22/pyo3/sync/struct.GILProtected.html +[`MutexExt`]: {{#PYO3_DOCS_URL}}/pyo3/sync/trait.MutexExt.html [`Once`]: https://doc.rust-lang.org/stable/std/sync/struct.Once.html [`Once::call_once`]: https://doc.rust-lang.org/stable/std/sync/struct.Once.html#tymethod.call_once [`Once::call_once_force`]: https://doc.rust-lang.org/stable/std/sync/struct.Once.html#tymethod.call_once_force diff --git a/newsfragments/4934.added.md b/newsfragments/4934.added.md new file mode 100644 index 00000000000..716e243097d --- /dev/null +++ b/newsfragments/4934.added.md @@ -0,0 +1 @@ +* Added `MutextExt`, an extension trait to avoid deadlocks with the GIL while locking a `std::sync::Mutex`. \ No newline at end of file diff --git a/src/sealed.rs b/src/sealed.rs index 0a2846b134a..2c715468047 100644 --- a/src/sealed.rs +++ b/src/sealed.rs @@ -55,3 +55,4 @@ impl Sealed for PyNativeTypeInitializer {} impl Sealed for PyClassInitializer {} impl Sealed for std::sync::Once {} +impl Sealed for std::sync::Mutex {} diff --git a/src/sync.rs b/src/sync.rs index 0845eaf8cec..5dae1744584 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -5,7 +5,7 @@ //! //! [PEP 703]: https://peps.python.org/pep-703/ use crate::{ - ffi, + gil::SuspendGIL, sealed::Sealed, types::{any::PyAnyMethods, PyAny, PyString}, Bound, Py, PyResult, PyTypeCheck, Python, @@ -498,7 +498,7 @@ pub trait OnceExt: Sealed { fn call_once_force_py_attached(&self, py: Python<'_>, f: impl FnOnce(&OnceState)); } -// Extension trait for [`std::sync::OnceLock`] which helps avoid deadlocks between the Python +/// Extension trait for [`std::sync::OnceLock`] which helps avoid deadlocks between the Python /// interpreter and initialization with the `OnceLock`. #[cfg(rustc_has_once_lock)] pub trait OnceLockExt: once_lock_ext_sealed::Sealed { @@ -516,12 +516,20 @@ pub trait OnceLockExt: once_lock_ext_sealed::Sealed { F: FnOnce() -> T; } -struct Guard(*mut crate::ffi::PyThreadState); - -impl Drop for Guard { - fn drop(&mut self) { - unsafe { ffi::PyEval_RestoreThread(self.0) }; - } +/// Extension trait for [`std::sync::Mutex`] which helps avoid deadlocks between +/// the Python interpreter and acquiring the `Mutex`. +pub trait MutexExt: Sealed { + /// Lock this `Mutex` in a manner that cannot deadlock with the Python interpreter. + /// + /// Before attempting to lock the mutex, this function detaches from the + /// Python runtime. When the lock is acquired, it re-attaches to the Python + /// runtime before returning the `LockResult`. This avoids deadlocks between + /// the GIL and other global synchronization events triggered by the Python + /// interpreter. + fn lock_py_attached( + &self, + py: Python<'_>, + ) -> std::sync::LockResult>; } impl OnceExt for Once { @@ -557,14 +565,41 @@ impl OnceLockExt for std::sync::OnceLock { } } +impl MutexExt for std::sync::Mutex { + fn lock_py_attached( + &self, + _py: Python<'_>, + ) -> std::sync::LockResult> { + // If try_lock is successful or returns a poisoned mutex, return them so + // the caller can deal with them. Otherwise we need to use blocking + // lock, which requires detaching from the Python runtime to avoid + // possible deadlocks. + match self.try_lock() { + Ok(inner) => return Ok(inner), + Err(std::sync::TryLockError::Poisoned(inner)) => { + return std::sync::LockResult::Err(inner) + } + Err(std::sync::TryLockError::WouldBlock) => {} + } + // SAFETY: detach from the runtime right before a possibly blocking call + // then reattach when the blocking call completes and before calling + // into the C API. + let ts_guard = unsafe { SuspendGIL::new() }; + let res = self.lock(); + drop(ts_guard); + res + } +} + #[cold] fn init_once_py_attached(once: &Once, _py: Python<'_>, f: F) where F: FnOnce() -> T, { - // Safety: we are currently attached to the GIL, and we expect to block. We will save - // the current thread state and restore it as soon as we are done blocking. - let ts_guard = Guard(unsafe { ffi::PyEval_SaveThread() }); + // SAFETY: detach from the runtime right before a possibly blocking call + // then reattach when the blocking call completes and before calling + // into the C API. + let ts_guard = unsafe { SuspendGIL::new() }; once.call_once(move || { drop(ts_guard); @@ -577,9 +612,10 @@ fn init_once_force_py_attached(once: &Once, _py: Python<'_>, f: F) where F: FnOnce(&OnceState) -> T, { - // Safety: we are currently attached to the GIL, and we expect to block. We will save - // the current thread state and restore it as soon as we are done blocking. - let ts_guard = Guard(unsafe { ffi::PyEval_SaveThread() }); + // SAFETY: detach from the runtime right before a possibly blocking call + // then reattach when the blocking call completes and before calling + // into the C API. + let ts_guard = unsafe { SuspendGIL::new() }; once.call_once_force(move |state| { drop(ts_guard); @@ -597,8 +633,10 @@ fn init_once_lock_py_attached<'a, F, T>( where F: FnOnce() -> T, { - // SAFETY: we are currently attached to a Python thread - let ts_guard = Guard(unsafe { ffi::PyEval_SaveThread() }); + // SAFETY: detach from the runtime right before a possibly blocking call + // then reattach when the blocking call completes and before calling + // into the C API. + let ts_guard = unsafe { SuspendGIL::new() }; // this trait is guarded by a rustc version config // so clippy's MSRV check is wrong @@ -618,6 +656,19 @@ mod tests { use super::*; use crate::types::{PyDict, PyDictMethods}; + #[cfg(not(target_arch = "wasm32"))] + use std::sync::Mutex; + #[cfg(not(target_arch = "wasm32"))] + #[cfg(feature = "macros")] + use std::sync::{ + atomic::{AtomicBool, Ordering}, + Barrier, + }; + + #[cfg(not(target_arch = "wasm32"))] + #[cfg(feature = "macros")] + #[crate::pyclass(crate = "crate")] + struct BoolWrapper(AtomicBool); #[test] fn test_intern() { @@ -692,16 +743,8 @@ mod tests { #[cfg(not(target_arch = "wasm32"))] // We are building wasm Python with pthreads disabled #[test] fn test_critical_section() { - use std::sync::{ - atomic::{AtomicBool, Ordering}, - Barrier, - }; - let barrier = Barrier::new(2); - #[crate::pyclass(crate = "crate")] - struct BoolWrapper(AtomicBool); - let bool_wrapper = Python::with_gil(|py| -> Py { Py::new(py, BoolWrapper(AtomicBool::new(false))).unwrap() }); @@ -781,4 +824,61 @@ mod tests { }); assert_eq!(cell.get(), Some(&12345)); } + + #[cfg(feature = "macros")] + #[cfg(not(target_arch = "wasm32"))] // We are building wasm Python with pthreads disabled + #[test] + fn test_mutex_ext() { + let barrier = Barrier::new(2); + + let mutex = Python::with_gil(|py| -> Mutex> { + Mutex::new(Py::new(py, BoolWrapper(AtomicBool::new(false))).unwrap()) + }); + + std::thread::scope(|s| { + s.spawn(|| { + Python::with_gil(|py| { + let b = mutex.lock_py_attached(py).unwrap(); + barrier.wait(); + // sleep to ensure the other thread actually blocks + std::thread::sleep(std::time::Duration::from_millis(10)); + (*b).bind(py).borrow().0.store(true, Ordering::Release); + drop(b); + }); + }); + s.spawn(|| { + barrier.wait(); + Python::with_gil(|py| { + // blocks until the other thread releases the lock + let b = mutex.lock_py_attached(py).unwrap(); + assert!((*b).bind(py).borrow().0.load(Ordering::Acquire)); + }); + }); + }); + } + + #[cfg(not(target_arch = "wasm32"))] // We are building wasm Python with pthreads disabled + #[test] + fn test_mutex_ext_poison() { + let mutex = Mutex::new(42); + + std::thread::scope(|s| { + let lock_result = s.spawn(|| { + Python::with_gil(|py| { + let _unused = mutex.lock_py_attached(py); + panic!(); + }); + }); + assert!(lock_result.join().is_err()); + assert!(mutex.is_poisoned()); + }); + let guard = Python::with_gil(|py| { + // recover from the poisoning + match mutex.lock_py_attached(py) { + Ok(guard) => guard, + Err(poisoned) => poisoned.into_inner(), + } + }); + assert!(*guard == 42); + } } From 8e90311a91d372a8521b35561186fd1a4baaf5b6 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Fri, 28 Feb 2025 03:44:05 -0700 Subject: [PATCH 460/495] Don't use gc.get_objects on the free-threaded build (#4938) --- tests/test_gc.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/test_gc.rs b/tests/test_gc.rs index eb2cb34e1d7..a88b9a21f91 100644 --- a/tests/test_gc.rs +++ b/tests/test_gc.rs @@ -4,6 +4,7 @@ use pyo3::class::PyTraverseError; use pyo3::class::PyVisit; use pyo3::ffi; use pyo3::prelude::*; +#[cfg(not(Py_GIL_DISABLED))] use pyo3::py_run; #[cfg(not(target_arch = "wasm32"))] use std::cell::Cell; @@ -182,6 +183,10 @@ fn test_cycle_clear() { inst.borrow_mut().cycle = Some(inst.clone().into_any().unbind()); + // gc.get_objects can create references to partially initialized objects, + // leading to races on the free-threaded build. + // see https://github.com/python/cpython/issues/130421#issuecomment-2682924142 + #[cfg(not(Py_GIL_DISABLED))] py_run!(py, inst, "import gc; assert inst in gc.get_objects()"); check.assert_not_dropped(); inst.as_ptr() From 2712640ac3e19087c288e12809d397a108c753a6 Mon Sep 17 00:00:00 2001 From: Yoav Alon <65133955+yoav-orca@users.noreply.github.com> Date: Fri, 28 Feb 2025 12:54:27 +0200 Subject: [PATCH 461/495] Add functions for accessing frame objects in Python C-API for versions 3.9 and 3.10 (#4866) --- newsfragments/4866.added.md | 1 + pyo3-ffi/src/cpython/frameobject.rs | 3 ++- pyo3-ffi/src/pystate.rs | 9 ++++++++- 3 files changed, 11 insertions(+), 2 deletions(-) create mode 100644 newsfragments/4866.added.md diff --git a/newsfragments/4866.added.md b/newsfragments/4866.added.md new file mode 100644 index 00000000000..2d41123342b --- /dev/null +++ b/newsfragments/4866.added.md @@ -0,0 +1 @@ +Exposing PyThreadState_GetFrame and PyFrame_GetBack. diff --git a/pyo3-ffi/src/cpython/frameobject.rs b/pyo3-ffi/src/cpython/frameobject.rs index 6d2346f9288..e9b9c183f37 100644 --- a/pyo3-ffi/src/cpython/frameobject.rs +++ b/pyo3-ffi/src/cpython/frameobject.rs @@ -89,7 +89,8 @@ extern "C" { pub fn PyFrame_FastToLocals(f: *mut PyFrameObject); // skipped _PyFrame_DebugMallocStats - // skipped PyFrame_GetBack + #[cfg(all(Py_3_9, not(PyPy)))] + pub fn PyFrame_GetBack(f: *mut PyFrameObject) -> *mut PyFrameObject; #[cfg(not(Py_3_9))] pub fn PyFrame_ClearFreeList() -> c_int; diff --git a/pyo3-ffi/src/pystate.rs b/pyo3-ffi/src/pystate.rs index 23aeea3a1de..0c062160ccc 100644 --- a/pyo3-ffi/src/pystate.rs +++ b/pyo3-ffi/src/pystate.rs @@ -1,3 +1,5 @@ +#[cfg(all(Py_3_10, not(PyPy), not(Py_LIMITED_API)))] +use crate::frameobject::PyFrameObject; use crate::moduleobject::PyModuleDef; use crate::object::PyObject; use std::os::raw::c_int; @@ -63,9 +65,14 @@ extern "C" { } // skipped non-limited / 3.9 PyThreadState_GetInterpreter -// skipped non-limited / 3.9 PyThreadState_GetFrame // skipped non-limited / 3.9 PyThreadState_GetID +extern "C" { + // PyThreadState_GetFrame + #[cfg(all(Py_3_10, not(PyPy), not(Py_LIMITED_API)))] + pub fn PyThreadState_GetFrame(arg1: *mut PyThreadState) -> *mut PyFrameObject; +} + #[repr(C)] #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum PyGILState_STATE { From 5e95eddd0eda20e8d27d319f1836b0b1d07154a1 Mon Sep 17 00:00:00 2001 From: Slvr <30467496+SilverBzH@users.noreply.github.com> Date: Fri, 28 Feb 2025 12:12:31 -0800 Subject: [PATCH 462/495] Allow parsing exotic python version (#4949) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * python version: allow to parse exotic python version On specific linux disto, the python version output can be exotic. For example, in my machine I got the following: ``` ❯ python --version Python 3.11.3+chromium.29 ``` Which can lead to not desired end of program: ``` from cryptography.hazmat.bindings._rust import openssl as rust_openssl pyo3_runtime.PanicException: called `Result::unwrap()` on an `Err` value: "Python version string has too many parts" ``` * python version: use splitn instead of split * add test to check if the suffix is properly capture * add newsfragment "fixed" entry for the changelog --- newsfragments/4949.fixed.md | 1 + src/version.rs | 12 ++++++++---- 2 files changed, 9 insertions(+), 4 deletions(-) create mode 100644 newsfragments/4949.fixed.md diff --git a/newsfragments/4949.fixed.md b/newsfragments/4949.fixed.md new file mode 100644 index 00000000000..4a24b1462ac --- /dev/null +++ b/newsfragments/4949.fixed.md @@ -0,0 +1 @@ +Allow parsing exotic python version diff --git a/src/version.rs b/src/version.rs index e241c02c92c..5c060d79daf 100644 --- a/src/version.rs +++ b/src/version.rs @@ -40,13 +40,10 @@ impl<'a> PythonVersionInfo<'a> { } } - let mut parts = version_number_str.split('.'); + let mut parts = version_number_str.splitn(3, '.'); let major_str = parts.next().ok_or("Python major version missing")?; let minor_str = parts.next().ok_or("Python minor version missing")?; let patch_str = parts.next(); - if parts.next().is_some() { - return Err("Python version string has too many parts"); - }; let major = major_str .parse() @@ -139,5 +136,12 @@ mod test { assert!(PythonVersionInfo::from_str("3.5+").unwrap() == (3, 5)); assert!(PythonVersionInfo::from_str("3.5.2a1+").unwrap() < (3, 6)); assert!(PythonVersionInfo::from_str("3.5.2a1+").unwrap() > (3, 4)); + assert!(PythonVersionInfo::from_str("3.11.3+chromium.29").unwrap() >= (3, 11, 3)); + assert_eq!( + PythonVersionInfo::from_str("3.11.3+chromium.29") + .unwrap() + .suffix, + Some("+chromium.29") + ); } } From c3f8b50a8ec223112a8e1eb17a14f1fd04516a57 Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Sat, 1 Mar 2025 11:45:29 +0100 Subject: [PATCH 463/495] introduce `into_py_with` for `#[derive(IntoPyObject, IntoPyObjectRef)]` (#4850) * introduce `into_py_with`/`into_py_with_ref` for `#[derive(IntoPyObject, IntoPyObjectRef)]` * unify to `into_py_with` by taking a `Cow<'_, T>` * refine docs Co-authored-by: David Hewitt --------- Co-authored-by: David Hewitt --- guide/src/conversions/traits.md | 27 ++++++++ newsfragments/4850.added.md | 1 + pyo3-macros-backend/src/attributes.rs | 2 + pyo3-macros-backend/src/intopyobject.rs | 83 ++++++++++++++++++++----- tests/test_compile_error.rs | 2 + tests/test_intopyobject.rs | 67 +++++++++++++++++--- tests/ui/invalid_intopy_derive.rs | 32 ++++++++++ tests/ui/invalid_intopy_derive.stderr | 30 +++++++++ tests/ui/invalid_intopy_with.rs | 13 ++++ tests/ui/invalid_intopy_with.stderr | 23 +++++++ 10 files changed, 257 insertions(+), 23 deletions(-) mode change 100644 => 100755 guide/src/conversions/traits.md create mode 100644 newsfragments/4850.added.md mode change 100644 => 100755 pyo3-macros-backend/src/attributes.rs mode change 100644 => 100755 pyo3-macros-backend/src/intopyobject.rs mode change 100644 => 100755 tests/test_intopyobject.rs mode change 100644 => 100755 tests/ui/invalid_intopy_derive.rs create mode 100755 tests/ui/invalid_intopy_with.rs create mode 100755 tests/ui/invalid_intopy_with.stderr diff --git a/guide/src/conversions/traits.md b/guide/src/conversions/traits.md old mode 100644 new mode 100755 index 00e2bd6bdcb..43041cda1b8 --- a/guide/src/conversions/traits.md +++ b/guide/src/conversions/traits.md @@ -610,6 +610,33 @@ enum Enum<'a, 'py, K: Hash + Eq, V> { // enums are supported and convert using t Additionally `IntoPyObject` can be derived for a reference to a struct or enum using the `IntoPyObjectRef` derive macro. All the same rules from above apply as well. +##### `#[derive(IntoPyObject)]`/`#[derive(IntoPyObjectRef)]` Field Attributes +- `pyo3(into_py_with = ...)` + - apply a custom function to convert the field from Rust into Python. + - the argument must be the function indentifier + - the function signature must be `fn(Cow<'_, T>, Python<'py>) -> PyResult>` where `T` is the Rust type of the argument. + - `#[derive(IntoPyObject)]` will invoke the function with `Cow::Owned` + - `#[derive(IntoPyObjectRef)]` will invoke the function with `Cow::Borrowed` + + ```rust + # use pyo3::prelude::*; + # use pyo3::IntoPyObjectExt; + # use std::borrow::Cow; + #[derive(Clone)] + struct NotIntoPy(usize); + + #[derive(IntoPyObject, IntoPyObjectRef)] + struct MyStruct { + #[pyo3(into_py_with = convert)] + not_into_py: NotIntoPy, + } + + /// Convert `NotIntoPy` into Python + fn convert<'py>(not_into_py: Cow<'_, NotIntoPy>, py: Python<'py>) -> PyResult> { + not_into_py.0.into_bound_py_any(py) + } + ``` + #### manual implementation If the derive macro is not suitable for your use case, `IntoPyObject` can be implemented manually as diff --git a/newsfragments/4850.added.md b/newsfragments/4850.added.md new file mode 100644 index 00000000000..acdd7c2e48a --- /dev/null +++ b/newsfragments/4850.added.md @@ -0,0 +1 @@ +introduce `into_py_with`/`into_py_with_ref` for `#[derive(IntoPyObject, IntoPyObjectRef)]` \ No newline at end of file diff --git a/pyo3-macros-backend/src/attributes.rs b/pyo3-macros-backend/src/attributes.rs old mode 100644 new mode 100755 index bd5da377121..c0591d74868 --- a/pyo3-macros-backend/src/attributes.rs +++ b/pyo3-macros-backend/src/attributes.rs @@ -25,6 +25,7 @@ pub mod kw { syn::custom_keyword!(get); syn::custom_keyword!(get_all); syn::custom_keyword!(hash); + syn::custom_keyword!(into_py_with); syn::custom_keyword!(item); syn::custom_keyword!(from_item_all); syn::custom_keyword!(mapping); @@ -350,6 +351,7 @@ impl ToTokens for OptionalKeywordAttribute { } pub type FromPyWithAttribute = KeywordAttribute>; +pub type IntoPyWithAttribute = KeywordAttribute; pub type DefaultAttribute = OptionalKeywordAttribute; diff --git a/pyo3-macros-backend/src/intopyobject.rs b/pyo3-macros-backend/src/intopyobject.rs old mode 100644 new mode 100755 index a60a5486cb8..787d1dd6259 --- a/pyo3-macros-backend/src/intopyobject.rs +++ b/pyo3-macros-backend/src/intopyobject.rs @@ -1,4 +1,4 @@ -use crate::attributes::{self, get_pyo3_options, CrateAttribute}; +use crate::attributes::{self, get_pyo3_options, CrateAttribute, IntoPyWithAttribute}; use crate::utils::Ctx; use proc_macro2::{Span, TokenStream}; use quote::{format_ident, quote, quote_spanned}; @@ -89,6 +89,7 @@ impl ItemOption { enum FieldAttribute { Item(ItemOption), + IntoPyWith(IntoPyWithAttribute), } impl Parse for FieldAttribute { @@ -118,6 +119,8 @@ impl Parse for FieldAttribute { span: attr.span, })) } + } else if lookahead.peek(attributes::kw::into_py_with) { + input.parse().map(FieldAttribute::IntoPyWith) } else { Err(lookahead.error()) } @@ -127,6 +130,7 @@ impl Parse for FieldAttribute { #[derive(Clone, Debug, Default)] struct FieldAttributes { item: Option, + into_py_with: Option, } impl FieldAttributes { @@ -159,6 +163,7 @@ impl FieldAttributes { match option { FieldAttribute::Item(item) => set_option!(item), + FieldAttribute::IntoPyWith(into_py_with) => set_option!(into_py_with), } Ok(()) } @@ -182,10 +187,12 @@ struct NamedStructField<'a> { ident: &'a syn::Ident, field: &'a syn::Field, item: Option, + into_py_with: Option, } struct TupleStructField<'a> { field: &'a syn::Field, + into_py_with: Option, } /// Container Style @@ -214,14 +221,14 @@ enum ContainerType<'a> { /// Data container /// /// Either describes a struct or an enum variant. -struct Container<'a> { +struct Container<'a, const REF: bool> { path: syn::Path, receiver: Option, ty: ContainerType<'a>, } /// Construct a container based on fields, identifier and attributes. -impl<'a> Container<'a> { +impl<'a, const REF: bool> Container<'a, REF> { /// /// Fails if the variant has no fields or incompatible attributes. fn new( @@ -241,13 +248,23 @@ impl<'a> Container<'a> { attrs.item.is_none(), attrs.item.unwrap().span() => "`item` is not permitted on tuple struct elements." ); - Ok(TupleStructField { field }) + Ok(TupleStructField { + field, + into_py_with: attrs.into_py_with, + }) }) .collect::>>()?; if tuple_fields.len() == 1 { // Always treat a 1-length tuple struct as "transparent", even without the // explicit annotation. - let TupleStructField { field } = tuple_fields.pop().unwrap(); + let TupleStructField { + field, + into_py_with, + } = tuple_fields.pop().unwrap(); + ensure_spanned!( + into_py_with.is_none(), + into_py_with.span() => "`into_py_with` is not permitted on `transparent` structs" + ); ContainerType::TupleNewtype(field) } else if options.transparent.is_some() { bail_spanned!( @@ -270,6 +287,10 @@ impl<'a> Container<'a> { attrs.item.is_none(), attrs.item.unwrap().span() => "`transparent` structs may not have `item` for the inner field" ); + ensure_spanned!( + attrs.into_py_with.is_none(), + attrs.into_py_with.span() => "`into_py_with` is not permitted on `transparent` structs or variants" + ); ContainerType::StructNewtype(field) } else { let struct_fields = named @@ -287,6 +308,7 @@ impl<'a> Container<'a> { ident, field, item: attrs.item, + into_py_with: attrs.into_py_with, }) }) .collect::>>()?; @@ -389,8 +411,21 @@ impl<'a> Container<'a> { .map(|item| item.value()) .unwrap_or_else(|| f.ident.unraw().to_string()); let value = Ident::new(&format!("arg{i}"), f.field.ty.span()); - quote! { - #pyo3_path::types::PyDictMethods::set_item(&dict, #key, #value)?; + + if let Some(expr_path) = f.into_py_with.as_ref().map(|i|&i.value) { + let cow = if REF { + quote!(::std::borrow::Cow::Borrowed(#value)) + } else { + quote!(::std::borrow::Cow::Owned(#value)) + }; + quote! { + let into_py_with: fn(::std::borrow::Cow<'_, _>, #pyo3_path::Python<'py>) -> #pyo3_path::PyResult<#pyo3_path::Bound<'py, #pyo3_path::PyAny>> = #expr_path; + #pyo3_path::types::PyDictMethods::set_item(&dict, #key, into_py_with(#cow, py)?)?; + } + } else { + quote! { + #pyo3_path::types::PyDictMethods::set_item(&dict, #key, #value)?; + } } }) .collect::(); @@ -426,11 +461,27 @@ impl<'a> Container<'a> { .iter() .enumerate() .map(|(i, f)| { + let ty = &f.field.ty; let value = Ident::new(&format!("arg{i}"), f.field.ty.span()); - quote_spanned! { f.field.ty.span() => - #pyo3_path::conversion::IntoPyObject::into_pyobject(#value, py) - .map(#pyo3_path::BoundObject::into_any) - .map(#pyo3_path::BoundObject::into_bound)?, + + if let Some(expr_path) = f.into_py_with.as_ref().map(|i|&i.value) { + let cow = if REF { + quote!(::std::borrow::Cow::Borrowed(#value)) + } else { + quote!(::std::borrow::Cow::Owned(#value)) + }; + quote_spanned! { ty.span() => + { + let into_py_with: fn(::std::borrow::Cow<'_, _>, #pyo3_path::Python<'py>) -> #pyo3_path::PyResult<#pyo3_path::Bound<'py, #pyo3_path::PyAny>> = #expr_path; + into_py_with(#cow, py)? + }, + } + } else { + quote_spanned! { ty.span() => + #pyo3_path::conversion::IntoPyObject::into_pyobject(#value, py) + .map(#pyo3_path::BoundObject::into_any) + .map(#pyo3_path::BoundObject::into_bound)?, + } } }) .collect::(); @@ -450,11 +501,11 @@ impl<'a> Container<'a> { } /// Describes derivation input of an enum. -struct Enum<'a> { - variants: Vec>, +struct Enum<'a, const REF: bool> { + variants: Vec>, } -impl<'a> Enum<'a> { +impl<'a, const REF: bool> Enum<'a, REF> { /// Construct a new enum representation. /// /// `data_enum` is the `syn` representation of the input enum, `ident` is the @@ -563,12 +614,12 @@ pub fn build_derive_into_pyobject(tokens: &DeriveInput) -> Resu if options.transparent.is_some() { bail_spanned!(tokens.span() => "`transparent` is not supported at top level for enums"); } - let en = Enum::new(en, &tokens.ident)?; + let en = Enum::::new(en, &tokens.ident)?; en.build(ctx) } syn::Data::Struct(st) => { let ident = &tokens.ident; - let st = Container::new( + let st = Container::::new( Some(Ident::new("self", Span::call_site())), &st.fields, parse_quote!(#ident), diff --git a/tests/test_compile_error.rs b/tests/test_compile_error.rs index 029548ec461..4c0f8f949c9 100644 --- a/tests/test_compile_error.rs +++ b/tests/test_compile_error.rs @@ -26,6 +26,8 @@ fn test_compile_errors() { t.compile_fail("tests/ui/pyclass_send.rs"); t.compile_fail("tests/ui/invalid_argument_attributes.rs"); t.compile_fail("tests/ui/invalid_intopy_derive.rs"); + #[cfg(not(windows))] + t.compile_fail("tests/ui/invalid_intopy_with.rs"); t.compile_fail("tests/ui/invalid_frompy_derive.rs"); t.compile_fail("tests/ui/static_ref.rs"); t.compile_fail("tests/ui/wrong_aspyref_lifetimes.rs"); diff --git a/tests/test_intopyobject.rs b/tests/test_intopyobject.rs old mode 100644 new mode 100755 index 971663b05d7..8e758c636cd --- a/tests/test_intopyobject.rs +++ b/tests/test_intopyobject.rs @@ -1,7 +1,7 @@ #![cfg(feature = "macros")] -use pyo3::types::{PyDict, PyString}; -use pyo3::{prelude::*, IntoPyObject}; +use pyo3::types::{PyDict, PyList, PyString}; +use pyo3::{prelude::*, py_run, IntoPyObject, IntoPyObjectExt}; use std::collections::HashMap; use std::hash::Hash; @@ -150,9 +150,20 @@ fn test_transparent_tuple_struct() { }); } -#[derive(Debug, IntoPyObject)] +fn phantom_into_py<'py, T>( + _: std::borrow::Cow<'_, std::marker::PhantomData>, + py: Python<'py>, +) -> PyResult> { + std::any::type_name::().into_bound_py_any(py) +} + +#[derive(Debug, IntoPyObject, IntoPyObjectRef)] pub enum Foo<'py> { - TupleVar(usize, String), + TupleVar( + usize, + String, + #[pyo3(into_py_with = phantom_into_py::<()>)] std::marker::PhantomData<()>, + ), StructVar { test: Bound<'py, PyString>, }, @@ -167,10 +178,12 @@ pub enum Foo<'py> { #[test] fn test_enum() { Python::with_gil(|py| { - let foo = Foo::TupleVar(1, "test".into()).into_pyobject(py).unwrap(); + let foo = Foo::TupleVar(1, "test".into(), std::marker::PhantomData) + .into_pyobject(py) + .unwrap(); assert_eq!( - foo.extract::<(usize, String)>().unwrap(), - (1, String::from("test")) + foo.extract::<(usize, String, String)>().unwrap(), + (1, String::from("test"), String::from("()")) ); let foo = Foo::StructVar { @@ -199,3 +212,43 @@ fn test_enum() { assert!(foo.is_none()); }); } + +#[derive(Debug, IntoPyObject, IntoPyObjectRef)] +pub struct Zap { + #[pyo3(item)] + name: String, + + #[pyo3(into_py_with = zap_into_py, item("my_object"))] + some_object_length: usize, +} + +fn zap_into_py<'py>( + len: std::borrow::Cow<'_, usize>, + py: Python<'py>, +) -> PyResult> { + Ok(PyList::new(py, 1..*len + 1)?.into_any()) +} + +#[test] +fn test_into_py_with() { + Python::with_gil(|py| { + let zap = Zap { + name: "whatever".into(), + some_object_length: 3, + }; + + let py_zap_ref = (&zap).into_pyobject(py).unwrap(); + let py_zap = zap.into_pyobject(py).unwrap(); + + py_run!( + py, + py_zap_ref, + "assert py_zap_ref == {'name': 'whatever', 'my_object': [1, 2, 3]},f'{py_zap_ref}'" + ); + py_run!( + py, + py_zap, + "assert py_zap == {'name': 'whatever', 'my_object': [1, 2, 3]},f'{py_zap}'" + ); + }); +} diff --git a/tests/ui/invalid_intopy_derive.rs b/tests/ui/invalid_intopy_derive.rs old mode 100644 new mode 100755 index c65d44ff1bc..b609a740e56 --- a/tests/ui/invalid_intopy_derive.rs +++ b/tests/ui/invalid_intopy_derive.rs @@ -106,4 +106,36 @@ struct StructTransparentItem { foo: String, } +#[derive(IntoPyObject)] +#[pyo3(transparent)] +struct StructTransparentIntoPyWith { + #[pyo3(into_py_with = into)] + foo: String, +} + +#[derive(IntoPyObjectRef)] +#[pyo3(transparent)] +struct StructTransparentIntoPyWithRef { + #[pyo3(into_py_with = into_ref)] + foo: String, +} + +#[derive(IntoPyObject)] +#[pyo3(transparent)] +struct TupleTransparentIntoPyWith(#[pyo3(into_py_with = into)] String); + +#[derive(IntoPyObject)] +enum EnumTupleIntoPyWith { + TransparentTuple(#[pyo3(into_py_with = into)] usize), +} + +#[derive(IntoPyObject)] +enum EnumStructIntoPyWith { + #[pyo3(transparent)] + TransparentStruct { + #[pyo3(into_py_with = into)] + a: usize, + }, +} + fn main() {} diff --git a/tests/ui/invalid_intopy_derive.stderr b/tests/ui/invalid_intopy_derive.stderr index cf125d9c073..4c04f8002a0 100644 --- a/tests/ui/invalid_intopy_derive.stderr +++ b/tests/ui/invalid_intopy_derive.stderr @@ -125,3 +125,33 @@ error: `transparent` structs may not have `item` for the inner field | 105 | #[pyo3(item)] | ^^^^ + +error: `into_py_with` is not permitted on `transparent` structs or variants + --> tests/ui/invalid_intopy_derive.rs:112:12 + | +112 | #[pyo3(into_py_with = into)] + | ^^^^^^^^^^^^ + +error: `into_py_with` is not permitted on `transparent` structs or variants + --> tests/ui/invalid_intopy_derive.rs:119:12 + | +119 | #[pyo3(into_py_with = into_ref)] + | ^^^^^^^^^^^^ + +error: `into_py_with` is not permitted on `transparent` structs + --> tests/ui/invalid_intopy_derive.rs:125:42 + | +125 | struct TupleTransparentIntoPyWith(#[pyo3(into_py_with = into)] String); + | ^^^^^^^^^^^^ + +error: `into_py_with` is not permitted on `transparent` structs + --> tests/ui/invalid_intopy_derive.rs:129:29 + | +129 | TransparentTuple(#[pyo3(into_py_with = into)] usize), + | ^^^^^^^^^^^^ + +error: `into_py_with` is not permitted on `transparent` structs or variants + --> tests/ui/invalid_intopy_derive.rs:136:16 + | +136 | #[pyo3(into_py_with = into)] + | ^^^^^^^^^^^^ diff --git a/tests/ui/invalid_intopy_with.rs b/tests/ui/invalid_intopy_with.rs new file mode 100755 index 00000000000..7cc910f57d8 --- /dev/null +++ b/tests/ui/invalid_intopy_with.rs @@ -0,0 +1,13 @@ +use pyo3::{IntoPyObject, IntoPyObjectRef}; + +#[derive(IntoPyObject, IntoPyObjectRef)] +struct InvalidIntoPyWithFn { + #[pyo3(into_py_with = into)] + inner: String, +} + +fn into(_a: String, _py: pyo3::Python<'_>) -> pyo3::PyResult> { + todo!() +} + +fn main() {} diff --git a/tests/ui/invalid_intopy_with.stderr b/tests/ui/invalid_intopy_with.stderr new file mode 100755 index 00000000000..bfa3e6ec274 --- /dev/null +++ b/tests/ui/invalid_intopy_with.stderr @@ -0,0 +1,23 @@ +error[E0308]: mismatched types + --> tests/ui/invalid_intopy_with.rs:5:27 + | +3 | #[derive(IntoPyObject, IntoPyObjectRef)] + | ------------ expected due to this +4 | struct InvalidIntoPyWithFn { +5 | #[pyo3(into_py_with = into)] + | ^^^^ expected fn pointer, found fn item + | + = note: expected fn pointer `for<'a> fn(Cow<'a, _>, Python<'py>) -> Result, PyErr>` + found fn item `for<'a> fn(String, Python<'a>) -> Result, PyErr> {into}` + +error[E0308]: mismatched types + --> tests/ui/invalid_intopy_with.rs:5:27 + | +3 | #[derive(IntoPyObject, IntoPyObjectRef)] + | --------------- expected due to this +4 | struct InvalidIntoPyWithFn { +5 | #[pyo3(into_py_with = into)] + | ^^^^ expected fn pointer, found fn item + | + = note: expected fn pointer `for<'a> fn(Cow<'a, _>, Python<'py>) -> Result, PyErr>` + found fn item `for<'a> fn(String, Python<'a>) -> Result, PyErr> {into}` From 31bb2f4b1cc69e1fb7987ffeb6e8f01ce29535f1 Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Sat, 1 Mar 2025 15:14:51 +0100 Subject: [PATCH 464/495] allow path in `from_py_with` and deprecate string literal (#4860) --- guide/src/class/numeric.md | 6 ++-- guide/src/conversions/traits.md | 6 ++-- guide/src/function.md | 4 +-- newsfragments/4860.changed.md | 1 + pyo3-macros-backend/src/attributes.rs | 32 +++++++++++++++++- pyo3-macros-backend/src/frompyobject.rs | 23 +++++++++++-- pyo3-macros-backend/src/params.rs | 4 ++- pyo3-macros-backend/src/pymethod.rs | 4 ++- pyo3-macros-backend/src/utils.rs | 18 ++++++++-- tests/test_class_basics.rs | 8 ++--- tests/test_compile_error.rs | 1 + tests/test_frompyobject.rs | 12 +++---- tests/test_getter_setter.rs | 2 +- tests/test_methods.rs | 2 +- tests/test_pyfunction.rs | 6 ++-- tests/ui/deprecated.rs | 37 +++++++++++++++++++++ tests/ui/deprecated.stderr | 33 ++++++++++++++++++ tests/ui/forbid_unsafe.rs | 2 +- tests/ui/invalid_argument_attributes.rs | 2 +- tests/ui/invalid_argument_attributes.stderr | 21 ++++++++---- tests/ui/invalid_frompy_derive.rs | 2 +- tests/ui/invalid_frompy_derive.stderr | 12 +++---- 22 files changed, 193 insertions(+), 45 deletions(-) create mode 100644 newsfragments/4860.changed.md create mode 100644 tests/ui/deprecated.rs create mode 100644 tests/ui/deprecated.stderr diff --git a/guide/src/class/numeric.md b/guide/src/class/numeric.md index a441eba4e13..4f73a44adab 100644 --- a/guide/src/class/numeric.md +++ b/guide/src/class/numeric.md @@ -27,7 +27,7 @@ OverflowError: Python int too large to convert to C long ``` Instead of relying on the default [`FromPyObject`] extraction to parse arguments, we can specify our -own extraction function, using the `#[pyo3(from_py_with = "...")]` attribute. Unfortunately PyO3 +own extraction function, using the `#[pyo3(from_py_with = ...)]` attribute. Unfortunately PyO3 doesn't provide a way to wrap Python integers out of the box, but we can do a Python call to mask it and cast it to an `i32`. @@ -62,7 +62,7 @@ struct Number(i32); #[pymethods] impl Number { #[new] - fn new(#[pyo3(from_py_with = "wrap")] value: i32) -> Self { + fn new(#[pyo3(from_py_with = wrap)] value: i32) -> Self { Self(value) } } @@ -225,7 +225,7 @@ struct Number(i32); #[pymethods] impl Number { #[new] - fn new(#[pyo3(from_py_with = "wrap")] value: i32) -> Self { + fn new(#[pyo3(from_py_with = wrap)] value: i32) -> Self { Self(value) } diff --git a/guide/src/conversions/traits.md b/guide/src/conversions/traits.md index 43041cda1b8..848dc041ef7 100755 --- a/guide/src/conversions/traits.md +++ b/guide/src/conversions/traits.md @@ -488,9 +488,9 @@ If the input is neither a string nor an integer, the error message will be: - `pyo3(item)`, `pyo3(item("key"))` - retrieve the field from a mapping, possibly with the custom key specified as an argument. - can be any literal that implements `ToBorrowedObject` -- `pyo3(from_py_with = "...")` +- `pyo3(from_py_with = ...)` - apply a custom function to convert the field from Python the desired Rust type. - - the argument must be the name of the function as a string. + - the argument must be the path to the function. - the function signature must be `fn(&Bound) -> PyResult` where `T` is the Rust type of the argument. - `pyo3(default)`, `pyo3(default = ...)` - if the argument is set, uses the given default value. @@ -507,7 +507,7 @@ use pyo3::prelude::*; #[derive(FromPyObject)] struct RustyStruct { - #[pyo3(item("value"), default, from_py_with = "Bound::<'_, PyAny>::len")] + #[pyo3(item("value"), default, from_py_with = Bound::<'_, PyAny>::len)] len: usize, #[pyo3(item)] other: usize, diff --git a/guide/src/function.md b/guide/src/function.md index fd215a1550e..323bc9c8f87 100644 --- a/guide/src/function.md +++ b/guide/src/function.md @@ -101,7 +101,7 @@ The `#[pyo3]` attribute can be used to modify properties of the generated Python The `#[pyo3]` attribute can be used on individual arguments to modify properties of them in the generated function. It can take any combination of the following options: - - `#[pyo3(from_py_with = "...")]` + - `#[pyo3(from_py_with = ...)]` Set this on an option to specify a custom function to convert the function argument from Python to the desired Rust type, instead of using the default `FromPyObject` extraction. The function signature must be `fn(&Bound<'_, PyAny>) -> PyResult` where `T` is the Rust type of the argument. @@ -115,7 +115,7 @@ The `#[pyo3]` attribute can be used on individual arguments to modify properties } #[pyfunction] - fn object_length(#[pyo3(from_py_with = "get_length")] argument: usize) -> usize { + fn object_length(#[pyo3(from_py_with = get_length)] argument: usize) -> usize { argument } diff --git a/newsfragments/4860.changed.md b/newsfragments/4860.changed.md new file mode 100644 index 00000000000..4f62e45a5c8 --- /dev/null +++ b/newsfragments/4860.changed.md @@ -0,0 +1 @@ +`#[pyo3(from_py_with = ...)]` now take a path rather than a string literal \ No newline at end of file diff --git a/pyo3-macros-backend/src/attributes.rs b/pyo3-macros-backend/src/attributes.rs index c0591d74868..19a12801065 100755 --- a/pyo3-macros-backend/src/attributes.rs +++ b/pyo3-macros-backend/src/attributes.rs @@ -350,7 +350,37 @@ impl ToTokens for OptionalKeywordAttribute { } } -pub type FromPyWithAttribute = KeywordAttribute>; +#[derive(Debug, Clone)] +pub struct ExprPathWrap { + pub from_lit_str: bool, + pub expr_path: ExprPath, +} + +impl Parse for ExprPathWrap { + fn parse(input: ParseStream<'_>) -> Result { + match input.parse::() { + Ok(expr_path) => Ok(ExprPathWrap { + from_lit_str: false, + expr_path, + }), + Err(e) => match input.parse::>() { + Ok(LitStrValue(expr_path)) => Ok(ExprPathWrap { + from_lit_str: true, + expr_path, + }), + Err(_) => Err(e), + }, + } + } +} + +impl ToTokens for ExprPathWrap { + fn to_tokens(&self, tokens: &mut TokenStream) { + self.expr_path.to_tokens(tokens) + } +} + +pub type FromPyWithAttribute = KeywordAttribute; pub type IntoPyWithAttribute = KeywordAttribute; pub type DefaultAttribute = OptionalKeywordAttribute; diff --git a/pyo3-macros-backend/src/frompyobject.rs b/pyo3-macros-backend/src/frompyobject.rs index ac7dbea681f..68f95e794a8 100644 --- a/pyo3-macros-backend/src/frompyobject.rs +++ b/pyo3-macros-backend/src/frompyobject.rs @@ -2,7 +2,7 @@ use crate::attributes::{ self, get_pyo3_options, CrateAttribute, DefaultAttribute, FromPyWithAttribute, RenameAllAttribute, RenamingRule, }; -use crate::utils::{self, Ctx}; +use crate::utils::{self, deprecated_from_py_with, Ctx}; use proc_macro2::TokenStream; use quote::{format_ident, quote, quote_spanned, ToTokens}; use syn::{ @@ -304,10 +304,13 @@ impl<'a> Container<'a> { value: expr_path, }) = from_py_with { + let deprecation = deprecated_from_py_with(expr_path).unwrap_or_default(); + let extractor = quote_spanned! { kw.span => { let from_py_with: fn(_) -> _ = #expr_path; from_py_with } }; quote! { + #deprecation Ok(#self_ty { #ident: #pyo3_path::impl_::frompyobject::extract_struct_field_with(#extractor, obj, #struct_name, #field_name)? }) @@ -324,10 +327,13 @@ impl<'a> Container<'a> { value: expr_path, }) = from_py_with { + let deprecation = deprecated_from_py_with(expr_path).unwrap_or_default(); + let extractor = quote_spanned! { kw.span => { let from_py_with: fn(_) -> _ = #expr_path; from_py_with } }; quote! { + #deprecation #pyo3_path::impl_::frompyobject::extract_tuple_struct_field_with(#extractor, obj, #struct_name, 0).map(#self_ty) } } else { @@ -361,7 +367,14 @@ impl<'a> Container<'a> { }} }); + let deprecations = struct_fields + .iter() + .filter_map(|fields| fields.from_py_with.as_ref()) + .filter_map(|kw| deprecated_from_py_with(&kw.value)) + .collect::(); + quote!( + #deprecations match #pyo3_path::types::PyAnyMethods::extract(obj) { ::std::result::Result::Ok((#(#field_idents),*)) => ::std::result::Result::Ok(#self_ty(#(#fields),*)), ::std::result::Result::Err(err) => ::std::result::Result::Err(err), @@ -435,7 +448,13 @@ impl<'a> Container<'a> { fields.push(quote!(#ident: #extracted)); } - quote!(::std::result::Result::Ok(#self_ty{#fields})) + let d = struct_fields + .iter() + .filter_map(|field| field.from_py_with.as_ref()) + .filter_map(|kw| deprecated_from_py_with(&kw.value)) + .collect::(); + + quote!(#d ::std::result::Result::Ok(#self_ty{#fields})) } } diff --git a/pyo3-macros-backend/src/params.rs b/pyo3-macros-backend/src/params.rs index 9425b8d32b6..806e9f57ad9 100644 --- a/pyo3-macros-backend/src/params.rs +++ b/pyo3-macros-backend/src/params.rs @@ -1,4 +1,4 @@ -use crate::utils::Ctx; +use crate::utils::{deprecated_from_py_with, Ctx}; use crate::{ attributes::FromPyWithAttribute, method::{FnArg, FnSpec, RegularArg}, @@ -62,7 +62,9 @@ pub fn impl_arg_params( .filter_map(|(i, arg)| { let from_py_with = &arg.from_py_with()?.value; let from_py_with_holder = format_ident!("from_py_with_{}", i); + let d = deprecated_from_py_with(from_py_with).unwrap_or_default(); Some(quote_spanned! { from_py_with.span() => + #d let #from_py_with_holder = #from_py_with; }) }) diff --git a/pyo3-macros-backend/src/pymethod.rs b/pyo3-macros-backend/src/pymethod.rs index 825a4addfd3..a94a6ad67ab 100644 --- a/pyo3-macros-backend/src/pymethod.rs +++ b/pyo3-macros-backend/src/pymethod.rs @@ -4,7 +4,7 @@ use std::ffi::CString; use crate::attributes::{FromPyWithAttribute, NameAttribute, RenamingRule}; use crate::method::{CallingConvention, ExtractErrorMode, PyArg}; use crate::params::{impl_regular_arg_param, Holders}; -use crate::utils::PythonDoc; +use crate::utils::{deprecated_from_py_with, PythonDoc}; use crate::utils::{Ctx, LitCStr}; use crate::{ method::{FnArg, FnSpec, FnType, SelfType}, @@ -660,8 +660,10 @@ pub fn impl_py_setter_def( let (from_py_with, ident) = if let Some(from_py_with) = &value_arg.from_py_with().as_ref().map(|f| &f.value) { let ident = syn::Ident::new("from_py_with", from_py_with.span()); + let d = deprecated_from_py_with(from_py_with).unwrap_or_default(); ( quote_spanned! { from_py_with.span() => + #d let #ident = #from_py_with; }, ident, diff --git a/pyo3-macros-backend/src/utils.rs b/pyo3-macros-backend/src/utils.rs index 191ee165bbc..d2f1eb84c6f 100644 --- a/pyo3-macros-backend/src/utils.rs +++ b/pyo3-macros-backend/src/utils.rs @@ -1,6 +1,6 @@ -use crate::attributes::{CrateAttribute, RenamingRule}; +use crate::attributes::{CrateAttribute, ExprPathWrap, RenamingRule}; use proc_macro2::{Span, TokenStream}; -use quote::{quote, ToTokens}; +use quote::{quote, quote_spanned, ToTokens}; use std::ffi::CString; use syn::spanned::Spanned; use syn::{punctuated::Punctuated, Token}; @@ -323,3 +323,17 @@ pub(crate) fn has_attribute_with_namespace( .eq(attr.path().segments.iter().map(|v| &v.ident)) }) } + +pub(crate) fn deprecated_from_py_with(expr_path: &ExprPathWrap) -> Option { + let path = quote!(#expr_path).to_string(); + let msg = + format!("remove the quotes from the literal\n= help: use `{path}` instead of `\"{path}\"`"); + expr_path.from_lit_str.then(|| { + quote_spanned! { expr_path.span() => + #[deprecated(since = "0.24.0", note = #msg)] + #[allow(dead_code)] + const LIT_STR_DEPRECATION: () = (); + let _: () = LIT_STR_DEPRECATION; + } + }) +} diff --git a/tests/test_class_basics.rs b/tests/test_class_basics.rs index 4a687a89eea..3cca37c3bd9 100644 --- a/tests/test_class_basics.rs +++ b/tests/test_class_basics.rs @@ -357,23 +357,23 @@ struct ClassWithFromPyWithMethods {} #[pymethods] impl ClassWithFromPyWithMethods { - fn instance_method(&self, #[pyo3(from_py_with = "get_length")] argument: usize) -> usize { + fn instance_method(&self, #[pyo3(from_py_with = get_length)] argument: usize) -> usize { argument } #[classmethod] fn classmethod( _cls: &Bound<'_, PyType>, - #[pyo3(from_py_with = "Bound::<'_, PyAny>::len")] argument: usize, + #[pyo3(from_py_with = Bound::<'_, PyAny>::len)] argument: usize, ) -> usize { argument } #[staticmethod] - fn staticmethod(#[pyo3(from_py_with = "get_length")] argument: usize) -> usize { + fn staticmethod(#[pyo3(from_py_with = get_length)] argument: usize) -> usize { argument } - fn __contains__(&self, #[pyo3(from_py_with = "is_even")] obj: bool) -> bool { + fn __contains__(&self, #[pyo3(from_py_with = is_even)] obj: bool) -> bool { obj } } diff --git a/tests/test_compile_error.rs b/tests/test_compile_error.rs index 4c0f8f949c9..9a4ffc114ba 100644 --- a/tests/test_compile_error.rs +++ b/tests/test_compile_error.rs @@ -72,4 +72,5 @@ fn test_compile_errors() { t.compile_fail("tests/ui/invalid_base_class.rs"); t.pass("tests/ui/ambiguous_associated_items.rs"); t.pass("tests/ui/pyclass_probe.rs"); + t.compile_fail("tests/ui/deprecated.rs"); } diff --git a/tests/test_frompyobject.rs b/tests/test_frompyobject.rs index ad0a2d4f3d9..f1a68c18f01 100644 --- a/tests/test_frompyobject.rs +++ b/tests/test_frompyobject.rs @@ -628,7 +628,7 @@ pub struct Zap { #[pyo3(item)] name: String, - #[pyo3(from_py_with = "Bound::<'_, PyAny>::len", item("my_object"))] + #[pyo3(from_py_with = Bound::<'_, PyAny>::len, item("my_object"))] some_object_length: usize, } @@ -653,7 +653,7 @@ fn test_from_py_with() { #[derive(Debug, FromPyObject)] pub struct ZapTuple( String, - #[pyo3(from_py_with = "Bound::<'_, PyAny>::len")] usize, + #[pyo3(from_py_with = Bound::<'_, PyAny>::len)] usize, ); #[test] @@ -693,10 +693,10 @@ fn test_from_py_with_tuple_struct_error() { #[derive(Debug, FromPyObject, PartialEq, Eq)] pub enum ZapEnum { - Zip(#[pyo3(from_py_with = "Bound::<'_, PyAny>::len")] usize), + Zip(#[pyo3(from_py_with = Bound::<'_, PyAny>::len)] usize), Zap( String, - #[pyo3(from_py_with = "Bound::<'_, PyAny>::len")] usize, + #[pyo3(from_py_with = Bound::<'_, PyAny>::len)] usize, ), } @@ -717,7 +717,7 @@ fn test_from_py_with_enum() { #[derive(Debug, FromPyObject, PartialEq, Eq)] #[pyo3(transparent)] pub struct TransparentFromPyWith { - #[pyo3(from_py_with = "Bound::<'_, PyAny>::len")] + #[pyo3(from_py_with = Bound::<'_, PyAny>::len)] len: usize, } @@ -815,7 +815,7 @@ fn test_with_explicit_default_item() { #[derive(Debug, FromPyObject, PartialEq, Eq)] pub struct WithDefaultItemAndConversionFunction { - #[pyo3(item, default, from_py_with = "Bound::<'_, PyAny>::len")] + #[pyo3(item, default, from_py_with = Bound::<'_, PyAny>::len)] opt: usize, #[pyo3(item)] value: usize, diff --git a/tests/test_getter_setter.rs b/tests/test_getter_setter.rs index cdc8136bede..82a50442ec5 100644 --- a/tests/test_getter_setter.rs +++ b/tests/test_getter_setter.rs @@ -43,7 +43,7 @@ impl ClassWithProperties { } #[setter] - fn set_from_len(&mut self, #[pyo3(from_py_with = "extract_len")] value: i32) { + fn set_from_len(&mut self, #[pyo3(from_py_with = extract_len)] value: i32) { self.num = value; } diff --git a/tests/test_methods.rs b/tests/test_methods.rs index 743fa6e6b4f..9a7f593df2a 100644 --- a/tests/test_methods.rs +++ b/tests/test_methods.rs @@ -1198,7 +1198,7 @@ fn test_issue_2988() { _data: Vec, // The from_py_with here looks a little odd, we just need some way // to encourage the macro to expand the from_py_with default path too - #[pyo3(from_py_with = " as PyAnyMethods>::extract")] _data2: Vec, + #[pyo3(from_py_with = as PyAnyMethods>::extract)] _data2: Vec, ) { } } diff --git a/tests/test_pyfunction.rs b/tests/test_pyfunction.rs index 13ba5405ed3..4e3bdce9e05 100644 --- a/tests/test_pyfunction.rs +++ b/tests/test_pyfunction.rs @@ -143,7 +143,7 @@ fn datetime_to_timestamp(dt: &Bound<'_, PyAny>) -> PyResult { #[cfg(not(Py_LIMITED_API))] #[pyfunction] fn function_with_custom_conversion( - #[pyo3(from_py_with = "datetime_to_timestamp")] timestamp: i64, + #[pyo3(from_py_with = datetime_to_timestamp)] timestamp: i64, ) -> i64 { timestamp } @@ -196,13 +196,13 @@ fn test_from_py_with_defaults() { // issue 2280 combination of from_py_with and Option did not compile #[pyfunction] #[pyo3(signature = (int=None))] - fn from_py_with_option(#[pyo3(from_py_with = "optional_int")] int: Option) -> i32 { + fn from_py_with_option(#[pyo3(from_py_with = optional_int)] int: Option) -> i32 { int.unwrap_or(0) } #[pyfunction(signature = (len=0))] fn from_py_with_default( - #[pyo3(from_py_with = " as PyAnyMethods>::len")] len: usize, + #[pyo3(from_py_with = as PyAnyMethods>::len)] len: usize, ) -> usize { len } diff --git a/tests/ui/deprecated.rs b/tests/ui/deprecated.rs new file mode 100644 index 00000000000..8d5c73780cf --- /dev/null +++ b/tests/ui/deprecated.rs @@ -0,0 +1,37 @@ +#![deny(deprecated)] +use pyo3::prelude::*; + +#[pyfunction] +fn from_py_with_in_function( + #[pyo3(from_py_with = "Bound::<'_, PyAny>::len")] argument: usize, +) -> usize { + argument +} + +#[pyclass] +struct Number(usize); + +#[pymethods] +impl Number { + #[new] + fn from_py_with_in_method( + #[pyo3(from_py_with = "Bound::<'_, PyAny>::len")] value: usize, + ) -> Self { + Self(value) + } +} + +#[derive(FromPyObject)] +struct FromPyWithStruct { + #[pyo3(from_py_with = "Bound::<'_, PyAny>::len")] + len: usize, + other: usize, +} + +#[derive(FromPyObject)] +struct FromPyWithTuple( + #[pyo3(from_py_with = "Bound::<'_, PyAny>::len")] usize, + usize, +); + +fn main() {} diff --git a/tests/ui/deprecated.stderr b/tests/ui/deprecated.stderr new file mode 100644 index 00000000000..63664c3f77c --- /dev/null +++ b/tests/ui/deprecated.stderr @@ -0,0 +1,33 @@ +error: use of deprecated constant `__pyfunction_from_py_with_in_function::LIT_STR_DEPRECATION`: remove the quotes from the literal + = help: use `Bound :: < '_, PyAny > :: len` instead of `"Bound :: < '_, PyAny > :: len"` + --> tests/ui/deprecated.rs:6:27 + | +6 | #[pyo3(from_py_with = "Bound::<'_, PyAny>::len")] argument: usize, + | ^^^^^^^^^^^^^^^^^^^^^^^^^ + | +note: the lint level is defined here + --> tests/ui/deprecated.rs:1:9 + | +1 | #![deny(deprecated)] + | ^^^^^^^^^^ + +error: use of deprecated constant `Number::__pymethod___new____::LIT_STR_DEPRECATION`: remove the quotes from the literal + = help: use `Bound :: < '_, PyAny > :: len` instead of `"Bound :: < '_, PyAny > :: len"` + --> tests/ui/deprecated.rs:18:31 + | +18 | #[pyo3(from_py_with = "Bound::<'_, PyAny>::len")] value: usize, + | ^^^^^^^^^^^^^^^^^^^^^^^^^ + +error: use of deprecated constant `>::extract_bound::LIT_STR_DEPRECATION`: remove the quotes from the literal + = help: use `Bound :: < '_, PyAny > :: len` instead of `"Bound :: < '_, PyAny > :: len"` + --> tests/ui/deprecated.rs:26:27 + | +26 | #[pyo3(from_py_with = "Bound::<'_, PyAny>::len")] + | ^^^^^^^^^^^^^^^^^^^^^^^^^ + +error: use of deprecated constant `>::extract_bound::LIT_STR_DEPRECATION`: remove the quotes from the literal + = help: use `Bound :: < '_, PyAny > :: len` instead of `"Bound :: < '_, PyAny > :: len"` + --> tests/ui/deprecated.rs:33:27 + | +33 | #[pyo3(from_py_with = "Bound::<'_, PyAny>::len")] usize, + | ^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/tests/ui/forbid_unsafe.rs b/tests/ui/forbid_unsafe.rs index 4ab7639d658..d9a51c52895 100644 --- a/tests/ui/forbid_unsafe.rs +++ b/tests/ui/forbid_unsafe.rs @@ -37,7 +37,7 @@ mod from_py_with { } #[pyfunction] - fn f(#[pyo3(from_py_with = "bytes_from_py")] _bytes: Vec) {} + fn f(#[pyo3(from_py_with = bytes_from_py)] _bytes: Vec) {} } fn main() {} diff --git a/tests/ui/invalid_argument_attributes.rs b/tests/ui/invalid_argument_attributes.rs index 819d6709ef8..43f07c46191 100644 --- a/tests/ui/invalid_argument_attributes.rs +++ b/tests/ui/invalid_argument_attributes.rs @@ -10,7 +10,7 @@ fn from_py_with_no_value(#[pyo3(from_py_with)] _param: String) {} fn from_py_with_string(#[pyo3("from_py_with")] _param: String) {} #[pyfunction] -fn from_py_with_value_not_a_string(#[pyo3(from_py_with = func)] _param: String) {} +fn from_py_with_value_not_found(#[pyo3(from_py_with = func)] _param: String) {} #[pyfunction] fn from_py_with_repeated(#[pyo3(from_py_with = "func", from_py_with = "func")] _param: String) {} diff --git a/tests/ui/invalid_argument_attributes.stderr b/tests/ui/invalid_argument_attributes.stderr index 6679dd635f1..d947402288d 100644 --- a/tests/ui/invalid_argument_attributes.stderr +++ b/tests/ui/invalid_argument_attributes.stderr @@ -16,18 +16,27 @@ error: expected `cancel_handle` or `from_py_with` 10 | fn from_py_with_string(#[pyo3("from_py_with")] _param: String) {} | ^^^^^^^^^^^^^^ -error: expected string literal - --> tests/ui/invalid_argument_attributes.rs:13:58 - | -13 | fn from_py_with_value_not_a_string(#[pyo3(from_py_with = func)] _param: String) {} - | ^^^^ - error: `from_py_with` may only be specified once per argument --> tests/ui/invalid_argument_attributes.rs:16:56 | 16 | fn from_py_with_repeated(#[pyo3(from_py_with = "func", from_py_with = "func")] _param: String) {} | ^^^^^^^^^^^^ +error[E0425]: cannot find value `func` in this scope + --> tests/ui/invalid_argument_attributes.rs:13:55 + | +13 | fn from_py_with_value_not_found(#[pyo3(from_py_with = func)] _param: String) {} + | ^^^^ not found in this scope + +warning: use of deprecated constant `__pyfunction_f::LIT_STR_DEPRECATION`: remove the quotes from the literal + = help: use `bytes_from_py` instead of `"bytes_from_py"` + --> tests/ui/invalid_argument_attributes.rs:23:28 + | +23 | fn f(#[pyo3(from_py_with = "bytes_from_py")] _bytes: Vec) {} + | ^^^^^^^^^^^^^^^ + | + = note: `#[warn(deprecated)]` on by default + error[E0308]: mismatched types --> tests/ui/invalid_argument_attributes.rs:23:13 | diff --git a/tests/ui/invalid_frompy_derive.rs b/tests/ui/invalid_frompy_derive.rs index 08d7a41b392..b6682345d9a 100644 --- a/tests/ui/invalid_frompy_derive.rs +++ b/tests/ui/invalid_frompy_derive.rs @@ -160,7 +160,7 @@ struct InvalidFromPyWith { } #[derive(FromPyObject)] -struct InvalidFromPyWithLiteral { +struct InvalidFromPyWithNotFound { #[pyo3(from_py_with = func)] field: String, } diff --git a/tests/ui/invalid_frompy_derive.stderr b/tests/ui/invalid_frompy_derive.stderr index 59bc69ffaaf..afb6bbc97cb 100644 --- a/tests/ui/invalid_frompy_derive.stderr +++ b/tests/ui/invalid_frompy_derive.stderr @@ -176,12 +176,6 @@ error: expected `=` 158 | #[pyo3(from_py_with)] | ^ -error: expected string literal - --> tests/ui/invalid_frompy_derive.rs:164:27 - | -164 | #[pyo3(from_py_with = func)] - | ^^^^ - error: `getter` is not permitted on tuple struct elements. --> tests/ui/invalid_frompy_derive.rs:169:27 | @@ -279,3 +273,9 @@ error: Useless variant `rename_all` - enum is already annotated with `rename_all | 258 | #[pyo3(rename_all = "camelCase")] | ^^^^^^^^^^ + +error[E0425]: cannot find value `func` in this scope + --> tests/ui/invalid_frompy_derive.rs:164:27 + | +164 | #[pyo3(from_py_with = func)] + | ^^^^ not found in this scope From 35c00845a29bc5c2372252a4c017cc5e6368d1dd Mon Sep 17 00:00:00 2001 From: messense Date: Tue, 4 Mar 2025 17:44:07 +0800 Subject: [PATCH 465/495] Add supported CPython/PyPy versions to cargo package metadata (#4756) * Add supported CPython/PyPy versions to cargo package metadata * Update max pypy version to 3.11 Co-authored-by: David Hewitt * Address code review comments --------- Co-authored-by: David Hewitt --- Cargo.toml | 4 ++-- newsfragments/4756.packaging.md | 1 + noxfile.py | 41 ++++++++++++++++++++++++++++----- pyo3-ffi/Cargo.toml | 8 +++++++ 4 files changed, 46 insertions(+), 8 deletions(-) create mode 100644 newsfragments/4756.packaging.md diff --git a/Cargo.toml b/Cargo.toml index 3b4fa81e56b..69ef0311d5f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,7 +42,7 @@ indexmap = { version = ">= 2.5.0, < 3", optional = true } jiff-02 = { package = "jiff", version = "0.2", optional = true } num-bigint = { version = "0.4.2", optional = true } num-complex = { version = ">= 0.4.6, < 0.5", optional = true } -num-rational = {version = "0.4.1", optional = true } +num-rational = { version = "0.4.1", optional = true } rust_decimal = { version = "1.15", default-features = false, optional = true } serde = { version = "1.0", optional = true } smallvec = { version = "1.0", optional = true } @@ -65,7 +65,7 @@ rayon = "1.6.1" futures = "0.3.28" tempfile = "3.12.0" static_assertions = "1.1.0" -uuid = {version = "1.10.0", features = ["v4"] } +uuid = { version = "1.10.0", features = ["v4"] } [build-dependencies] pyo3-build-config = { path = "pyo3-build-config", version = "=0.23.5", features = ["resolve-config"] } diff --git a/newsfragments/4756.packaging.md b/newsfragments/4756.packaging.md new file mode 100644 index 00000000000..a3259c1fc42 --- /dev/null +++ b/newsfragments/4756.packaging.md @@ -0,0 +1 @@ +Add supported CPython/PyPy versions to cargo package metadata. diff --git a/noxfile.py b/noxfile.py index a233f778a59..11506d541ff 100644 --- a/noxfile.py +++ b/noxfile.py @@ -17,6 +17,7 @@ Iterable, Iterator, List, + Literal, Optional, Tuple, Generator, @@ -41,11 +42,43 @@ PYO3_GUIDE_SRC = PYO3_DIR / "guide" / "src" PYO3_GUIDE_TARGET = PYO3_TARGET / "guide" PYO3_DOCS_TARGET = PYO3_TARGET / "doc" -PY_VERSIONS = ("3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13") -PYPY_VERSIONS = ("3.9", "3.10", "3.11") FREE_THREADED_BUILD = bool(sysconfig.get_config_var("Py_GIL_DISABLED")) +def _get_output(*args: str) -> str: + return subprocess.run(args, capture_output=True, text=True, check=True).stdout + + +def _parse_supported_interpreter_version( + python_impl: Literal["cpython", "pypy"], +) -> Tuple[str, str]: + output = _get_output("cargo", "metadata", "--format-version=1", "--no-deps") + cargo_packages = json.loads(output)["packages"] + # Check Python interpreter version support in package metadata + package = "pyo3-ffi" + metadata = next(pkg["metadata"] for pkg in cargo_packages if pkg["name"] == package) + version_info = metadata[python_impl] + assert "min-version" in version_info, f"missing min-version for {python_impl}" + assert "max-version" in version_info, f"missing max-version for {python_impl}" + return version_info["min-version"], version_info["max-version"] + + +def _supported_interpreter_versions( + python_impl: Literal["cpython", "pypy"], +) -> List[str]: + min_version, max_version = _parse_supported_interpreter_version(python_impl) + major = int(min_version.split(".")[0]) + assert major == 3, f"unsupported Python major version {major}" + min_minor = int(min_version.split(".")[1]) + max_minor = int(max_version.split(".")[1]) + versions = [f"{major}.{minor}" for minor in range(min_minor, max_minor + 1)] + return versions + + +PY_VERSIONS = _supported_interpreter_versions("cpython") +PYPY_VERSIONS = _supported_interpreter_versions("pypy") + + @nox.session(venv_backend="none") def test(session: nox.Session) -> None: test_rust(session) @@ -931,10 +964,6 @@ def _run_cargo_set_package_version( _run(session, *command, external=True) -def _get_output(*args: str) -> str: - return subprocess.run(args, capture_output=True, text=True, check=True).stdout - - def _for_all_version_configs( session: nox.Session, job: Callable[[Dict[str, str]], None] ) -> None: diff --git a/pyo3-ffi/Cargo.toml b/pyo3-ffi/Cargo.toml index 3dd5a711eb6..656146e12aa 100644 --- a/pyo3-ffi/Cargo.toml +++ b/pyo3-ffi/Cargo.toml @@ -46,3 +46,11 @@ pyo3-build-config = { path = "../pyo3-build-config", version = "=0.23.5", featur [lints] workspace = true + +[package.metadata.cpython] +min-version = "3.7" +max-version = "3.13" # inclusive + +[package.metadata.pypy] +min-version = "3.9" +max-version = "3.11" # inclusive From 98a6faf99f699ab91161f78587bca1ab1d6535ef Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Thu, 6 Mar 2025 20:26:28 +0100 Subject: [PATCH 466/495] bump `inventory` minimum version (#4954) --- Cargo.toml | 2 +- newsfragments/4954.packaging.md | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 newsfragments/4954.packaging.md diff --git a/Cargo.toml b/Cargo.toml index 69ef0311d5f..66683ff74b9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,7 +29,7 @@ indoc = { version = "2.0.1", optional = true } unindent = { version = "0.2.1", optional = true } # support crate for multiple-pymethods feature -inventory = { version = "0.3.0", optional = true } +inventory = { version = "0.3.5", optional = true } # crate integrations that can be added using the eponymous features anyhow = { version = "1.0.1", optional = true } diff --git a/newsfragments/4954.packaging.md b/newsfragments/4954.packaging.md new file mode 100644 index 00000000000..bfaa4174ef8 --- /dev/null +++ b/newsfragments/4954.packaging.md @@ -0,0 +1 @@ +bump minimum supported `inventory` version to 0.3.5 \ No newline at end of file From 43ada7ce76d41044fb597f71c8a97a398e10afe5 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Fri, 7 Mar 2025 16:40:30 +0000 Subject: [PATCH 467/495] ignore `PyConfig` struct in `ffi-check` on 3.13 for now (#4961) * ignore `PyConfig` struct in `ffi-check` on 3.13 for now * fixup --- pyo3-ffi-check/macro/src/lib.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/pyo3-ffi-check/macro/src/lib.rs b/pyo3-ffi-check/macro/src/lib.rs index e3d442c3703..8438393b9eb 100644 --- a/pyo3-ffi-check/macro/src/lib.rs +++ b/pyo3-ffi-check/macro/src/lib.rs @@ -9,6 +9,11 @@ const PY_3_12: PythonVersion = PythonVersion { minor: 12, }; +const PY_3_13: PythonVersion = PythonVersion { + major: 3, + minor: 13, +}; + /// Macro which expands to multiple macro calls, one per pyo3-ffi struct. #[proc_macro] pub fn for_all_structs(input: proc_macro::TokenStream) -> proc_macro::TokenStream { @@ -49,6 +54,14 @@ pub fn for_all_structs(input: proc_macro::TokenStream) -> proc_macro::TokenStrea .unwrap() .strip_suffix(".html") .unwrap(); + + if struct_name == "PyConfig" && pyo3_build_config::get().version == PY_3_13 { + // https://github.com/python/cpython/issues/130940 + // PyConfig has an ABI break on Python 3.13.1 -> 3.13.2, waiting for advice + // how to proceed in PyO3. + continue; + } + let struct_ident = Ident::new(struct_name, Span::call_site()); output.extend(quote!(#macro_name!(#struct_ident);)); } From bdc372fbfb117bde6c55b8f0c1b7412fa49d8f31 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Fri, 7 Mar 2025 19:49:10 +0000 Subject: [PATCH 468/495] remove `Deref` implementations for old "gil refs" types (#4593) * remove `Deref` implementations for old "gil refs" types * add changelog entries --- newsfragments/4593.changed.md | 1 + newsfragments/4593.removed.md | 1 + src/exceptions.rs | 2 ++ src/instance.rs | 4 ++-- src/types/mod.rs | 16 ---------------- 5 files changed, 6 insertions(+), 18 deletions(-) create mode 100644 newsfragments/4593.changed.md create mode 100644 newsfragments/4593.removed.md diff --git a/newsfragments/4593.changed.md b/newsfragments/4593.changed.md new file mode 100644 index 00000000000..e384854e918 --- /dev/null +++ b/newsfragments/4593.changed.md @@ -0,0 +1 @@ +Use `DerefToPyAny` in blanket implementations of `From>` and `From>` for `PyObject`. diff --git a/newsfragments/4593.removed.md b/newsfragments/4593.removed.md new file mode 100644 index 00000000000..de1c0fb45cb --- /dev/null +++ b/newsfragments/4593.removed.md @@ -0,0 +1 @@ +Remove implementations of `Deref` for `PyAny` and other "native" types. diff --git a/src/exceptions.rs b/src/exceptions.rs index 6f0fa3e674c..66044a6658f 100644 --- a/src/exceptions.rs +++ b/src/exceptions.rs @@ -274,6 +274,7 @@ macro_rules! create_exception_type_object { macro_rules! impl_native_exception ( ($name:ident, $exc_name:ident, $doc:expr, $layout:path $(, #checkfunction=$checkfunction:path)?) => ( #[doc = $doc] + #[repr(transparent)] #[allow(clippy::upper_case_acronyms)] pub struct $name($crate::PyAny); @@ -291,6 +292,7 @@ macro_rules! impl_windows_native_exception ( ($name:ident, $exc_name:ident, $doc:expr, $layout:path) => ( #[cfg(windows)] #[doc = $doc] + #[repr(transparent)] #[allow(clippy::upper_case_acronyms)] pub struct $name($crate::PyAny); diff --git a/src/instance.rs b/src/instance.rs index 08a8779eadc..f3fe7a91783 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -1798,7 +1798,7 @@ unsafe impl crate::AsPyPointer for Py { impl std::convert::From> for PyObject where - T: AsRef, + T: DerefToPyAny, { #[inline] fn from(other: Py) -> Self { @@ -1808,7 +1808,7 @@ where impl std::convert::From> for PyObject where - T: AsRef, + T: DerefToPyAny, { #[inline] fn from(other: Bound<'_, T>) -> Self { diff --git a/src/types/mod.rs b/src/types/mod.rs index aa2ef0d07d3..e3a317ea33a 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -121,22 +121,6 @@ pub trait DerefToPyAny { #[macro_export] macro_rules! pyobject_native_type_named ( ($name:ty $(;$generics:ident)*) => { - impl<$($generics,)*> ::std::convert::AsRef<$crate::PyAny> for $name { - #[inline] - fn as_ref(&self) -> &$crate::PyAny { - &self.0 - } - } - - impl<$($generics,)*> ::std::ops::Deref for $name { - type Target = $crate::PyAny; - - #[inline] - fn deref(&self) -> &$crate::PyAny { - &self.0 - } - } - impl $crate::types::DerefToPyAny for $name {} }; ); From c06883105abd1bd1be6b4f820dcb9039aa138c2d Mon Sep 17 00:00:00 2001 From: Alejandro Perez Gancedo <37455131+LifeLex@users.noreply.github.com> Date: Fri, 7 Mar 2025 20:40:19 +0000 Subject: [PATCH 469/495] docs: added docs on debugging using breakpoints (#4943) * docs: added docs on debugging using breakpoints * docs: added missing code type md * Update guide/src/debugging.md Co-authored-by: Nathan Goldbaum * docs: refactor debugging section to address comments * docs: install mdbook-tabs and update docs --------- Co-authored-by: Alejandro Perez Gancedo Co-authored-by: Nathan Goldbaum --- .github/workflows/gh-pages.yml | 2 +- .netlify/build.sh | 8 + Contributing.md | 3 +- guide/book.toml | 4 + guide/src/debugging.md | 299 ++++++++++++++++++++++++++++++++- guide/theme/tabs.css | 25 +++ guide/theme/tabs.js | 75 +++++++++ 7 files changed, 409 insertions(+), 7 deletions(-) create mode 100644 guide/theme/tabs.css create mode 100644 guide/theme/tabs.js diff --git a/.github/workflows/gh-pages.yml b/.github/workflows/gh-pages.yml index 1050b97f314..21b33c93796 100644 --- a/.github/workflows/gh-pages.yml +++ b/.github/workflows/gh-pages.yml @@ -30,7 +30,7 @@ jobs: - name: Setup mdBook uses: taiki-e/install-action@v2 with: - tool: mdbook,lychee + tool: mdbook,mdbook-tabs,lychee - name: Prepare tag id: prepare_tag diff --git a/.netlify/build.sh b/.netlify/build.sh index a61180be59a..97b7bd72414 100755 --- a/.netlify/build.sh +++ b/.netlify/build.sh @@ -89,6 +89,14 @@ if [ "${INSTALLED_MDBOOK_LINKCHECK_VERSION}" != "mdbook v${MDBOOK_LINKCHECK_VERS cargo install mdbook-linkcheck@${MDBOOK_LINKCHECK_VERSION} --force fi +# Install latest mdbook-tabs. Netlify will cache the cargo bin dir, so this will +# only build mdbook-tabs if needed. +MDBOOK_TABS_VERSION=$(cargo search mdbook-tabs --limit 1 | head -1 | tr -s ' ' | cut -d ' ' -f 3 | tr -d '"') +INSTALLED_MDBOOK_TABS_VERSION=$(mdbook-tabs --version || echo "none") +if [ "${INSTALLED_MDBOOK_TABS_VERSION}" != "mdbook-tabs v${MDBOOK_TABS_VERSION}" ]; then + cargo install mdbook-tabs@${MDBOOK_TABS_VERSION} --force +fi + pip install nox nox -s build-guide mv target/guide/ netlify_build/main/ diff --git a/Contributing.md b/Contributing.md index 76af08325fb..054ef42fb88 100644 --- a/Contributing.md +++ b/Contributing.md @@ -60,7 +60,7 @@ https://doc.rust-lang.org/rustdoc/documentation-tests.html for a guide on doctes You can preview the user guide by building it locally with `mdbook`. -First, install [`mdbook`][mdbook] and [`nox`][nox]. Then, run +First, install [`mdbook`][mdbook], the [`mdbook-tabs`][mdbook-tabs] plugin and [`nox`][nox]. Then, run ```shell nox -s build-guide -- --open @@ -235,5 +235,6 @@ In the meanwhile, some of our maintainers have personal GitHub sponsorship pages - [messense](https://github.com/sponsors/messense) [mdbook]: https://rust-lang.github.io/mdBook/cli/index.html +[mdbook-tabs]: https://mdbook-plugins.rustforweb.org/tabs.html [lychee]: https://github.com/lycheeverse/lychee [nox]: https://github.com/theacodes/nox diff --git a/guide/book.toml b/guide/book.toml index bccc3506098..be682a64eab 100644 --- a/guide/book.toml +++ b/guide/book.toml @@ -6,7 +6,11 @@ author = "PyO3 Project and Contributors" [preprocessor.pyo3_version] command = "python3 guide/pyo3_version.py" +[preprocessor.tabs] + [output.html] git-repository-url = "https://github.com/PyO3/pyo3/tree/main/guide" edit-url-template = "https://github.com/PyO3/pyo3/edit/main/guide/{path}" playground.runnable = false +additional-css = ["theme/tabs.css"] +additional-js = ["theme/tabs.js"] diff --git a/guide/src/debugging.md b/guide/src/debugging.md index 00c22631c3b..02f1e9951de 100644 --- a/guide/src/debugging.md +++ b/guide/src/debugging.md @@ -34,14 +34,303 @@ Run Valgrind with `valgrind --suppressions=valgrind-python.supp ./my-command --w The best start to investigate a crash such as an segmentation fault is a backtrace. You can set `RUST_BACKTRACE=1` as an environment variable to get the stack trace on a `panic!`. Alternatively you can use a debugger such as `gdb` to explore the issue. Rust provides a wrapper, `rust-gdb`, which has pretty-printers for inspecting Rust variables. Since PyO3 uses `cdylib` for Python shared objects, it does not receive the pretty-print debug hooks in `rust-gdb` ([rust-lang/rust#96365](https://github.com/rust-lang/rust/issues/96365)). The mentioned issue contains a workaround for enabling pretty-printers in this case. - * Link against a debug build of python as described in the previous chapter - * Run `rust-gdb ` - * Set a breakpoint (`b`) on `rust_panic` if you are investigating a `panic!` - * Enter `r` to run - * After the crash occurred, enter `bt` or `bt full` to print the stacktrace +* Link against a debug build of python as described in the previous chapter +* Run `rust-gdb ` +* Set a breakpoint (`b`) on `rust_panic` if you are investigating a `panic!` +* Enter `r` to run +* After the crash occurred, enter `bt` or `bt full` to print the stacktrace Often it is helpful to run a small piece of Python code to exercise a section of Rust. ```console rust-gdb --args python -c "import my_package; my_package.sum_to_string(1, 2)" ``` + +## Setting breakpoints in your Rust code + +One of the preferred ways by developers to debug their code is by setting breakpoints. This can be achieved in PyO3 by using a debugger like `rust-gdb` or `rust-lldb` with your Python interpreter. + +For more information about how to use both `lldb` and `gdb` you can read the [gdb to lldb command map](https://lldb.llvm.org/use/map.html) from the lldb documentation. + +### Common setup + +1. Compile your extension with debug symbols: + + ```bash + # Debug is the default for maturin, but you can explicitly ensure debug symbols with: + RUSTFLAGS="-g" maturin develop + + # For setuptools-rust users: + pip install -e . + ``` + + > **Note**: When using debuggers, make sure that `python` resolves to an actual Python binary or symlink and not a shim script. Some tools like pyenv use shim scripts which can interfere with debugging. + +### Debugger specific setup + +Depeding on your OS and your preferences you can use two different debuggers, `rust-gdb` or `rust-lldb`. + +{{#tabs }} +{{#tab name="Using rust-gdb" }} + +1. Launch rust-gdb with the Python interpreter: + + ```bash + rust-gdb --args python + ``` + +2. Once in gdb, set a breakpoint in your Rust code: + + ```bash + (gdb) break your_module.rs:42 + ``` + +3. Run your Python script that imports and uses your Rust extension: + + ```bash + # Option 1: Run an inline Python command + (gdb) run -c "import your_module; your_module.your_function()" + + # Option 2: Run a Python script + (gdb) run your_script.py + + # Option 3: Run pytest tests + (gdb) run -m pytest tests/test_something.py::TestName + ``` + +{{#endtab }} +{{#tab name="Using rust-lldb (for macOS users)" }} + +1. Start rust-lldb with Python: + + ```bash + rust-lldb -- python + ``` + +2. Set breakpoints in your Rust code: + + ```bash + (lldb) breakpoint set --file your_module.rs --line 42 + ``` + +3. Run your Python script: + + ```bash + # Option 1: Run an inline Python command + (lldb) run -c "import your_module; your_module.your_function()" + + # Option 2: Run a Python script + (lldb) run your_script.py + + # Option 3: Run pytest tests + (lldb) run -m pytest tests/test_something.py::TestName + ``` + +{{#endtab }} +{{#endtabs }} + +### Using VS Code + +VS Code with the Rust and Python extensions provides an integrated debugging experience: + +1. First, install the necessary VS Code extensions: + * [Rust Analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer) + * [CodeLLDB](https://marketplace.visualstudio.com/items?itemName=vadimcn.vscode-lldb) + * [Python](https://marketplace.visualstudio.com/items?itemName=ms-python.python) + +2. Create a `.vscode/launch.json` file with a configuration that uses the LLDB Debug Launcher: + + ```json + { + "version": "0.2.0", + "configurations": [ + { + "name": "Debug PyO3", + "type": "lldb", + "request": "attach", + "program": "${workspaceFolder}/.venv/bin/python", + "pid": "${command:pickProcess}", + "sourceLanguages": [ + "rust" + ] + }, + { + "name": "Launch Python with PyO3", + "type": "lldb", + "request": "launch", + "program": "${workspaceFolder}/.venv/bin/python", + "args": ["${file}"], + "cwd": "${workspaceFolder}", + "sourceLanguages": ["rust"] + }, + { + "name": "Debug PyO3 with Args", + "type": "lldb", + "request": "launch", + "program": "${workspaceFolder}/.venv/bin/python", + "args": ["path/to/your/script.py", "arg1", "arg2"], + "cwd": "${workspaceFolder}", + "sourceLanguages": ["rust"] + }, + { + "name": "Debug PyO3 Tests", + "type": "lldb", + "request": "launch", + "program": "${workspaceFolder}/.venv/bin/python", + "args": ["-m", "pytest", "tests/your_test.py::test_function", "-v"], + "cwd": "${workspaceFolder}", + "sourceLanguages": ["rust"] + } + ] + } + ``` + + This configuration supports multiple debugging scenarios: + * Attaching to a running Python process + * Launching the currently open Python file + * Running a specific script with command-line arguments + * Running pytest tests + +3. Set breakpoints in your Rust code by clicking in the gutter next to line numbers. + +4. Start debugging: + * For attaching to a running Python process: First start the process, then select the "Debug PyO3" configuration and click Start Debugging (F5). You'll be prompted to select the Python process to attach to. + * For launching a Python script: Open your Python script, select the "Launch Python with PyO3" configuration and click Start Debugging (F5). + * For running with arguments: Select "Debug PyO3 with Args" (remember to edit the configuration with your actual script path and arguments). + * For running tests: Select "Debug PyO3 Tests" (edit the test path as needed). + +5. When debugging PyO3 code: + * You can inspect Rust variables and data structures + * Use the debug console to evaluate expressions + * Step through Rust code line by line using the step controls + * Set conditional breakpoints for more complex debugging scenarios + +### Advanced Debugging Configurations + +For advanced debugging scenarios, you might want to add environment variables or enable specific Rust debug flags: + +```json +{ + "name": "Debug PyO3 with Environment", + "type": "lldb", + "request": "launch", + "program": "${workspaceFolder}/.venv/bin/python", + "args": ["${file}"], + "env": { + "RUST_BACKTRACE": "1", + "PYTHONPATH": "${workspaceFolder}" + }, + "sourceLanguages": ["rust"] +} +``` + +### Debugging from Jupyter Notebooks + +For Jupyter Notebooks run from VS Code, you can use the following helper functions to automate the launch configuration: + +```python +from pathlib import Path +import os +import json +import sys + + +def update_launch_json(vscode_config_file_path=None): + """Update VSCode launch.json with the correct Jupyter kernel PID. + + Args: + vscode_config_file_path (str, optional): Path to the .vscode/launch.json file. + If not provided, will use the current working directory. + """ + pid = get_jupyter_kernel_pid() + if not pid: + print("Could not determine Jupyter kernel PID.") + return + + # Determine launch.json path + if vscode_config_file_path: + launch_json_path = vscode_config_file_path + else: + launch_json_path = os.path.join(Path(os.getcwd()), ".vscode", "launch.json") + + # Get Python interpreter path + python_path = sys.executable + + # Default debugger config + debug_config = { + "version": "0.2.0", + "configurations": [ + { + "name": "Debug PyO3 (Jupyter)", + "type": "lldb", + "request": "attach", + "program": python_path, + "pid": pid, + "sourceLanguages": ["rust"], + }, + { + "name": "Launch Python with PyO3", + "type": "lldb", + "request": "launch", + "program": python_path, + "args": ["${file}"], + "cwd": "${workspaceFolder}", + "sourceLanguages": ["rust"] + } + ], + } + + # Create .vscode directory if it doesn't exist + try: + os.makedirs(os.path.dirname(launch_json_path), exist_ok=True) + + # If launch.json already exists, try to update it instead of overwriting + if os.path.exists(launch_json_path): + try: + with open(launch_json_path, "r") as f: + existing_config = json.load(f) + + # Check if our configuration already exists + config_exists = False + for config in existing_config.get("configurations", []): + if config.get("name") == "Debug PyO3 (Jupyter)": + config["pid"] = pid + config["program"] = python_path + config_exists = True + + if not config_exists: + existing_config.setdefault("configurations", []).append(debug_config["configurations"][0]) + + debug_config = existing_config + except Exception: + # If reading fails, we'll just overwrite with our new configuration + pass + + with open(launch_json_path, "w") as f: + json.dump(debug_config, f, indent=4) + print(f"Updated launch.json with PID: {pid} at {launch_json_path}") + except Exception as e: + print(f"Error updating launch.json: {e}") + + +def get_jupyter_kernel_pid(): + """Find the process ID (PID) of the running Jupyter kernel. + + Returns: + int: The process ID of the Jupyter kernel, or None if not found. + """ + # Check if we're running in a Jupyter environment + if 'ipykernel' in sys.modules: + pid = os.getpid() + print(f"Jupyter kernel PID: {pid}") + return pid + else: + print("Not running in a Jupyter environment.") + return None +``` + +To use these functions: + +1. Run the cell containing these functions in your Jupyter notebook +2. Run `update_launch_json()` in a cell +3. In VS Code, select the "Debug PyO3 (Jupyter)" configuration and start debugging diff --git a/guide/theme/tabs.css b/guide/theme/tabs.css new file mode 100644 index 00000000000..8712b859c0b --- /dev/null +++ b/guide/theme/tabs.css @@ -0,0 +1,25 @@ +.mdbook-tabs { + display: flex; +} + +.mdbook-tab { + background-color: var(--table-alternate-bg); + padding: 0.5rem 1rem; + cursor: pointer; + border: none; + font-size: 1.6rem; + line-height: 1.45em; +} + +.mdbook-tab.active { + background-color: var(--table-header-bg); + font-weight: bold; +} + +.mdbook-tab-content { + padding: 1rem 0rem; +} + +.mdbook-tab-content table { + margin: unset; +} diff --git a/guide/theme/tabs.js b/guide/theme/tabs.js new file mode 100644 index 00000000000..8ba5e878c39 --- /dev/null +++ b/guide/theme/tabs.js @@ -0,0 +1,75 @@ +/** + * Change active tab of tabs. + * + * @param {Element} container + * @param {string} name + */ +const changeTab = (container, name) => { + for (const child of container.children) { + if (!(child instanceof HTMLElement)) { + continue; + } + + if (child.classList.contains('mdbook-tabs')) { + for (const tab of child.children) { + if (!(tab instanceof HTMLElement)) { + continue; + } + + if (tab.dataset.tabname === name) { + tab.classList.add('active'); + } else { + tab.classList.remove('active'); + } + } + } else if (child.classList.contains('mdbook-tab-content')) { + if (child.dataset.tabname === name) { + child.classList.remove('hidden'); + } else { + child.classList.add('hidden'); + } + } + } +}; + +document.addEventListener('DOMContentLoaded', () => { + const tabs = document.querySelectorAll('.mdbook-tab'); + for (const tab of tabs) { + tab.addEventListener('click', () => { + if (!(tab instanceof HTMLElement)) { + return; + } + + if (!tab.parentElement || !tab.parentElement.parentElement) { + return; + } + + const container = tab.parentElement.parentElement; + const name = tab.dataset.tabname; + const global = container.dataset.tabglobal; + + changeTab(container, name); + + if (global) { + localStorage.setItem(`mdbook-tabs-${global}`, name); + + const globalContainers = document.querySelectorAll( + `.mdbook-tabs-container[data-tabglobal="${global}"]` + ); + for (const globalContainer of globalContainers) { + changeTab(globalContainer, name); + } + } + }); + } + + const containers = document.querySelectorAll('.mdbook-tabs-container[data-tabglobal]'); + for (const container of containers) { + const global = container.dataset.tabglobal; + + const name = localStorage.getItem(`mdbook-tabs-${global}`); + if (name && document.querySelector(`.mdbook-tab[data-tabname=${name}]`)) { + changeTab(container, name); + } + } +}); From 295e67a838027a22d8a4678e5f92252bfeeea9f0 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers <7943856+bschoenmaeckers@users.noreply.github.com> Date: Fri, 7 Mar 2025 21:49:15 +0100 Subject: [PATCH 470/495] improve signature of `ffi::PyIter_Send` & add `PyIterator::send` (#4746) Co-authored-by: David Hewitt --- newsfragments/4746.added.md | 1 + newsfragments/4746.fixed.md | 1 + pyo3-ffi/src/abstract_.rs | 6 +++- src/types/iterator.rs | 69 +++++++++++++++++++++++++++++++++++++ 4 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 newsfragments/4746.added.md create mode 100644 newsfragments/4746.fixed.md diff --git a/newsfragments/4746.added.md b/newsfragments/4746.added.md new file mode 100644 index 00000000000..43fbab18f2c --- /dev/null +++ b/newsfragments/4746.added.md @@ -0,0 +1 @@ +Added `PyIterator::send` method to allow sending values into a python generator. diff --git a/newsfragments/4746.fixed.md b/newsfragments/4746.fixed.md new file mode 100644 index 00000000000..51611432e30 --- /dev/null +++ b/newsfragments/4746.fixed.md @@ -0,0 +1 @@ +Fixed the return value of pyo3-ffi's PyIter_Send() function to return PySendResult. \ No newline at end of file diff --git a/pyo3-ffi/src/abstract_.rs b/pyo3-ffi/src/abstract_.rs index 6e0f44ddd6f..a79ec43f271 100644 --- a/pyo3-ffi/src/abstract_.rs +++ b/pyo3-ffi/src/abstract_.rs @@ -149,7 +149,11 @@ extern "C" { pub fn PyIter_Next(arg1: *mut PyObject) -> *mut PyObject; #[cfg(all(not(PyPy), Py_3_10))] #[cfg_attr(PyPy, link_name = "PyPyIter_Send")] - pub fn PyIter_Send(iter: *mut PyObject, arg: *mut PyObject, presult: *mut *mut PyObject); + pub fn PyIter_Send( + iter: *mut PyObject, + arg: *mut PyObject, + presult: *mut *mut PyObject, + ) -> PySendResult; #[cfg_attr(PyPy, link_name = "PyPyNumber_Check")] pub fn PyNumber_Check(o: *mut PyObject) -> c_int; diff --git a/src/types/iterator.rs b/src/types/iterator.rs index 068ab1fce34..f6709a8f234 100644 --- a/src/types/iterator.rs +++ b/src/types/iterator.rs @@ -52,6 +52,35 @@ impl PyIterator { } } +#[derive(Debug)] +#[cfg(all(not(PyPy), Py_3_10))] +pub enum PySendResult<'py> { + Next(Bound<'py, PyAny>), + Return(Bound<'py, PyAny>), +} + +#[cfg(all(not(PyPy), Py_3_10))] +impl<'py> Bound<'py, PyIterator> { + /// Sends a value into a python generator. This is the equivalent of calling `generator.send(value)` in Python. + /// This resumes the generator and continues its execution until the next `yield` or `return` statement. + /// If the generator exits without returning a value, this function returns a `StopException`. + /// The first call to `send` must be made with `None` as the argument to start the generator, failing to do so will raise a `TypeError`. + #[inline] + pub fn send(&self, value: &Bound<'py, PyAny>) -> PyResult> { + let py = self.py(); + let mut result = std::ptr::null_mut(); + match unsafe { ffi::PyIter_Send(self.as_ptr(), value.as_ptr(), &mut result) } { + ffi::PySendResult::PYGEN_ERROR => Err(PyErr::fetch(py)), + ffi::PySendResult::PYGEN_RETURN => Ok(PySendResult::Return(unsafe { + result.assume_owned_unchecked(py) + })), + ffi::PySendResult::PYGEN_NEXT => Ok(PySendResult::Next(unsafe { + result.assume_owned_unchecked(py) + })), + } + } +} + impl<'py> Iterator for Bound<'py, PyIterator> { type Item = PyResult>; @@ -106,7 +135,11 @@ impl PyTypeCheck for PyIterator { #[cfg(test)] mod tests { use super::PyIterator; + #[cfg(all(not(PyPy), Py_3_10))] + use super::PySendResult; use crate::exceptions::PyTypeError; + #[cfg(all(not(PyPy), Py_3_10))] + use crate::types::PyNone; use crate::types::{PyAnyMethods, PyDict, PyList, PyListMethods}; use crate::{ffi, IntoPyObject, Python}; @@ -201,6 +234,42 @@ def fibonacci(target): }); } + #[test] + #[cfg(all(not(PyPy), Py_3_10))] + fn send_generator() { + let generator = ffi::c_str!( + r#" +def gen(): + value = None + while(True): + value = yield value + if value is None: + return +"# + ); + + Python::with_gil(|py| { + let context = PyDict::new(py); + py.run(generator, None, Some(&context)).unwrap(); + + let generator = py.eval(ffi::c_str!("gen()"), None, Some(&context)).unwrap(); + + let one = 1i32.into_pyobject(py).unwrap(); + assert!(matches!( + generator.try_iter().unwrap().send(&PyNone::get(py)).unwrap(), + PySendResult::Next(value) if value.is_none() + )); + assert!(matches!( + generator.try_iter().unwrap().send(&one).unwrap(), + PySendResult::Next(value) if value.is(&one) + )); + assert!(matches!( + generator.try_iter().unwrap().send(&PyNone::get(py)).unwrap(), + PySendResult::Return(value) if value.is_none() + )); + }); + } + #[test] fn fibonacci_generator_bound() { use crate::types::any::PyAnyMethods; From b726d6aeab3a35b273f0ade0bd4eed28823a09ff Mon Sep 17 00:00:00 2001 From: Ariel Ben-Yehuda Date: Sun, 9 Mar 2025 22:04:14 +0200 Subject: [PATCH 471/495] hang instead of pthread_exit during interpreter shutdown (#4874) * hang instead of pthread_exit during interpreter shutdown see https://github.com/python/cpython/issues/87135 and https://github.com/rust-lang/rust/issues/135929 * relnotes * fix warnings * version using pthread_cleanup_push * add tests * new attempt * clippy * comment * msrv * address review comments * update comment * add comment * try to skip test on debug builds --------- Co-authored-by: Ariel Ben-Yehuda Co-authored-by: David Hewitt --- newsfragments/4874.changed.md | 1 + pyo3-build-config/src/lib.rs | 8 ++- pyo3-ffi/src/pystate.rs | 68 +++++++++++++++++++++- pytests/src/misc.rs | 25 ++++++++ pytests/tests/test_hammer_gil_in_thread.py | 28 +++++++++ 5 files changed, 127 insertions(+), 3 deletions(-) create mode 100644 newsfragments/4874.changed.md create mode 100644 pytests/tests/test_hammer_gil_in_thread.py diff --git a/newsfragments/4874.changed.md b/newsfragments/4874.changed.md new file mode 100644 index 00000000000..fd483f43de5 --- /dev/null +++ b/newsfragments/4874.changed.md @@ -0,0 +1 @@ + * PyO3 threads now hang instead of `pthread_exit` trying to acquire the GIL when the interpreter is shutting down. This mimics the [Python 3.14](https://github.com/python/cpython/issues/87135) behavior and avoids undefined behavior and crashes. diff --git a/pyo3-build-config/src/lib.rs b/pyo3-build-config/src/lib.rs index 9070f6d7401..a908fe88068 100644 --- a/pyo3-build-config/src/lib.rs +++ b/pyo3-build-config/src/lib.rs @@ -180,6 +180,10 @@ pub fn print_feature_cfgs() { println!("cargo:rustc-cfg=rustc_has_once_lock"); } + if rustc_minor_version >= 71 { + println!("cargo:rustc-cfg=rustc_has_extern_c_unwind"); + } + // invalid_from_utf8 lint was added in Rust 1.74 if rustc_minor_version >= 74 { println!("cargo:rustc-cfg=invalid_from_utf8_lint"); @@ -226,12 +230,14 @@ pub fn print_expected_cfgs() { println!("cargo:rustc-check-cfg=cfg(diagnostic_namespace)"); println!("cargo:rustc-check-cfg=cfg(c_str_lit)"); println!("cargo:rustc-check-cfg=cfg(rustc_has_once_lock)"); + println!("cargo:rustc-check-cfg=cfg(rustc_has_extern_c_unwind)"); println!("cargo:rustc-check-cfg=cfg(io_error_more)"); println!("cargo:rustc-check-cfg=cfg(fn_ptr_eq)"); // allow `Py_3_*` cfgs from the minimum supported version up to the // maximum minor version (+1 for development for the next) - for i in impl_::MINIMUM_SUPPORTED_VERSION.minor..=impl_::ABI3_MAX_MINOR + 1 { + // FIXME: support cfg(Py_3_14) as well due to PyGILState_Ensure + for i in impl_::MINIMUM_SUPPORTED_VERSION.minor..=std::cmp::max(14, impl_::ABI3_MAX_MINOR + 1) { println!("cargo:rustc-check-cfg=cfg(Py_3_{i})"); } } diff --git a/pyo3-ffi/src/pystate.rs b/pyo3-ffi/src/pystate.rs index 0c062160ccc..a6caf421ff6 100644 --- a/pyo3-ffi/src/pystate.rs +++ b/pyo3-ffi/src/pystate.rs @@ -80,9 +80,73 @@ pub enum PyGILState_STATE { PyGILState_UNLOCKED, } +struct HangThread; + +impl Drop for HangThread { + fn drop(&mut self) { + loop { + #[cfg(target_family = "unix")] + unsafe { + libc::pause(); + } + #[cfg(not(target_family = "unix"))] + std::thread::sleep(std::time::Duration::from_secs(9_999_999)); + } + } +} + +// The PyGILState_Ensure function will call pthread_exit during interpreter shutdown, +// which causes undefined behavior. Redirect to the "safe" version that hangs instead, +// as Python 3.14 does. +// +// See https://github.com/rust-lang/rust/issues/135929 + +// C-unwind only supported (and necessary) since 1.71. Python 3.14+ does not do +// pthread_exit from PyGILState_Ensure (https://github.com/python/cpython/issues/87135). +mod raw { + #[cfg(all(not(Py_3_14), rustc_has_extern_c_unwind))] + extern "C-unwind" { + #[cfg_attr(PyPy, link_name = "PyPyGILState_Ensure")] + pub fn PyGILState_Ensure() -> super::PyGILState_STATE; + } + + #[cfg(not(all(not(Py_3_14), rustc_has_extern_c_unwind)))] + extern "C" { + #[cfg_attr(PyPy, link_name = "PyPyGILState_Ensure")] + pub fn PyGILState_Ensure() -> super::PyGILState_STATE; + } +} + +#[cfg(not(Py_3_14))] +pub unsafe extern "C" fn PyGILState_Ensure() -> PyGILState_STATE { + let guard = HangThread; + // If `PyGILState_Ensure` calls `pthread_exit`, which it does on Python < 3.14 + // when the interpreter is shutting down, this will cause a forced unwind. + // doing a forced unwind through a function with a Rust destructor is unspecified + // behavior. + // + // However, currently it runs the destructor, which will cause the thread to + // hang as it should. + // + // And if we don't catch the unwinding here, then one of our callers probably has a destructor, + // so it's unspecified behavior anyway, and on many configurations causes the process to abort. + // + // The alternative is for pyo3 to contain custom C or C++ code that catches the `pthread_exit`, + // but that's also annoying from a portability point of view. + // + // On Windows, `PyGILState_Ensure` calls `_endthreadex` instead, which AFAICT can't be caught + // and therefore will cause unsafety if there are pinned objects on the stack. AFAICT there's + // nothing we can do it other than waiting for Python 3.14 or not using Windows. At least, + // if there is nothing pinned on the stack, it won't cause the process to crash. + let ret: PyGILState_STATE = raw::PyGILState_Ensure(); + std::mem::forget(guard); + ret +} + +#[cfg(Py_3_14)] +pub use self::raw::PyGILState_Ensure; + extern "C" { - #[cfg_attr(PyPy, link_name = "PyPyGILState_Ensure")] - pub fn PyGILState_Ensure() -> PyGILState_STATE; #[cfg_attr(PyPy, link_name = "PyPyGILState_Release")] pub fn PyGILState_Release(arg1: PyGILState_STATE); #[cfg(not(PyPy))] diff --git a/pytests/src/misc.rs b/pytests/src/misc.rs index e44d1aa0ecf..b3ef5ee283e 100644 --- a/pytests/src/misc.rs +++ b/pytests/src/misc.rs @@ -9,6 +9,30 @@ fn issue_219() { Python::with_gil(|_| {}); } +#[pyclass] +struct LockHolder { + #[allow(unused)] + // Mutex needed for the MSRV + sender: std::sync::Mutex>, +} + +// This will hammer the GIL once the LockHolder is dropped. +#[pyfunction] +fn hammer_gil_in_thread() -> LockHolder { + let (sender, receiver) = std::sync::mpsc::channel(); + std::thread::spawn(move || { + receiver.recv().ok(); + // now the interpreter has shut down, so hammer the GIL. In buggy + // versions of PyO3 this will cause a crash. + loop { + Python::with_gil(|_py| ()); + } + }); + LockHolder { + sender: std::sync::Mutex::new(sender), + } +} + #[pyfunction] fn get_type_fully_qualified_name<'py>(obj: &Bound<'py, PyAny>) -> PyResult> { obj.get_type().fully_qualified_name() @@ -35,6 +59,7 @@ fn get_item_and_run_callback(dict: Bound<'_, PyDict>, callback: Bound<'_, PyAny> #[pymodule(gil_used = false)] pub fn misc(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(issue_219, m)?)?; + m.add_function(wrap_pyfunction!(hammer_gil_in_thread, m)?)?; m.add_function(wrap_pyfunction!(get_type_fully_qualified_name, m)?)?; m.add_function(wrap_pyfunction!(accepts_bool, m)?)?; m.add_function(wrap_pyfunction!(get_item_and_run_callback, m)?)?; diff --git a/pytests/tests/test_hammer_gil_in_thread.py b/pytests/tests/test_hammer_gil_in_thread.py new file mode 100644 index 00000000000..9eed640f65c --- /dev/null +++ b/pytests/tests/test_hammer_gil_in_thread.py @@ -0,0 +1,28 @@ +import sysconfig + +import pytest + +from pyo3_pytests import misc + + +def make_loop(): + # create a reference loop that will only be destroyed when the GC is called at the end + # of execution + start = [] + cur = [start] + for _ in range(1000 * 1000 * 10): + cur = [cur] + start.append(cur) + return start + + +# set a bomb that will explode when modules are cleaned up +loopy = [make_loop()] + + +@pytest.mark.skipif( + sysconfig.get_config_var("Py_DEBUG"), + reason="causes a crash on debug builds, see discussion in https://github.com/PyO3/pyo3/pull/4874", +) +def test_hammer_gil(): + loopy.append(misc.hammer_gil_in_thread()) From fa15d9094034b94b50d45917053425ade0a9963b Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Sun, 9 Mar 2025 21:34:47 +0000 Subject: [PATCH 472/495] fix 3.7 builds (#4963) --- noxfile.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/noxfile.py b/noxfile.py index 11506d541ff..418f9194ddd 100644 --- a/noxfile.py +++ b/noxfile.py @@ -17,12 +17,12 @@ Iterable, Iterator, List, - Literal, Optional, Tuple, Generator, ) + import nox import nox.command @@ -50,7 +50,7 @@ def _get_output(*args: str) -> str: def _parse_supported_interpreter_version( - python_impl: Literal["cpython", "pypy"], + python_impl: str, # Literal["cpython", "pypy"], TODO update after 3.7 dropped ) -> Tuple[str, str]: output = _get_output("cargo", "metadata", "--format-version=1", "--no-deps") cargo_packages = json.loads(output)["packages"] @@ -64,7 +64,7 @@ def _parse_supported_interpreter_version( def _supported_interpreter_versions( - python_impl: Literal["cpython", "pypy"], + python_impl: str, # Literal["cpython", "pypy"], TODO update after 3.7 dropped ) -> List[str]: min_version, max_version = _parse_supported_interpreter_version(python_impl) major = int(min_version.split(".")[0]) From 059e249057e895cfa36504890dfa79943f0b49c5 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Sun, 9 Mar 2025 22:35:01 +0000 Subject: [PATCH 473/495] release: 0.24.0 (#4959) Co-authored-by: Icxolu <10486322+Icxolu@users.noreply.github.com> --- CHANGELOG.md | 52 ++++++++++++++++++- Cargo.toml | 8 +-- README.md | 4 +- 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/4593.changed.md | 1 - newsfragments/4593.removed.md | 1 - newsfragments/4729.removed.md | 1 - newsfragments/4730.removed.md | 1 - newsfragments/4746.added.md | 1 - newsfragments/4746.fixed.md | 1 - newsfragments/4747.changed.md | 1 - newsfragments/4756.packaging.md | 1 - newsfragments/4768.added.md | 1 - newsfragments/4768.changed.md | 1 - newsfragments/4810.added.md | 1 - newsfragments/4822.changed.md | 1 - newsfragments/4823.added.md | 1 - newsfragments/4829.added.md | 1 - newsfragments/4850.added.md | 1 - newsfragments/4853.added.md | 1 - newsfragments/4860.changed.md | 1 - newsfragments/4864.added.md | 1 - newsfragments/4866.added.md | 1 - newsfragments/4874.changed.md | 1 - newsfragments/4878.added.md | 2 - newsfragments/4897.added.md | 1 - newsfragments/4900.changed.md | 1 - newsfragments/4917.added.md | 2 - newsfragments/4925.changed.md | 1 - newsfragments/4934.added.md | 1 - newsfragments/4941.added.md | 1 - newsfragments/4948.fixed.md | 1 - newsfragments/4949.fixed.md | 1 - newsfragments/4954.packaging.md | 1 - pyo3-build-config/Cargo.toml | 2 +- pyo3-ffi/Cargo.toml | 4 +- pyo3-ffi/README.md | 4 +- pyo3-macros-backend/Cargo.toml | 6 +-- pyo3-macros/Cargo.toml | 4 +- pyproject.toml | 2 +- tests/ui/reject_generics.stderr | 4 +- 45 files changed, 75 insertions(+), 57 deletions(-) delete mode 100644 newsfragments/4593.changed.md delete mode 100644 newsfragments/4593.removed.md delete mode 100644 newsfragments/4729.removed.md delete mode 100644 newsfragments/4730.removed.md delete mode 100644 newsfragments/4746.added.md delete mode 100644 newsfragments/4746.fixed.md delete mode 100644 newsfragments/4747.changed.md delete mode 100644 newsfragments/4756.packaging.md delete mode 100644 newsfragments/4768.added.md delete mode 100644 newsfragments/4768.changed.md delete mode 100644 newsfragments/4810.added.md delete mode 100644 newsfragments/4822.changed.md delete mode 100644 newsfragments/4823.added.md delete mode 100644 newsfragments/4829.added.md delete mode 100644 newsfragments/4850.added.md delete mode 100644 newsfragments/4853.added.md delete mode 100644 newsfragments/4860.changed.md delete mode 100644 newsfragments/4864.added.md delete mode 100644 newsfragments/4866.added.md delete mode 100644 newsfragments/4874.changed.md delete mode 100644 newsfragments/4878.added.md delete mode 100644 newsfragments/4897.added.md delete mode 100644 newsfragments/4900.changed.md delete mode 100644 newsfragments/4917.added.md delete mode 100644 newsfragments/4925.changed.md delete mode 100644 newsfragments/4934.added.md delete mode 100644 newsfragments/4941.added.md delete mode 100644 newsfragments/4948.fixed.md delete mode 100644 newsfragments/4949.fixed.md delete mode 100644 newsfragments/4954.packaging.md diff --git a/CHANGELOG.md b/CHANGELOG.md index b08db4d36d6..1532d3a7e9f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,55 @@ To see unreleased changes, please see the [CHANGELOG on the main branch guide](h +## [0.24.0] - 2025-03-09 + +### Packaging + +- Add supported CPython/PyPy versions to cargo package metadata. [#4756](https://github.com/PyO3/pyo3/pull/4756) +- Bump `target-lexicon` dependency to 0.13. [#4822](https://github.com/PyO3/pyo3/pull/4822) +- Add optional `jiff` dependency to add conversions for `jiff` datetime types. [#4823](https://github.com/PyO3/pyo3/pull/4823) +- Bump minimum supported `inventory` version to 0.3.5. [#4954](https://github.com/PyO3/pyo3/pull/4954) + +### Added + +- Add `PyIterator::send` method to allow sending values into a python generator. [#4746](https://github.com/PyO3/pyo3/pull/4746) +- Add `PyCallArgs` trait for passing arguments into the Python calling protocol. This enabled using a faster calling convention for certain types, improving performance. [#4768](https://github.com/PyO3/pyo3/pull/4768) +- Add `#[pyo3(default = ...']` option for `#[derive(FromPyObject)]` to set a default value for extracted fields of named structs. [#4829](https://github.com/PyO3/pyo3/pull/4829) +- Add `#[pyo3(into_py_with = ...)]` option for `#[derive(IntoPyObject, IntoPyObjectRef)]`. [#4850](https://github.com/PyO3/pyo3/pull/4850) +- Add uuid to/from python conversions. [#4864](https://github.com/PyO3/pyo3/pull/4864) +- Add FFI definitions `PyThreadState_GetFrame` and `PyFrame_GetBack`. [#4866](https://github.com/PyO3/pyo3/pull/4866) +- Optimize `last` for `BoundListIterator`, `BoundTupleIterator` and `BorrowedTupleIterator`. [#4878](https://github.com/PyO3/pyo3/pull/4878) +- Optimize `Iterator::count()` for `PyDict`, `PyList`, `PyTuple` & `PySet`. [#4878](https://github.com/PyO3/pyo3/pull/4878) +- Optimize `nth`, `nth_back`, `advance_by` and `advance_back_by` for `BoundTupleIterator` [#4897](https://github.com/PyO3/pyo3/pull/4897) +- Add support for `types.GenericAlias` as `pyo3::types::PyGenericAlias`. [#4917](https://github.com/PyO3/pyo3/pull/4917) +- Add `MutextExt` trait to help avoid deadlocks with the GIL while locking a `std::sync::Mutex`. [#4934](https://github.com/PyO3/pyo3/pull/4934) +- Add `#[pyo3(rename_all = "...")]` option for `#[derive(FromPyObject)]`. [#4941](https://github.com/PyO3/pyo3/pull/4941) + +### Changed + +- Optimize `nth`, `nth_back`, `advance_by` and `advance_back_by` for `BoundListIterator`. [#4810](https://github.com/PyO3/pyo3/pull/4810) +- Use `DerefToPyAny` in blanket implementations of `From>` and `From>` for `PyObject`. [#4593](https://github.com/PyO3/pyo3/pull/4593) +- Map `io::ErrorKind::IsADirectory`/`NotADirectory` to the corresponding Python exception on Rust 1.83+. [#4747](https://github.com/PyO3/pyo3/pull/4747) +- `PyAnyMethods::call` and friends now require `PyCallArgs` for their positional arguments. [#4768](https://github.com/PyO3/pyo3/pull/4768) +- Expose FFI definitions for `PyObject_Vectorcall(Method)` on the stable abi on 3.12+. [#4853](https://github.com/PyO3/pyo3/pull/4853) +- `#[pyo3(from_py_with = ...)]` now take a path rather than a string literal [#4860](https://github.com/PyO3/pyo3/pull/4860) +- Format Python traceback in impl Debug for PyErr. [#4900](https://github.com/PyO3/pyo3/pull/4900) +- Convert `PathBuf` & `Path` into Python `pathlib.Path` instead of `PyString`. [#4925](https://github.com/PyO3/pyo3/pull/4925) +- Relax parsing of exotic Python versions. [#4949](https://github.com/PyO3/pyo3/pull/4949) +- PyO3 threads now hang instead of `pthread_exit` trying to acquire the GIL when the interpreter is shutting down. This mimics the [Python 3.14](https://github.com/python/cpython/issues/87135) behavior and avoids undefined behavior and crashes. [#4874](https://github.com/PyO3/pyo3/pull/4874) + +### Removed + +- Remove implementations of `Deref` for `PyAny` and other "native" types. [#4593](https://github.com/PyO3/pyo3/pull/4593) +- Remove implicit default of trailing optional arguments (see #2935) [#4729](https://github.com/PyO3/pyo3/pull/4729) +- Remove the deprecated implicit eq fallback for simple enums. [#4730](https://github.com/PyO3/pyo3/pull/4730) + +### Fixed + +- Correct FFI definition of `PyIter_Send` to return a `PySendResult`. [#4746](https://github.com/PyO3/pyo3/pull/4746) +- Fix a thread safety issue in the runtime borrow checker used by mutable pyclass instances on the free-threaded build. [#4948](https://github.com/PyO3/pyo3/pull/4948) + + ## [0.23.5] - 2025-02-22 ### Packaging @@ -2064,7 +2113,8 @@ Yanked - Initial release -[Unreleased]: https://github.com/pyo3/pyo3/compare/v0.23.5...HEAD +[Unreleased]: https://github.com/pyo3/pyo3/compare/v0.24.0...HEAD +[0.24.0]: https://github.com/pyo3/pyo3/compare/v0.23.5...v0.24.0 [0.23.5]: https://github.com/pyo3/pyo3/compare/v0.23.4...v0.23.5 [0.23.4]: https://github.com/pyo3/pyo3/compare/v0.23.3...v0.23.4 [0.23.3]: https://github.com/pyo3/pyo3/compare/v0.23.2...v0.23.3 diff --git a/Cargo.toml b/Cargo.toml index 66683ff74b9..315c1f622ea 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3" -version = "0.23.5" +version = "0.24.0" description = "Bindings to Python interpreter" authors = ["PyO3 Project and Contributors "] readme = "README.md" @@ -21,10 +21,10 @@ memoffset = "0.9" once_cell = "1.13" # ffi bindings to the python interpreter, split into a separate crate so they can be used independently -pyo3-ffi = { path = "pyo3-ffi", version = "=0.23.5" } +pyo3-ffi = { path = "pyo3-ffi", version = "=0.24.0" } # support crates for macros feature -pyo3-macros = { path = "pyo3-macros", version = "=0.23.5", optional = true } +pyo3-macros = { path = "pyo3-macros", version = "=0.24.0", optional = true } indoc = { version = "2.0.1", optional = true } unindent = { version = "0.2.1", optional = true } @@ -68,7 +68,7 @@ static_assertions = "1.1.0" uuid = { version = "1.10.0", features = ["v4"] } [build-dependencies] -pyo3-build-config = { path = "pyo3-build-config", version = "=0.23.5", features = ["resolve-config"] } +pyo3-build-config = { path = "pyo3-build-config", version = "=0.24.0", features = ["resolve-config"] } [features] default = ["macros"] diff --git a/README.md b/README.md index 5e7a14d8297..18d6389ff1b 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,7 @@ name = "string_sum" crate-type = ["cdylib"] [dependencies] -pyo3 = { version = "0.23.5", features = ["extension-module"] } +pyo3 = { version = "0.24.0", features = ["extension-module"] } ``` **`src/lib.rs`** @@ -140,7 +140,7 @@ Start a new project with `cargo new` and add `pyo3` to the `Cargo.toml` like th ```toml [dependencies.pyo3] -version = "0.23.5" +version = "0.24.0" features = ["auto-initialize"] ``` diff --git a/examples/decorator/.template/pre-script.rhai b/examples/decorator/.template/pre-script.rhai index c403a167400..1ad80e9afbe 100644 --- a/examples/decorator/.template/pre-script.rhai +++ b/examples/decorator/.template/pre-script.rhai @@ -1,4 +1,4 @@ -variable::set("PYO3_VERSION", "0.23.5"); +variable::set("PYO3_VERSION", "0.24.0"); 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 c403a167400..1ad80e9afbe 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.23.5"); +variable::set("PYO3_VERSION", "0.24.0"); 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 f958e1da13e..ffd73d3a0fa 100644 --- a/examples/plugin/.template/pre-script.rhai +++ b/examples/plugin/.template/pre-script.rhai @@ -1,4 +1,4 @@ -variable::set("PYO3_VERSION", "0.23.5"); +variable::set("PYO3_VERSION", "0.24.0"); 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 3e9f2a4a04d..fd6e6775627 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.23.5"); +variable::set("PYO3_VERSION", "0.24.0"); 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 c403a167400..1ad80e9afbe 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.23.5"); +variable::set("PYO3_VERSION", "0.24.0"); file::rename(".template/Cargo.toml", "Cargo.toml"); file::rename(".template/pyproject.toml", "pyproject.toml"); file::delete(".template"); diff --git a/newsfragments/4593.changed.md b/newsfragments/4593.changed.md deleted file mode 100644 index e384854e918..00000000000 --- a/newsfragments/4593.changed.md +++ /dev/null @@ -1 +0,0 @@ -Use `DerefToPyAny` in blanket implementations of `From>` and `From>` for `PyObject`. diff --git a/newsfragments/4593.removed.md b/newsfragments/4593.removed.md deleted file mode 100644 index de1c0fb45cb..00000000000 --- a/newsfragments/4593.removed.md +++ /dev/null @@ -1 +0,0 @@ -Remove implementations of `Deref` for `PyAny` and other "native" types. diff --git a/newsfragments/4729.removed.md b/newsfragments/4729.removed.md deleted file mode 100644 index da1498ee69f..00000000000 --- a/newsfragments/4729.removed.md +++ /dev/null @@ -1 +0,0 @@ -removes implicit default of trailing optional arguments (see #2935) \ No newline at end of file diff --git a/newsfragments/4730.removed.md b/newsfragments/4730.removed.md deleted file mode 100644 index de8b64f9ba6..00000000000 --- a/newsfragments/4730.removed.md +++ /dev/null @@ -1 +0,0 @@ -Removed the deprecated implicit eq fallback for simple enums. \ No newline at end of file diff --git a/newsfragments/4746.added.md b/newsfragments/4746.added.md deleted file mode 100644 index 43fbab18f2c..00000000000 --- a/newsfragments/4746.added.md +++ /dev/null @@ -1 +0,0 @@ -Added `PyIterator::send` method to allow sending values into a python generator. diff --git a/newsfragments/4746.fixed.md b/newsfragments/4746.fixed.md deleted file mode 100644 index 51611432e30..00000000000 --- a/newsfragments/4746.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Fixed the return value of pyo3-ffi's PyIter_Send() function to return PySendResult. \ No newline at end of file diff --git a/newsfragments/4747.changed.md b/newsfragments/4747.changed.md deleted file mode 100644 index ca04831d064..00000000000 --- a/newsfragments/4747.changed.md +++ /dev/null @@ -1 +0,0 @@ -Map `io::ErrorKind::IsADirectory`/`NotADirectory` to the corresponding Python exception on Rust 1.83+ \ No newline at end of file diff --git a/newsfragments/4756.packaging.md b/newsfragments/4756.packaging.md deleted file mode 100644 index a3259c1fc42..00000000000 --- a/newsfragments/4756.packaging.md +++ /dev/null @@ -1 +0,0 @@ -Add supported CPython/PyPy versions to cargo package metadata. diff --git a/newsfragments/4768.added.md b/newsfragments/4768.added.md deleted file mode 100644 index 1ce9c6f5b92..00000000000 --- a/newsfragments/4768.added.md +++ /dev/null @@ -1 +0,0 @@ -Added `PyCallArgs` trait for arguments into the Python calling protocol. This enabled using a faster calling convention for certain types, improving performance. \ No newline at end of file diff --git a/newsfragments/4768.changed.md b/newsfragments/4768.changed.md deleted file mode 100644 index 6b09fd0e093..00000000000 --- a/newsfragments/4768.changed.md +++ /dev/null @@ -1 +0,0 @@ -`PyAnyMethods::call` an friends now require `PyCallArgs` for their positional arguments. \ No newline at end of file diff --git a/newsfragments/4810.added.md b/newsfragments/4810.added.md deleted file mode 100644 index 00c7c9e1127..00000000000 --- a/newsfragments/4810.added.md +++ /dev/null @@ -1 +0,0 @@ -Optimizes `nth`, `nth_back`, `advance_by` and `advance_back_by` for `BoundListIterator` \ No newline at end of file diff --git a/newsfragments/4822.changed.md b/newsfragments/4822.changed.md deleted file mode 100644 index a06613292c4..00000000000 --- a/newsfragments/4822.changed.md +++ /dev/null @@ -1 +0,0 @@ -Bumped `target-lexicon` dependency to 0.13 diff --git a/newsfragments/4823.added.md b/newsfragments/4823.added.md deleted file mode 100644 index f51227a20b2..00000000000 --- a/newsfragments/4823.added.md +++ /dev/null @@ -1 +0,0 @@ -Add jiff to/from python conversions. diff --git a/newsfragments/4829.added.md b/newsfragments/4829.added.md deleted file mode 100644 index 9400501a799..00000000000 --- a/newsfragments/4829.added.md +++ /dev/null @@ -1 +0,0 @@ -`derive(FromPyObject)` allow a `default` attribute to set a default value for extracted fields of named structs. The default value is either provided explicitly or fetched via `Default::default()`. \ No newline at end of file diff --git a/newsfragments/4850.added.md b/newsfragments/4850.added.md deleted file mode 100644 index acdd7c2e48a..00000000000 --- a/newsfragments/4850.added.md +++ /dev/null @@ -1 +0,0 @@ -introduce `into_py_with`/`into_py_with_ref` for `#[derive(IntoPyObject, IntoPyObjectRef)]` \ No newline at end of file diff --git a/newsfragments/4853.added.md b/newsfragments/4853.added.md deleted file mode 100644 index d3df4d219cc..00000000000 --- a/newsfragments/4853.added.md +++ /dev/null @@ -1 +0,0 @@ -pyo3-ffi: expose `PyObject_Vectorcall(Method)` on the stable abi on 3.12+ \ No newline at end of file diff --git a/newsfragments/4860.changed.md b/newsfragments/4860.changed.md deleted file mode 100644 index 4f62e45a5c8..00000000000 --- a/newsfragments/4860.changed.md +++ /dev/null @@ -1 +0,0 @@ -`#[pyo3(from_py_with = ...)]` now take a path rather than a string literal \ No newline at end of file diff --git a/newsfragments/4864.added.md b/newsfragments/4864.added.md deleted file mode 100644 index 7b3e433b1fe..00000000000 --- a/newsfragments/4864.added.md +++ /dev/null @@ -1 +0,0 @@ -Add uuid to/from python conversions. \ No newline at end of file diff --git a/newsfragments/4866.added.md b/newsfragments/4866.added.md deleted file mode 100644 index 2d41123342b..00000000000 --- a/newsfragments/4866.added.md +++ /dev/null @@ -1 +0,0 @@ -Exposing PyThreadState_GetFrame and PyFrame_GetBack. diff --git a/newsfragments/4874.changed.md b/newsfragments/4874.changed.md deleted file mode 100644 index fd483f43de5..00000000000 --- a/newsfragments/4874.changed.md +++ /dev/null @@ -1 +0,0 @@ - * PyO3 threads now hang instead of `pthread_exit` trying to acquire the GIL when the interpreter is shutting down. This mimics the [Python 3.14](https://github.com/python/cpython/issues/87135) behavior and avoids undefined behavior and crashes. diff --git a/newsfragments/4878.added.md b/newsfragments/4878.added.md deleted file mode 100644 index 0130b2b805b..00000000000 --- a/newsfragments/4878.added.md +++ /dev/null @@ -1,2 +0,0 @@ -- Optimizes `last` for `BoundListIterator`, `BoundTupleIterator` and `BorrowedTupleIterator` -- Optimizes `Iterator::count()` for `PyDict`, `PyList`, `PyTuple` & `PySet` \ No newline at end of file diff --git a/newsfragments/4897.added.md b/newsfragments/4897.added.md deleted file mode 100644 index cfa23d37673..00000000000 --- a/newsfragments/4897.added.md +++ /dev/null @@ -1 +0,0 @@ -Optimizes `nth`, `nth_back`, `advance_by` and `advance_back_by` for `BoundTupleIterator` \ No newline at end of file diff --git a/newsfragments/4900.changed.md b/newsfragments/4900.changed.md deleted file mode 100644 index 89bab779af1..00000000000 --- a/newsfragments/4900.changed.md +++ /dev/null @@ -1 +0,0 @@ -Format python traceback in impl Debug for PyErr. diff --git a/newsfragments/4917.added.md b/newsfragments/4917.added.md deleted file mode 100644 index 4cc65a8f404..00000000000 --- a/newsfragments/4917.added.md +++ /dev/null @@ -1,2 +0,0 @@ -Added support for creating [types.GenericAlias](https://docs.python.org/3/library/types.html#types.GenericAlias) -objects in PyO3 with `pyo3::types::PyGenericAlias`. \ No newline at end of file diff --git a/newsfragments/4925.changed.md b/newsfragments/4925.changed.md deleted file mode 100644 index 1501e375c63..00000000000 --- a/newsfragments/4925.changed.md +++ /dev/null @@ -1 +0,0 @@ -Convert `PathBuf` & `Path` into python `pathlib.Path` instead of `PyString` \ No newline at end of file diff --git a/newsfragments/4934.added.md b/newsfragments/4934.added.md deleted file mode 100644 index 716e243097d..00000000000 --- a/newsfragments/4934.added.md +++ /dev/null @@ -1 +0,0 @@ -* Added `MutextExt`, an extension trait to avoid deadlocks with the GIL while locking a `std::sync::Mutex`. \ No newline at end of file diff --git a/newsfragments/4941.added.md b/newsfragments/4941.added.md deleted file mode 100644 index 7aca45df073..00000000000 --- a/newsfragments/4941.added.md +++ /dev/null @@ -1 +0,0 @@ -add `#[pyo3(rename_all = "...")]` for `#[derive(FromPyObject)]` \ No newline at end of file diff --git a/newsfragments/4948.fixed.md b/newsfragments/4948.fixed.md deleted file mode 100644 index eca5be21e3b..00000000000 --- a/newsfragments/4948.fixed.md +++ /dev/null @@ -1 +0,0 @@ -* Fixed a thread safety issue in the runtime borrow checker used by mutable pyclass instances on the free-threaded build. \ No newline at end of file diff --git a/newsfragments/4949.fixed.md b/newsfragments/4949.fixed.md deleted file mode 100644 index 4a24b1462ac..00000000000 --- a/newsfragments/4949.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Allow parsing exotic python version diff --git a/newsfragments/4954.packaging.md b/newsfragments/4954.packaging.md deleted file mode 100644 index bfaa4174ef8..00000000000 --- a/newsfragments/4954.packaging.md +++ /dev/null @@ -1 +0,0 @@ -bump minimum supported `inventory` version to 0.3.5 \ No newline at end of file diff --git a/pyo3-build-config/Cargo.toml b/pyo3-build-config/Cargo.toml index 2eb0750cc0d..66d09ed3315 100644 --- a/pyo3-build-config/Cargo.toml +++ b/pyo3-build-config/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3-build-config" -version = "0.23.5" +version = "0.24.0" 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 656146e12aa..f5ee9b5f98c 100644 --- a/pyo3-ffi/Cargo.toml +++ b/pyo3-ffi/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3-ffi" -version = "0.23.5" +version = "0.24.0" description = "Python-API bindings for the PyO3 ecosystem" authors = ["PyO3 Project and Contributors "] keywords = ["pyo3", "python", "cpython", "ffi"] @@ -42,7 +42,7 @@ generate-import-lib = ["pyo3-build-config/python3-dll-a"] paste = "1" [build-dependencies] -pyo3-build-config = { path = "../pyo3-build-config", version = "=0.23.5", features = ["resolve-config"] } +pyo3-build-config = { path = "../pyo3-build-config", version = "=0.24.0", features = ["resolve-config"] } [lints] workspace = true diff --git a/pyo3-ffi/README.md b/pyo3-ffi/README.md index 8224217c4e7..3fada2ffab6 100644 --- a/pyo3-ffi/README.md +++ b/pyo3-ffi/README.md @@ -41,13 +41,13 @@ name = "string_sum" crate-type = ["cdylib"] [dependencies.pyo3-ffi] -version = "0.23.5" +version = "0.24.0" features = ["extension-module"] [build-dependencies] # This is only necessary if you need to configure your build based on # the Python version or the compile-time configuration for the interpreter. -pyo3_build_config = "0.23.5" +pyo3_build_config = "0.24.0" ``` If you need to use conditional compilation based on Python version or how diff --git a/pyo3-macros-backend/Cargo.toml b/pyo3-macros-backend/Cargo.toml index fced6a5d287..91e0009f9f6 100644 --- a/pyo3-macros-backend/Cargo.toml +++ b/pyo3-macros-backend/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3-macros-backend" -version = "0.23.5" +version = "0.24.0" description = "Code generation for PyO3 package" authors = ["PyO3 Project and Contributors "] keywords = ["pyo3", "python", "cpython", "ffi"] @@ -17,7 +17,7 @@ rust-version = "1.63" [dependencies] heck = "0.5" proc-macro2 = { version = "1.0.60", default-features = false } -pyo3-build-config = { path = "../pyo3-build-config", version = "=0.23.5", features = ["resolve-config"] } +pyo3-build-config = { path = "../pyo3-build-config", version = "=0.24.0", features = ["resolve-config"] } quote = { version = "1", default-features = false } [dependencies.syn] @@ -26,7 +26,7 @@ default-features = false features = ["derive", "parsing", "printing", "clone-impls", "full", "extra-traits"] [build-dependencies] -pyo3-build-config = { path = "../pyo3-build-config", version = "=0.23.5" } +pyo3-build-config = { path = "../pyo3-build-config", version = "=0.24.0" } [lints] workspace = true diff --git a/pyo3-macros/Cargo.toml b/pyo3-macros/Cargo.toml index 3de7a556b0b..25821b84e81 100644 --- a/pyo3-macros/Cargo.toml +++ b/pyo3-macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3-macros" -version = "0.23.5" +version = "0.24.0" description = "Proc macros for PyO3 package" authors = ["PyO3 Project and Contributors "] keywords = ["pyo3", "python", "cpython", "ffi"] @@ -22,7 +22,7 @@ experimental-async = ["pyo3-macros-backend/experimental-async"] proc-macro2 = { version = "1.0.60", default-features = false } quote = "1" syn = { version = "2", features = ["full", "extra-traits"] } -pyo3-macros-backend = { path = "../pyo3-macros-backend", version = "=0.23.5" } +pyo3-macros-backend = { path = "../pyo3-macros-backend", version = "=0.24.0" } [lints] workspace = true diff --git a/pyproject.toml b/pyproject.toml index 48a5e8a9747..d757c927f4f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ [tool.towncrier] filename = "CHANGELOG.md" -version = "0.23.5" +version = "0.24.0" start_string = "\n" template = ".towncrier.template.md" title_format = "## [{version}] - {project_date}" diff --git a/tests/ui/reject_generics.stderr b/tests/ui/reject_generics.stderr index 850387aadd4..9cb7e6e2068 100644 --- a/tests/ui/reject_generics.stderr +++ b/tests/ui/reject_generics.stderr @@ -1,10 +1,10 @@ -error: #[pyclass] cannot have generic parameters. For an explanation, see https://pyo3.rs/v0.23.5/class.html#no-generic-parameters +error: #[pyclass] cannot have generic parameters. For an explanation, see https://pyo3.rs/v0.24.0/class.html#no-generic-parameters --> tests/ui/reject_generics.rs:4:25 | 4 | struct ClassWithGenerics { | ^ -error: #[pyclass] cannot have lifetime parameters. For an explanation, see https://pyo3.rs/v0.23.5/class.html#no-lifetime-parameters +error: #[pyclass] cannot have lifetime parameters. For an explanation, see https://pyo3.rs/v0.24.0/class.html#no-lifetime-parameters --> tests/ui/reject_generics.rs:9:27 | 9 | struct ClassWithLifetimes<'a> { From 980268d3a7cfa37f304d0de258aaa89e3f0f709c Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers <7943856+bschoenmaeckers@users.noreply.github.com> Date: Thu, 13 Mar 2025 21:23:44 +0100 Subject: [PATCH 474/495] fix `check-msrv` ci job by resolving to latest compatible dependencies (#4972) * fix `check-msrv` ci job by resolving to latest compatible version using `resolver.incompatible-rust-versions = "fallback"` * Do the same for examples * Set `rust-version` in example projects * set msrv in examples, simplify noxfile * don't need toml * repeat for `pyo3-ffi` examples --------- Co-authored-by: David Hewitt --- examples/decorator/Cargo.toml | 1 + examples/getitem/Cargo.toml | 1 + examples/maturin-starter/Cargo.toml | 1 + examples/plugin/Cargo.toml | 2 +- examples/setuptools-rust-starter/Cargo.toml | 1 + examples/word-count/Cargo.toml | 1 + noxfile.py | 50 +++++++++------------ pyo3-ffi/examples/sequential/Cargo.toml | 1 + pyo3-ffi/examples/string-sum/Cargo.toml | 1 + 9 files changed, 28 insertions(+), 31 deletions(-) diff --git a/examples/decorator/Cargo.toml b/examples/decorator/Cargo.toml index 3456302a9fd..785895121a3 100644 --- a/examples/decorator/Cargo.toml +++ b/examples/decorator/Cargo.toml @@ -2,6 +2,7 @@ name = "decorator" version = "0.1.0" edition = "2021" +rust-version = "1.63" [lib] name = "decorator" diff --git a/examples/getitem/Cargo.toml b/examples/getitem/Cargo.toml index 17020b9bd05..99430483171 100644 --- a/examples/getitem/Cargo.toml +++ b/examples/getitem/Cargo.toml @@ -2,6 +2,7 @@ name = "getitem" version = "0.1.0" edition = "2021" +rust-version = "1.63" [lib] name = "getitem" diff --git a/examples/maturin-starter/Cargo.toml b/examples/maturin-starter/Cargo.toml index 257908a4bb0..ee1ab9aff06 100644 --- a/examples/maturin-starter/Cargo.toml +++ b/examples/maturin-starter/Cargo.toml @@ -2,6 +2,7 @@ name = "maturin-starter" version = "0.1.0" edition = "2021" +rust-version = "1.63" [lib] name = "maturin_starter" diff --git a/examples/plugin/Cargo.toml b/examples/plugin/Cargo.toml index 08127b5003f..062dab1ff21 100644 --- a/examples/plugin/Cargo.toml +++ b/examples/plugin/Cargo.toml @@ -2,7 +2,7 @@ name = "plugin_example" version = "0.1.0" edition = "2021" - +rust-version = "1.63" [dependencies] pyo3={path="../../", features=["macros"]} diff --git a/examples/setuptools-rust-starter/Cargo.toml b/examples/setuptools-rust-starter/Cargo.toml index 5777cbbcd78..ffabd8df849 100644 --- a/examples/setuptools-rust-starter/Cargo.toml +++ b/examples/setuptools-rust-starter/Cargo.toml @@ -2,6 +2,7 @@ name = "setuptools-rust-starter" version = "0.1.0" edition = "2021" +rust-version = "1.63" [lib] name = "setuptools_rust_starter" diff --git a/examples/word-count/Cargo.toml b/examples/word-count/Cargo.toml index cfb3444d5fe..8d79c8a4ff9 100644 --- a/examples/word-count/Cargo.toml +++ b/examples/word-count/Cargo.toml @@ -2,6 +2,7 @@ name = "word-count" version = "0.1.0" edition = "2021" +rust-version = "1.63" [lib] name = "word_count" diff --git a/noxfile.py b/noxfile.py index 418f9194ddd..14f2cffa0be 100644 --- a/noxfile.py +++ b/noxfile.py @@ -624,21 +624,13 @@ def check_changelog(session: nox.Session): def set_msrv_package_versions(session: nox.Session): from collections import defaultdict - if toml is None: - session.error("requires Python 3.11 or `toml` to be installed") - projects = ( - None, - "examples/decorator", - "examples/maturin-starter", - "examples/setuptools-rust-starter", - "examples/word-count", + PYO3_DIR, + *(Path(p).parent for p in glob("examples/*/Cargo.toml")), + *(Path(p).parent for p in glob("pyo3-ffi/examples/*/Cargo.toml")), ) min_pkg_versions = { - "regex": "1.9.6", - "proptest": "1.2.0", "trybuild": "1.0.89", - "eyre": "0.6.8", "allocator-api2": "0.2.10", "indexmap": "2.5.0", # to be compatible with hashbrown 0.14 "hashbrown": "0.14.5", # https://github.com/rust-lang/hashbrown/issues/574 @@ -647,13 +639,15 @@ def set_msrv_package_versions(session: nox.Session): # run cargo update first to ensure that everything is at highest # possible version, so that this matches what CI will resolve to. for project in projects: - if project is None: - _run_cargo(session, "update") - else: - _run_cargo(session, "update", f"--manifest-path={project}/Cargo.toml") + _run_cargo( + session, + "+stable", + "update", + f"--manifest-path={project}/Cargo.toml", + env=os.environ | {"CARGO_RESOLVER_INCOMPATIBLE_RUST_VERSIONS": "fallback"}, + ) - for project in projects: - lock_file = Path(project or "") / "Cargo.lock" + lock_file = project / "Cargo.lock" def load_pkg_versions(): cargo_lock = toml.loads(lock_file.read_text()) @@ -679,19 +673,15 @@ def load_pkg_versions(): # and re-read `Cargo.lock` pkg_versions = load_pkg_versions() - # As a smoke test, cargo metadata solves all dependencies, so - # will break if any crates rely on cargo features not - # supported on MSRV - for project in projects: - if project is None: - _run_cargo(session, "metadata", silent=True) - else: - _run_cargo( - session, - "metadata", - f"--manifest-path={project}/Cargo.toml", - silent=True, - ) + # As a smoke test, cargo metadata solves all dependencies, so + # will break if any crates rely on cargo features not + # supported on MSRV + _run_cargo( + session, + "metadata", + f"--manifest-path={project}/Cargo.toml", + silent=True, + ) @nox.session(name="ffi-check") diff --git a/pyo3-ffi/examples/sequential/Cargo.toml b/pyo3-ffi/examples/sequential/Cargo.toml index 288eb1ba326..e62693303d9 100644 --- a/pyo3-ffi/examples/sequential/Cargo.toml +++ b/pyo3-ffi/examples/sequential/Cargo.toml @@ -2,6 +2,7 @@ name = "sequential" version = "0.1.0" edition = "2021" +rust-version = "1.63" [lib] name = "sequential" diff --git a/pyo3-ffi/examples/string-sum/Cargo.toml b/pyo3-ffi/examples/string-sum/Cargo.toml index 3c9893b3e8a..b0f784d26f2 100644 --- a/pyo3-ffi/examples/string-sum/Cargo.toml +++ b/pyo3-ffi/examples/string-sum/Cargo.toml @@ -2,6 +2,7 @@ name = "string_sum" version = "0.1.0" edition = "2021" +rust-version = "1.63" [lib] name = "string_sum" From fdf62e0e20081a1d5be14b77fd8ac26aa1e8d9dc Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Thu, 13 Mar 2025 15:50:46 -0600 Subject: [PATCH 475/495] docs: ignore 404 errors from https://pyo3.rs (#4967) * docs: ignore 404 errors from https://pyo3.rs * Update .github/workflows/gh-pages.yml Co-authored-by: David Hewitt --------- Co-authored-by: David Hewitt --- .github/workflows/gh-pages.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/gh-pages.yml b/.github/workflows/gh-pages.yml index 21b33c93796..1dc5927b49e 100644 --- a/.github/workflows/gh-pages.yml +++ b/.github/workflows/gh-pages.yml @@ -42,7 +42,7 @@ jobs: - name: Build the guide run: | python -m pip install --upgrade pip && pip install nox - nox -s check-guide + nox -s ${{ github.event_name == 'release' && 'build-guide' || 'check-guide' }} env: PYO3_VERSION_TAG: ${{ steps.prepare_tag.outputs.tag_name }} From 8e7ac5d4476c2b6eb2c15b291a156cd37d1e9a24 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Mon, 17 Mar 2025 18:30:56 +0000 Subject: [PATCH 476/495] fix native types not implementing `is_type_of` correctly (#4981) --- newsfragments/4981.fixed.md | 1 + src/types/mod.rs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 newsfragments/4981.fixed.md diff --git a/newsfragments/4981.fixed.md b/newsfragments/4981.fixed.md new file mode 100644 index 00000000000..0ac31b19a11 --- /dev/null +++ b/newsfragments/4981.fixed.md @@ -0,0 +1 @@ +Fix `is_type_of` for native types not using same specialized check as `is_type_of_bound`. diff --git a/src/types/mod.rs b/src/types/mod.rs index e3a317ea33a..637d07d72b6 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -152,7 +152,7 @@ macro_rules! pyobject_native_type_info( $( #[inline] - fn is_type_of_bound(obj: &$crate::Bound<'_, $crate::PyAny>) -> bool { + fn is_type_of(obj: &$crate::Bound<'_, $crate::PyAny>) -> bool { #[allow(unused_unsafe)] unsafe { $checkfunction(obj.as_ptr()) > 0 } } From f08cc603d2cce20e9fa1bdf76bd5cd93f2f20bec Mon Sep 17 00:00:00 2001 From: Pontus Andersson Date: Mon, 17 Mar 2025 22:11:57 +0100 Subject: [PATCH 477/495] Fix `Probe` class naming issue with `#[pymethods]` (#4988) Commit 603a55f204e3 ("fix `#[pyclass]` could not be named `Probe` (#4794)") fixed the issue where it was not possible to define a class named `Probe`. However, similar issue exists when trying to add `#[pymethods]` implementations to the `Probe` class. It generates similar confusing errors as the original issue (#4792), due to the internal `Probe` trait being in scope at the user code: error[E0782]: expected a type, found a trait help: you can add the `dyn` keyword if you want a trait object # | impl dyn Probe { Fix the issue by avoiding pollution of the imports from the macro expansion and instead use qualified path to the internal types. --- newsfragments/4988.fixed.md | 1 + pyo3-macros-backend/src/pymethod.rs | 3 +-- tests/ui/pyclass_probe.rs | 14 ++++++++++++++ 3 files changed, 16 insertions(+), 2 deletions(-) create mode 100644 newsfragments/4988.fixed.md diff --git a/newsfragments/4988.fixed.md b/newsfragments/4988.fixed.md new file mode 100644 index 00000000000..1a050b905e3 --- /dev/null +++ b/newsfragments/4988.fixed.md @@ -0,0 +1 @@ +Fix `Probe` class naming issue with `#[pymethods]` diff --git a/pyo3-macros-backend/src/pymethod.rs b/pyo3-macros-backend/src/pymethod.rs index a94a6ad67ab..a116acf69a3 100644 --- a/pyo3-macros-backend/src/pymethod.rs +++ b/pyo3-macros-backend/src/pymethod.rs @@ -367,9 +367,8 @@ pub fn impl_py_method_def_new( args: *mut #pyo3_path::ffi::PyObject, kwargs: *mut #pyo3_path::ffi::PyObject, ) -> *mut #pyo3_path::ffi::PyObject { - use #pyo3_path::impl_::pyclass::*; #[allow(unknown_lints, non_local_definitions)] - impl PyClassNewTextSignature<#cls> for PyClassImplCollector<#cls> { + impl #pyo3_path::impl_::pyclass::PyClassNewTextSignature<#cls> for #pyo3_path::impl_::pyclass::PyClassImplCollector<#cls> { #[inline] fn new_text_signature(self) -> ::std::option::Option<&'static str> { #text_signature_body diff --git a/tests/ui/pyclass_probe.rs b/tests/ui/pyclass_probe.rs index 590af194f6f..6029ceca1f7 100644 --- a/tests/ui/pyclass_probe.rs +++ b/tests/ui/pyclass_probe.rs @@ -3,4 +3,18 @@ use pyo3::prelude::*; #[pyclass] pub struct Probe {} +#[pymethods] +impl Probe { + #[new] + fn new() -> Self { + Self {} + } +} + +#[pymodule] +fn probe(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + m.add_class::()?; + Ok(()) +} + fn main() {} From 478d08e465ee3e1d5bf359bcf3428fcb0e139b01 Mon Sep 17 00:00:00 2001 From: Emma Gordon Date: Tue, 18 Mar 2025 21:09:45 +0000 Subject: [PATCH 478/495] added abi3-py313 feature (#4969) * add abi3-py313 feature * add check for missing abi3 features --------- Co-authored-by: David Hewitt --- Cargo.toml | 3 ++- newsfragments/4969.added.md | 1 + noxfile.py | 12 ++++++++++++ pyo3-build-config/Cargo.toml | 3 ++- pyo3-build-config/src/impl_.rs | 2 +- pyo3-ffi/Cargo.toml | 3 ++- 6 files changed, 20 insertions(+), 4 deletions(-) create mode 100644 newsfragments/4969.added.md diff --git a/Cargo.toml b/Cargo.toml index 315c1f622ea..3456353b532 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -100,7 +100,8 @@ 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-py312", "pyo3-build-config/abi3-py311", "pyo3-ffi/abi3-py311"] -abi3-py312 = ["abi3", "pyo3-build-config/abi3-py312", "pyo3-ffi/abi3-py312"] +abi3-py312 = ["abi3-py313", "pyo3-build-config/abi3-py312", "pyo3-ffi/abi3-py312"] +abi3-py313 = ["abi3", "pyo3-build-config/abi3-py313", "pyo3-ffi/abi3-py313"] # Automatically generates `python3.dll` import libraries for Windows targets. generate-import-lib = ["pyo3-ffi/generate-import-lib"] diff --git a/newsfragments/4969.added.md b/newsfragments/4969.added.md new file mode 100644 index 00000000000..7a59e9aef6f --- /dev/null +++ b/newsfragments/4969.added.md @@ -0,0 +1 @@ +Added `abi3-py313` feature \ No newline at end of file diff --git a/noxfile.py b/noxfile.py index 14f2cffa0be..61e8dee71fd 100644 --- a/noxfile.py +++ b/noxfile.py @@ -724,6 +724,8 @@ def check_feature_powerset(session: nox.Session): cargo_toml = toml.loads((PYO3_DIR / "Cargo.toml").read_text()) + EXPECTED_ABI3_FEATURES = {f"abi3-py3{ver.split('.')[1]}" for ver in PY_VERSIONS} + EXCLUDED_FROM_FULL = { "nightly", "extension-module", @@ -740,6 +742,16 @@ def check_feature_powerset(session: nox.Session): abi3_features = {feature for feature in features if feature.startswith("abi3")} abi3_version_features = abi3_features - {"abi3"} + unexpected_abi3_features = abi3_version_features - EXPECTED_ABI3_FEATURES + if unexpected_abi3_features: + session.error( + f"unexpected `abi3` features found in Cargo.toml: {unexpected_abi3_features}" + ) + + missing_abi3_features = EXPECTED_ABI3_FEATURES - abi3_version_features + if missing_abi3_features: + session.error(f"missing `abi3` features in Cargo.toml: {missing_abi3_features}") + expected_full_feature = features.keys() - EXCLUDED_FROM_FULL - abi3_features uncovered_features = expected_full_feature - full_feature diff --git a/pyo3-build-config/Cargo.toml b/pyo3-build-config/Cargo.toml index 66d09ed3315..b8030cc4304 100644 --- a/pyo3-build-config/Cargo.toml +++ b/pyo3-build-config/Cargo.toml @@ -37,7 +37,8 @@ abi3-py38 = ["abi3-py39"] abi3-py39 = ["abi3-py310"] abi3-py310 = ["abi3-py311"] abi3-py311 = ["abi3-py312"] -abi3-py312 = ["abi3"] +abi3-py312 = ["abi3-py313"] +abi3-py313 = ["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 f130c2d6557..2c4955dcc6f 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -40,7 +40,7 @@ const MINIMUM_SUPPORTED_VERSION_GRAALPY: PythonVersion = PythonVersion { }; /// Maximum Python version that can be used as minimum required Python version with abi3. -pub(crate) const ABI3_MAX_MINOR: u8 = 12; +pub(crate) const ABI3_MAX_MINOR: u8 = 13; #[cfg(test)] thread_local! { diff --git a/pyo3-ffi/Cargo.toml b/pyo3-ffi/Cargo.toml index f5ee9b5f98c..16c8a3374cd 100644 --- a/pyo3-ffi/Cargo.toml +++ b/pyo3-ffi/Cargo.toml @@ -33,7 +33,8 @@ 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-py312", "pyo3-build-config/abi3-py311"] -abi3-py312 = ["abi3", "pyo3-build-config/abi3-py312"] +abi3-py312 = ["abi3-py313", "pyo3-build-config/abi3-py312"] +abi3-py313 = ["abi3", "pyo3-build-config/abi3-py313"] # Automatically generates `python3.dll` import libraries for Windows targets. generate-import-lib = ["pyo3-build-config/python3-dll-a"] From 3454ab6cd7f7b186a0849a91cafe505b882d9bec Mon Sep 17 00:00:00 2001 From: Benedikt Radtke Date: Wed, 19 Mar 2025 10:15:32 +0100 Subject: [PATCH 479/495] add PyInt constructor for all supported number types (i32, u32, i64, u64, isize, usize) (#4984) * add PyInt constructor for all supported number types (i32, u32, i64, u64, isize, usize, f64) Solves a part of https://github.com/PyO3/pyo3/issues/2221 * added news fragment * respect different integer widths * upcast 4byte bytes to 8 byte types on linux * only define int_from_upcasting where it is required * avoid clippy::approx_constant * IntoPyObject is already implemented for PyInt * Update newsfragment * properly cargo fmt immports * Remove ununsed pyint impl block * apply style suggestions --- newsfragments/4984.added.md | 1 + src/types/num.rs | 34 ++++++++++++++++++++++++++++++++-- 2 files changed, 33 insertions(+), 2 deletions(-) create mode 100644 newsfragments/4984.added.md diff --git a/newsfragments/4984.added.md b/newsfragments/4984.added.md new file mode 100644 index 00000000000..63559617812 --- /dev/null +++ b/newsfragments/4984.added.md @@ -0,0 +1 @@ +Added PyInt constructor for all supported number types (i32, u32, i64, u64, isize, usize) \ No newline at end of file diff --git a/src/types/num.rs b/src/types/num.rs index 0e377f66d48..8de116a470f 100644 --- a/src/types/num.rs +++ b/src/types/num.rs @@ -1,6 +1,6 @@ use super::any::PyAnyMethods; - -use crate::{ffi, instance::Bound, PyAny}; +use crate::{ffi, instance::Bound, IntoPyObject, PyAny, Python}; +use std::convert::Infallible; /// Represents a Python `int` object. /// @@ -20,6 +20,21 @@ pyobject_native_type_core!(PyInt, pyobject_native_static_type_object!(ffi::PyLon #[deprecated(since = "0.23.0", note = "use `PyInt` instead")] pub type PyLong = PyInt; +impl PyInt { + /// Creates a new Python int object. + /// + /// Panics if out of memory. + pub fn new<'a, T>(py: Python<'a>, i: T) -> Bound<'a, PyInt> + where + T: IntoPyObject<'a, Target = PyInt, Output = Bound<'a, PyInt>, Error = Infallible>, + { + match T::into_pyobject(i, py) { + Ok(v) => v, + Err(never) => match never {}, + } + } +} + macro_rules! int_compare { ($rust_type: ty) => { impl PartialEq<$rust_type> for Bound<'_, PyInt> { @@ -60,6 +75,7 @@ int_compare!(usize); #[cfg(test)] mod tests { + use super::*; use crate::{IntoPyObject, Python}; #[test] @@ -123,4 +139,18 @@ mod tests { } }); } + + #[test] + fn test_display_int() { + Python::with_gil(|py| { + let s = PyInt::new(py, 42u8); + assert_eq!(format!("{}", s), "42"); + + let s = PyInt::new(py, 43i32); + assert_eq!(format!("{}", s), "43"); + + let s = PyInt::new(py, 44usize); + assert_eq!(format!("{}", s), "44"); + }) + } } From 9eee01f49d5db8235ab5293ffd36420e1f65b527 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Wed, 19 Mar 2025 21:41:00 +0000 Subject: [PATCH 480/495] ci: make emscripten job only save cache on main (#4993) --- .github/workflows/ci.yml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 607b8484c6c..37e38b8cc0a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -512,12 +512,12 @@ jobs: with: node-version: 18 - run: python -m pip install --upgrade pip && pip install nox - - uses: actions/cache@v4 + - uses: actions/cache/restore@v4 id: cache with: path: | .nox/emscripten - key: ${{ hashFiles('emscripten/*') }} - ${{ hashFiles('noxfile.py') }} - ${{ steps.setup-python.outputs.python-path }} + key: emscripten-${{ hashFiles('emscripten/*') }}-${{ hashFiles('noxfile.py') }}-${{ steps.setup-python.outputs.python-path }} - uses: Swatinem/rust-cache@v2 with: save-if: ${{ github.event_name != 'merge_group' }} @@ -526,6 +526,12 @@ jobs: run: nox -s build-emscripten - name: Test run: nox -s test-emscripten + - uses: actions/cache/save@v4 + if: ${{ github.event_name != 'merge_group' }} + with: + path: | + .nox/emscripten + key: emscripten-${{ hashFiles('emscripten/*') }}-${{ hashFiles('noxfile.py') }}-${{ steps.setup-python.outputs.python-path }} test-debug: if: ${{ contains(github.event.pull_request.labels.*.name, 'CI-build-full') || github.event_name != 'pull_request' }} From e487da2a2c453b727029496bea99449146318b60 Mon Sep 17 00:00:00 2001 From: Owen Leung Date: Fri, 21 Mar 2025 04:35:17 +0800 Subject: [PATCH 481/495] Implement getattr_opt on PyAnyMethods (#4978) * Implement getattr_opt on PyAnyMethods * Add missing newsfragments. Fix CI test * Rewrite unit test & doc. Delegate to getattr API for non py313 build --- newsfragments/4978.added.md | 1 + src/types/any.rs | 125 ++++++++++++++++++++++++++++++++++++ 2 files changed, 126 insertions(+) create mode 100644 newsfragments/4978.added.md diff --git a/newsfragments/4978.added.md b/newsfragments/4978.added.md new file mode 100644 index 00000000000..5518cee89d8 --- /dev/null +++ b/newsfragments/4978.added.md @@ -0,0 +1 @@ +Implement getattr_opt in `PyAnyMethods` \ No newline at end of file diff --git a/src/types/any.rs b/src/types/any.rs index 0725453e569..b6ee6fdd532 100644 --- a/src/types/any.rs +++ b/src/types/any.rs @@ -118,6 +118,38 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed { where N: IntoPyObject<'py, Target = PyString>; + /// Retrieves an attribute value optionally. + /// + /// This is equivalent to the Python expression `getattr(self, attr_name, None)`, which may + /// be more efficient in some cases by simply returning `None` if the attribute is not found + /// instead of raising `AttributeError`. + /// + /// To avoid repeated temporary allocations of Python strings, the [`intern!`] macro can be used + /// to intern `attr_name`. + /// + /// # Errors + /// Returns `Err` if an exception other than `AttributeError` is raised during attribute lookup, + /// such as a `ValueError` from a property or descriptor. + /// + /// # Example: Retrieving an optional attribute + /// ``` + /// # use pyo3::{prelude::*, intern}; + /// # + /// #[pyfunction] + /// fn get_version_if_exists<'py>(sys: &Bound<'py, PyModule>) -> PyResult>> { + /// sys.getattr_opt(intern!(sys.py(), "version")) + /// } + /// # + /// # Python::with_gil(|py| { + /// # let sys = py.import("sys").unwrap(); + /// # let version = get_version_if_exists(&sys).unwrap(); + /// # assert!(version.is_some()); + /// # }); + /// ``` + fn getattr_opt(&self, attr_name: N) -> PyResult>> + where + N: IntoPyObject<'py, Target = PyString>; + /// Sets an attribute value. /// /// This is equivalent to the Python expression `self.attr_name = value`. @@ -975,6 +1007,54 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { ) } + fn getattr_opt(&self, attr_name: N) -> PyResult>> + where + N: IntoPyObject<'py, Target = PyString>, + { + fn inner<'py>( + any: &Bound<'py, PyAny>, + attr_name: Borrowed<'_, 'py, PyString>, + ) -> PyResult>> { + #[cfg(Py_3_13)] + { + let mut resp_ptr: *mut ffi::PyObject = std::ptr::null_mut(); + match unsafe { + ffi::PyObject_GetOptionalAttr(any.as_ptr(), attr_name.as_ptr(), &mut resp_ptr) + } { + // Attribute found, result is a new strong reference + 1 => { + let bound = unsafe { Bound::from_owned_ptr(any.py(), resp_ptr) }; + Ok(Some(bound)) + } + // Attribute not found, result is NULL + 0 => Ok(None), + + // An error occurred (other than AttributeError) + _ => Err(PyErr::fetch(any.py())), + } + } + + #[cfg(not(Py_3_13))] + { + match any.getattr(attr_name) { + Ok(bound) => Ok(Some(bound)), + Err(err) => { + let err_type = err + .get_type(any.py()) + .is(&PyType::new::(any.py())); + match err_type { + true => Ok(None), + false => Err(err), + } + } + } + } + } + + let py = self.py(); + inner(self, attr_name.into_pyobject_or_pyerr(py)?.as_borrowed()) + } + fn setattr(&self, attr_name: N, value: V) -> PyResult<()> where N: IntoPyObject<'py, Target = PyString>, @@ -1656,6 +1736,51 @@ class NonHeapNonDescriptorInt: }) } + #[test] + fn test_getattr_opt() { + Python::with_gil(|py| { + let module = PyModule::from_code( + py, + c_str!( + r#" +class Test: + class_str_attribute = "class_string" + + @property + def error(self): + raise ValueError("This is an intentional error") + "# + ), + c_str!("test.py"), + &generate_unique_module_name("test"), + ) + .unwrap(); + + // Get the class Test + let class_test = module.getattr_opt("Test").unwrap().unwrap(); + + // Test attribute that exist + let cls_attr_str = class_test + .getattr_opt("class_str_attribute") + .unwrap() + .unwrap(); + assert_eq!(cls_attr_str.extract::().unwrap(), "class_string"); + + // Test non-existent attribute + let do_not_exist = class_test.getattr_opt("doNotExist").unwrap(); + assert!(do_not_exist.is_none()); + + // Test error attribute + let instance = class_test.call0().unwrap(); + let error = instance.getattr_opt("error"); + assert!(error.is_err()); + assert!(error + .unwrap_err() + .to_string() + .contains("This is an intentional error")); + }); + } + #[test] fn test_call_for_non_existing_method() { Python::with_gil(|py| { From d88eae217d05996052e86dfd364ae139dec36ccd Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Fri, 21 Mar 2025 09:42:57 +0000 Subject: [PATCH 482/495] use `clang` in `cross-compilation-windows` (#4997) --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 37e38b8cc0a..0bb2aacecad 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -748,11 +748,11 @@ jobs: pip install cargo-xwin # abi3 cargo build --manifest-path examples/maturin-starter/Cargo.toml --features abi3 --target x86_64-pc-windows-gnu - cargo xwin build --manifest-path examples/maturin-starter/Cargo.toml --features abi3 --target x86_64-pc-windows-msvc + cargo xwin build --cross-compiler clang --manifest-path examples/maturin-starter/Cargo.toml --features abi3 --target x86_64-pc-windows-msvc # non-abi3 export PYO3_CROSS_PYTHON_VERSION=3.12 cargo build --manifest-path examples/maturin-starter/Cargo.toml --features generate-import-lib --target x86_64-pc-windows-gnu - cargo xwin build --manifest-path examples/maturin-starter/Cargo.toml --features generate-import-lib --target x86_64-pc-windows-msvc + cargo xwin build --cross-compiler clang --manifest-path examples/maturin-starter/Cargo.toml --features generate-import-lib --target x86_64-pc-windows-msvc - if: ${{ github.ref == 'refs/heads/main' }} uses: actions/cache/save@v4 with: From ed37c4028f72211b1d08f60f94e58b699489211e Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Fri, 21 Mar 2025 09:43:52 +0000 Subject: [PATCH 483/495] Migrate lints to use 2024 edition practice of `unsafe_op_in_unsafe_fn` (#4994) * mark unsafe function calls with keyword * mark unsafe function calls in tests as well * add workspace lints to enhance migration to 2024 edition * formatting * Apply suggestions from code review * add feature to control warnings on MSRV * fix configuration of workspace lints * another run of `cargo fix --lib -p pyo3-ffi` * fmt * even more fixes * fix unnecessary unsafe * allow `unsafe_op_in_unsafe_fn` in `pyo3-ffi` * fix tests on MSRV --------- Co-authored-by: Jonas Pleyer --- Cargo.toml | 1 + pyo3-build-config/src/lib.rs | 59 +++---- pyo3-ffi/src/lib.rs | 5 + src/err/err_state.rs | 8 +- src/ffi_ptr_ext.rs | 16 +- src/gil.rs | 32 ++-- src/impl_/extract_argument.rs | 31 ++-- src/impl_/pyclass.rs | 262 ++++++++++++++++-------------- src/impl_/pyclass_init.rs | 19 ++- src/impl_/pymethods.rs | 60 +++---- src/impl_/trampoline.rs | 30 ++-- src/instance.rs | 44 +++-- src/internal/get_slot.rs | 22 +-- src/lib.rs | 2 + src/marker.rs | 2 +- src/py_result_ext.rs | 2 +- src/pycell/impl_.rs | 76 +++++---- src/pyclass/create_type_object.rs | 106 ++++++------ src/pyclass_init.rs | 32 ++-- src/types/any.rs | 4 +- src/types/bytearray.rs | 8 +- src/types/bytes.rs | 10 +- src/types/capsule.rs | 20 ++- src/types/datetime.rs | 4 +- src/types/function.rs | 34 ++-- src/types/list.rs | 8 +- src/types/mappingproxy.rs | 2 +- src/types/string.rs | 66 ++++---- src/types/tuple.rs | 10 +- src/types/typeobject.rs | 8 +- src/types/weakref/anyref.rs | 2 +- tests/test_buffer.rs | 69 ++++---- tests/test_buffer_protocol.rs | 68 ++++---- tests/test_gc.rs | 3 +- tests/test_pyfunction.rs | 33 ++-- 35 files changed, 620 insertions(+), 538 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 3456353b532..84c6daba30e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -175,6 +175,7 @@ invalid_doc_attributes = "warn" rust_2018_idioms = { level = "warn", priority = -1 } rust_2021_prelude_collisions = "warn" unused_lifetimes = "warn" +unsafe_op_in_unsafe_fn = "warn" [workspace.lints.rustdoc] broken_intra_doc_links = "warn" diff --git a/pyo3-build-config/src/lib.rs b/pyo3-build-config/src/lib.rs index a908fe88068..68d597a8f2e 100644 --- a/pyo3-build-config/src/lib.rs +++ b/pyo3-build-config/src/lib.rs @@ -168,44 +168,36 @@ fn resolve_cross_compile_config_path() -> Option { }) } +/// Helper to print a feature cfg with a minimum rust version required. +fn print_feature_cfg(minor_version_required: u32, cfg: &str) { + let minor_version = rustc_minor_version().unwrap_or(0); + + if minor_version >= minor_version_required { + println!("cargo:rustc-cfg={}", cfg); + } + + // rustc 1.80.0 stabilized `rustc-check-cfg` feature, don't emit before + if minor_version >= 80 { + println!("cargo:rustc-check-cfg=cfg({})", cfg); + } +} + /// Use certain features if we detect the compiler being used supports them. /// /// Features may be removed or added as MSRV gets bumped or new features become available, /// so this function is unstable. #[doc(hidden)] pub fn print_feature_cfgs() { - let rustc_minor_version = rustc_minor_version().unwrap_or(0); - - if rustc_minor_version >= 70 { - println!("cargo:rustc-cfg=rustc_has_once_lock"); - } - - if rustc_minor_version >= 71 { - println!("cargo:rustc-cfg=rustc_has_extern_c_unwind"); - } - - // invalid_from_utf8 lint was added in Rust 1.74 - if rustc_minor_version >= 74 { - println!("cargo:rustc-cfg=invalid_from_utf8_lint"); - } - - if rustc_minor_version >= 79 { - println!("cargo:rustc-cfg=c_str_lit"); - } - + print_feature_cfg(70, "rustc_has_once_lock"); + print_feature_cfg(70, "cargo_toml_lints"); + print_feature_cfg(71, "rustc_has_extern_c_unwind"); + print_feature_cfg(74, "invalid_from_utf8_lint"); + print_feature_cfg(79, "c_str_lit"); // Actually this is available on 1.78, but we should avoid // https://github.com/rust-lang/rust/issues/124651 just in case - if rustc_minor_version >= 79 { - println!("cargo:rustc-cfg=diagnostic_namespace"); - } - - if rustc_minor_version >= 83 { - println!("cargo:rustc-cfg=io_error_more"); - } - - if rustc_minor_version >= 85 { - println!("cargo:rustc-cfg=fn_ptr_eq"); - } + print_feature_cfg(79, "diagnostic_namespace"); + print_feature_cfg(83, "io_error_more"); + print_feature_cfg(85, "fn_ptr_eq"); } /// Registers `pyo3`s config names as reachable cfg expressions @@ -224,15 +216,8 @@ pub fn print_expected_cfgs() { println!("cargo:rustc-check-cfg=cfg(PyPy)"); println!("cargo:rustc-check-cfg=cfg(GraalPy)"); println!("cargo:rustc-check-cfg=cfg(py_sys_config, values(\"Py_DEBUG\", \"Py_REF_DEBUG\", \"Py_TRACE_REFS\", \"COUNT_ALLOCS\"))"); - println!("cargo:rustc-check-cfg=cfg(invalid_from_utf8_lint)"); println!("cargo:rustc-check-cfg=cfg(pyo3_disable_reference_pool)"); println!("cargo:rustc-check-cfg=cfg(pyo3_leak_on_drop_without_reference_pool)"); - println!("cargo:rustc-check-cfg=cfg(diagnostic_namespace)"); - println!("cargo:rustc-check-cfg=cfg(c_str_lit)"); - println!("cargo:rustc-check-cfg=cfg(rustc_has_once_lock)"); - println!("cargo:rustc-check-cfg=cfg(rustc_has_extern_c_unwind)"); - println!("cargo:rustc-check-cfg=cfg(io_error_more)"); - println!("cargo:rustc-check-cfg=cfg(fn_ptr_eq)"); // allow `Py_3_*` cfgs from the minimum supported version up to the // maximum minor version (+1 for development for the next) diff --git a/pyo3-ffi/src/lib.rs b/pyo3-ffi/src/lib.rs index bb0d0ad040b..b14fe1e8611 100644 --- a/pyo3-ffi/src/lib.rs +++ b/pyo3-ffi/src/lib.rs @@ -330,6 +330,11 @@ clippy::missing_safety_doc )] #![warn(elided_lifetimes_in_paths, unused_lifetimes)] +// This crate is a hand-maintained translation of CPython's headers, so requiring "unsafe" +// blocks within those translations increases maintenance burden without providing any +// additional safety. The safety of the functions in this crate is determined by the +// original CPython headers +#![allow(unsafe_op_in_unsafe_fn)] // Until `extern type` is stabilized, use the recommended approach to // model opaque types: diff --git a/src/err/err_state.rs b/src/err/err_state.rs index 98be633e91c..9353f032e93 100644 --- a/src/err/err_state.rs +++ b/src/err/err_state.rs @@ -240,9 +240,11 @@ impl PyErrStateNormalized { ptraceback: *mut ffi::PyObject, ) -> Self { PyErrStateNormalized { - ptype: Py::from_owned_ptr_or_opt(py, ptype).expect("Exception type missing"), - pvalue: Py::from_owned_ptr_or_opt(py, pvalue).expect("Exception value missing"), - ptraceback: Py::from_owned_ptr_or_opt(py, ptraceback), + ptype: unsafe { Py::from_owned_ptr_or_opt(py, ptype).expect("Exception type missing") }, + pvalue: unsafe { + Py::from_owned_ptr_or_opt(py, pvalue).expect("Exception value missing") + }, + ptraceback: unsafe { Py::from_owned_ptr_or_opt(py, ptraceback) }, } } diff --git a/src/ffi_ptr_ext.rs b/src/ffi_ptr_ext.rs index 956b4e8406c..108f088ea18 100644 --- a/src/ffi_ptr_ext.rs +++ b/src/ffi_ptr_ext.rs @@ -40,23 +40,23 @@ pub(crate) trait FfiPtrExt: Sealed { impl FfiPtrExt for *mut ffi::PyObject { #[inline] unsafe fn assume_owned_or_err(self, py: Python<'_>) -> PyResult> { - Bound::from_owned_ptr_or_err(py, self) + unsafe { Bound::from_owned_ptr_or_err(py, self) } } #[inline] unsafe fn assume_owned_or_opt(self, py: Python<'_>) -> Option> { - Bound::from_owned_ptr_or_opt(py, self) + unsafe { Bound::from_owned_ptr_or_opt(py, self) } } #[inline] #[track_caller] unsafe fn assume_owned(self, py: Python<'_>) -> Bound<'_, PyAny> { - Bound::from_owned_ptr(py, self) + unsafe { Bound::from_owned_ptr(py, self) } } #[inline] unsafe fn assume_owned_unchecked(self, py: Python<'_>) -> Bound<'_, PyAny> { - Bound::from_owned_ptr_unchecked(py, self) + unsafe { Bound::from_owned_ptr_unchecked(py, self) } } #[inline] @@ -64,22 +64,22 @@ impl FfiPtrExt for *mut ffi::PyObject { self, py: Python<'_>, ) -> PyResult> { - Borrowed::from_ptr_or_err(py, self) + unsafe { Borrowed::from_ptr_or_err(py, self) } } #[inline] unsafe fn assume_borrowed_or_opt<'a>(self, py: Python<'_>) -> Option> { - Borrowed::from_ptr_or_opt(py, self) + unsafe { Borrowed::from_ptr_or_opt(py, self) } } #[inline] #[track_caller] unsafe fn assume_borrowed<'a>(self, py: Python<'_>) -> Borrowed<'a, '_, PyAny> { - Borrowed::from_ptr(py, self) + unsafe { Borrowed::from_ptr(py, self) } } #[inline] unsafe fn assume_borrowed_unchecked<'a>(self, py: Python<'_>) -> Borrowed<'a, '_, PyAny> { - Borrowed::from_ptr_unchecked(py, self) + unsafe { Borrowed::from_ptr_unchecked(py, self) } } } diff --git a/src/gil.rs b/src/gil.rs index d7e6daae918..5fccb04ef6a 100644 --- a/src/gil.rs +++ b/src/gil.rs @@ -111,15 +111,15 @@ where F: for<'p> FnOnce(Python<'p>) -> R, { assert_eq!( - ffi::Py_IsInitialized(), + unsafe { ffi::Py_IsInitialized() }, 0, "called `with_embedded_python_interpreter` but a Python interpreter is already running." ); - ffi::Py_InitializeEx(0); + unsafe { ffi::Py_InitializeEx(0) }; let result = { - let guard = GILGuard::assume(); + let guard = unsafe { GILGuard::assume() }; let py = guard.python(); // Import the threading module - this ensures that it will associate this thread as the "main" // thread, which is important to avoid an `AssertionError` at finalization. @@ -130,7 +130,7 @@ where }; // Finalize the Python interpreter. - ffi::Py_Finalize(); + unsafe { ffi::Py_Finalize() }; result } @@ -201,15 +201,15 @@ impl GILGuard { /// as part of multi-phase interpreter initialization. pub(crate) unsafe fn acquire_unchecked() -> Self { if gil_is_acquired() { - return Self::assume(); + return unsafe { Self::assume() }; } - let gstate = ffi::PyGILState_Ensure(); // acquire GIL + let gstate = unsafe { ffi::PyGILState_Ensure() }; // acquire GIL increment_gil_count(); #[cfg(not(pyo3_disable_reference_pool))] if let Some(pool) = Lazy::get(&POOL) { - pool.update_counts(Python::assume_gil_acquired()); + pool.update_counts(unsafe { Python::assume_gil_acquired() }); } GILGuard::Ensured { gstate } } @@ -300,7 +300,7 @@ pub(crate) struct SuspendGIL { impl SuspendGIL { pub(crate) unsafe fn new() -> Self { let count = GIL_COUNT.with(|c| c.replace(0)); - let tstate = ffi::PyEval_SaveThread(); + let tstate = unsafe { ffi::PyEval_SaveThread() }; Self { count, tstate } } @@ -364,7 +364,7 @@ impl Drop for LockGIL { #[track_caller] pub unsafe fn register_incref(obj: NonNull) { if gil_is_acquired() { - ffi::Py_INCREF(obj.as_ptr()) + unsafe { ffi::Py_INCREF(obj.as_ptr()) } } else { panic!("Cannot clone pointer into Python heap without the GIL being held."); } @@ -381,7 +381,7 @@ pub unsafe fn register_incref(obj: NonNull) { #[track_caller] pub unsafe fn register_decref(obj: NonNull) { if gil_is_acquired() { - ffi::Py_DECREF(obj.as_ptr()) + unsafe { ffi::Py_DECREF(obj.as_ptr()) } } else { #[cfg(not(pyo3_disable_reference_pool))] POOL.register_decref(obj); @@ -617,13 +617,15 @@ mod tests { unsafe extern "C" fn capsule_drop(capsule: *mut ffi::PyObject) { // This line will implicitly call update_counts // -> and so cause deadlock if update_counts is not handling recursion correctly. - let pool = GILGuard::assume(); + let pool = unsafe { GILGuard::assume() }; // Rebuild obj so that it can be dropped - PyObject::from_owned_ptr( - pool.python(), - ffi::PyCapsule_GetPointer(capsule, std::ptr::null()) as _, - ); + unsafe { + PyObject::from_owned_ptr( + pool.python(), + ffi::PyCapsule_GetPointer(capsule, std::ptr::null()) as _, + ) + }; } let ptr = obj.into_ptr(); diff --git a/src/impl_/extract_argument.rs b/src/impl_/extract_argument.rs index cbf79a14707..f06298dface 100644 --- a/src/impl_/extract_argument.rs +++ b/src/impl_/extract_argument.rs @@ -296,9 +296,10 @@ impl FunctionDescription { // the rest are varargs. let positional_args_to_consume = num_positional_parameters.min(positional_args_provided); - let (positional_parameters, remaining) = + let (positional_parameters, remaining) = unsafe { std::slice::from_raw_parts(args, positional_args_provided) - .split_at(positional_args_to_consume); + .split_at(positional_args_to_consume) + }; output[..positional_args_to_consume].copy_from_slice(positional_parameters); remaining }; @@ -309,14 +310,17 @@ impl FunctionDescription { // Safety: kwnames is known to be a pointer to a tuple, or null // - we both have the GIL and can borrow this input reference for the `'py` lifetime. - let kwnames: Option> = - Borrowed::from_ptr_or_opt(py, kwnames).map(|kwnames| kwnames.downcast_unchecked()); + let kwnames: Option> = unsafe { + Borrowed::from_ptr_or_opt(py, kwnames).map(|kwnames| kwnames.downcast_unchecked()) + }; if let Some(kwnames) = kwnames { - let kwargs = ::std::slice::from_raw_parts( - // Safety: PyArg has the same memory layout as `*mut ffi::PyObject` - args.offset(nargs).cast::>(), - kwnames.len(), - ); + let kwargs = unsafe { + ::std::slice::from_raw_parts( + // Safety: PyArg has the same memory layout as `*mut ffi::PyObject` + args.offset(nargs).cast::>(), + kwnames.len(), + ) + }; self.handle_kwargs::( kwnames.iter_borrowed().zip(kwargs.iter().copied()), @@ -362,9 +366,10 @@ impl FunctionDescription { // - `kwargs` is known to be a dict or null // - we both have the GIL and can borrow these input references for the `'py` lifetime. let args: Borrowed<'py, 'py, PyTuple> = - Borrowed::from_ptr(py, args).downcast_unchecked::(); - let kwargs: Option> = - Borrowed::from_ptr_or_opt(py, kwargs).map(|kwargs| kwargs.downcast_unchecked()); + unsafe { Borrowed::from_ptr(py, args).downcast_unchecked::() }; + let kwargs: Option> = unsafe { + Borrowed::from_ptr_or_opt(py, kwargs).map(|kwargs| kwargs.downcast_unchecked()) + }; let num_positional_parameters = self.positional_parameter_names.len(); @@ -391,7 +396,7 @@ impl FunctionDescription { let mut varkeywords = K::Varkeywords::default(); if let Some(kwargs) = kwargs { self.handle_kwargs::( - kwargs.iter_borrowed(), + unsafe { kwargs.iter_borrowed() }, &mut varkeywords, num_positional_parameters, output, diff --git a/src/impl_/pyclass.rs b/src/impl_/pyclass.rs index 06ec83d6ff2..270ba2de47e 100644 --- a/src/impl_/pyclass.rs +++ b/src/impl_/pyclass.rs @@ -116,7 +116,7 @@ impl PyClassWeakRef for PyClassWeakRefSlot { #[inline] unsafe fn clear_weakrefs(&mut self, obj: *mut ffi::PyObject, _py: Python<'_>) { if !self.0.is_null() { - ffi::PyObject_ClearWeakRefs(obj) + unsafe { ffi::PyObject_ClearWeakRefs(obj) } } } } @@ -332,7 +332,7 @@ slot_fragment_trait! { slf: *mut ffi::PyObject, attr: *mut ffi::PyObject, ) -> PyResult<*mut ffi::PyObject> { - let res = ffi::PyObject_GenericGetAttr(slf, attr); + let res = unsafe { ffi::PyObject_GenericGetAttr(slf, attr) }; if res.is_null() { Err(PyErr::fetch(py)) } else { @@ -353,7 +353,7 @@ slot_fragment_trait! { attr: *mut ffi::PyObject, ) -> PyResult<*mut ffi::PyObject> { Err(PyErr::new::( - (Py::::from_borrowed_ptr(py, attr),) + (unsafe {Py::::from_borrowed_ptr(py, attr)},) )) } } @@ -366,24 +366,26 @@ macro_rules! generate_pyclass_getattro_slot { _slf: *mut $crate::ffi::PyObject, attr: *mut $crate::ffi::PyObject, ) -> *mut $crate::ffi::PyObject { - $crate::impl_::trampoline::getattrofunc(_slf, attr, |py, _slf, attr| { - use ::std::result::Result::*; - use $crate::impl_::pyclass::*; - let collector = PyClassImplCollector::<$cls>::new(); - - // Strategy: - // - Try __getattribute__ first. Its default is PyObject_GenericGetAttr. - // - If it returns a result, use it. - // - If it fails with AttributeError, try __getattr__. - // - If it fails otherwise, reraise. - match collector.__getattribute__(py, _slf, attr) { - Ok(obj) => Ok(obj), - Err(e) if e.is_instance_of::<$crate::exceptions::PyAttributeError>(py) => { - collector.__getattr__(py, _slf, attr) + unsafe { + $crate::impl_::trampoline::getattrofunc(_slf, attr, |py, _slf, attr| { + use ::std::result::Result::*; + use $crate::impl_::pyclass::*; + let collector = PyClassImplCollector::<$cls>::new(); + + // Strategy: + // - Try __getattribute__ first. Its default is PyObject_GenericGetAttr. + // - If it returns a result, use it. + // - If it fails with AttributeError, try __getattr__. + // - If it fails otherwise, reraise. + match collector.__getattribute__(py, _slf, attr) { + Ok(obj) => Ok(obj), + Err(e) if e.is_instance_of::<$crate::exceptions::PyAttributeError>(py) => { + collector.__getattr__(py, _slf, attr) + } + Err(e) => Err(e), } - Err(e) => Err(e), - } - }) + }) + } } $crate::ffi::PyType_Slot { slot: $crate::ffi::Py_tp_getattro, @@ -450,22 +452,24 @@ macro_rules! define_pyclass_setattr_slot { attr: *mut $crate::ffi::PyObject, value: *mut $crate::ffi::PyObject, ) -> ::std::os::raw::c_int { - $crate::impl_::trampoline::setattrofunc( - _slf, - attr, - value, - |py, _slf, attr, value| { - use ::std::option::Option::*; - use $crate::impl_::callback::IntoPyCallbackOutput; - use $crate::impl_::pyclass::*; - let collector = PyClassImplCollector::<$cls>::new(); - if let Some(value) = ::std::ptr::NonNull::new(value) { - collector.$set(py, _slf, attr, value).convert(py) - } else { - collector.$del(py, _slf, attr).convert(py) - } - }, - ) + unsafe { + $crate::impl_::trampoline::setattrofunc( + _slf, + attr, + value, + |py, _slf, attr, value| { + use ::std::option::Option::*; + use $crate::impl_::callback::IntoPyCallbackOutput; + use $crate::impl_::pyclass::*; + let collector = PyClassImplCollector::<$cls>::new(); + if let Some(value) = ::std::ptr::NonNull::new(value) { + collector.$set(py, _slf, attr, value).convert(py) + } else { + collector.$del(py, _slf, attr).convert(py) + } + }, + ) + } } $crate::ffi::PyType_Slot { slot: $crate::ffi::$slot, @@ -565,17 +569,19 @@ macro_rules! define_pyclass_binary_operator_slot { _slf: *mut $crate::ffi::PyObject, _other: *mut $crate::ffi::PyObject, ) -> *mut $crate::ffi::PyObject { - $crate::impl_::trampoline::binaryfunc(_slf, _other, |py, _slf, _other| { - use $crate::impl_::pyclass::*; - let collector = PyClassImplCollector::<$cls>::new(); - let lhs_result = collector.$lhs(py, _slf, _other)?; - if lhs_result == $crate::ffi::Py_NotImplemented() { - $crate::ffi::Py_DECREF(lhs_result); - collector.$rhs(py, _other, _slf) - } else { - ::std::result::Result::Ok(lhs_result) - } - }) + unsafe { + $crate::impl_::trampoline::binaryfunc(_slf, _other, |py, _slf, _other| { + use $crate::impl_::pyclass::*; + let collector = PyClassImplCollector::<$cls>::new(); + let lhs_result = collector.$lhs(py, _slf, _other)?; + if lhs_result == $crate::ffi::Py_NotImplemented() { + $crate::ffi::Py_DECREF(lhs_result); + collector.$rhs(py, _other, _slf) + } else { + ::std::result::Result::Ok(lhs_result) + } + }) + } } $crate::ffi::PyType_Slot { slot: $crate::ffi::$slot, @@ -758,17 +764,24 @@ macro_rules! generate_pyclass_pow_slot { _other: *mut $crate::ffi::PyObject, _mod: *mut $crate::ffi::PyObject, ) -> *mut $crate::ffi::PyObject { - $crate::impl_::trampoline::ternaryfunc(_slf, _other, _mod, |py, _slf, _other, _mod| { - use $crate::impl_::pyclass::*; - let collector = PyClassImplCollector::<$cls>::new(); - let lhs_result = collector.__pow__(py, _slf, _other, _mod)?; - if lhs_result == $crate::ffi::Py_NotImplemented() { - $crate::ffi::Py_DECREF(lhs_result); - collector.__rpow__(py, _other, _slf, _mod) - } else { - ::std::result::Result::Ok(lhs_result) - } - }) + unsafe { + $crate::impl_::trampoline::ternaryfunc( + _slf, + _other, + _mod, + |py, _slf, _other, _mod| { + use $crate::impl_::pyclass::*; + let collector = PyClassImplCollector::<$cls>::new(); + let lhs_result = collector.__pow__(py, _slf, _other, _mod)?; + if lhs_result == $crate::ffi::Py_NotImplemented() { + $crate::ffi::Py_DECREF(lhs_result); + collector.__rpow__(py, _other, _slf, _mod) + } else { + ::std::result::Result::Ok(lhs_result) + } + }, + ) + } } $crate::ffi::PyType_Slot { slot: $crate::ffi::Py_nb_power, @@ -835,8 +848,8 @@ slot_fragment_trait! { other: *mut ffi::PyObject, ) -> PyResult<*mut ffi::PyObject> { // By default `__ne__` will try `__eq__` and invert the result - let slf = Borrowed::from_ptr(py, slf); - let other = Borrowed::from_ptr(py, other); + let slf = unsafe { Borrowed::from_ptr(py, slf)}; + let other = unsafe { Borrowed::from_ptr(py, other)}; slf.eq(other).map(|is_eq| PyBool::new(py, !is_eq).to_owned().into_ptr()) } } @@ -883,19 +896,21 @@ macro_rules! generate_pyclass_richcompare_slot { other: *mut $crate::ffi::PyObject, op: ::std::os::raw::c_int, ) -> *mut $crate::ffi::PyObject { - $crate::impl_::trampoline::richcmpfunc(slf, other, op, |py, slf, other, op| { - use $crate::class::basic::CompareOp; - use $crate::impl_::pyclass::*; - let collector = PyClassImplCollector::<$cls>::new(); - match CompareOp::from_raw(op).expect("invalid compareop") { - CompareOp::Lt => collector.__lt__(py, slf, other), - CompareOp::Le => collector.__le__(py, slf, other), - CompareOp::Eq => collector.__eq__(py, slf, other), - CompareOp::Ne => collector.__ne__(py, slf, other), - CompareOp::Gt => collector.__gt__(py, slf, other), - CompareOp::Ge => collector.__ge__(py, slf, other), - } - }) + unsafe { + $crate::impl_::trampoline::richcmpfunc(slf, other, op, |py, slf, other, op| { + use $crate::class::basic::CompareOp; + use $crate::impl_::pyclass::*; + let collector = PyClassImplCollector::<$cls>::new(); + match CompareOp::from_raw(op).expect("invalid compareop") { + CompareOp::Lt => collector.__lt__(py, slf, other), + CompareOp::Le => collector.__le__(py, slf, other), + CompareOp::Eq => collector.__eq__(py, slf, other), + CompareOp::Ne => collector.__ne__(py, slf, other), + CompareOp::Gt => collector.__gt__(py, slf, other), + CompareOp::Ge => collector.__ge__(py, slf, other), + } + }) + } } } $crate::ffi::PyType_Slot { @@ -925,10 +940,12 @@ pub unsafe extern "C" fn alloc_with_freelist( subtype: *mut ffi::PyTypeObject, nitems: ffi::Py_ssize_t, ) -> *mut ffi::PyObject { - let py = Python::assume_gil_acquired(); + let py = unsafe { Python::assume_gil_acquired() }; #[cfg(not(Py_3_8))] - bpo_35810_workaround(py, subtype); + unsafe { + bpo_35810_workaround(py, subtype) + }; let self_type = T::type_object_raw(py); // If this type is a variable type or the subtype is not equal to this type, we cannot use the @@ -937,12 +954,13 @@ pub unsafe extern "C" fn alloc_with_freelist( let mut free_list = T::get_free_list(py).lock().unwrap(); if let Some(obj) = free_list.pop() { drop(free_list); - ffi::PyObject_Init(obj, subtype); + unsafe { ffi::PyObject_Init(obj, subtype) }; + unsafe { ffi::PyObject_Init(obj, subtype) }; return obj as _; } } - ffi::PyType_GenericAlloc(subtype, nitems) + unsafe { ffi::PyType_GenericAlloc(subtype, nitems) } } /// Implementation of tp_free for `freelist` classes. @@ -952,28 +970,30 @@ pub unsafe extern "C" fn alloc_with_freelist( /// - The GIL must be held. pub unsafe extern "C" fn free_with_freelist(obj: *mut c_void) { let obj = obj as *mut ffi::PyObject; - debug_assert_eq!( - T::type_object_raw(Python::assume_gil_acquired()), - ffi::Py_TYPE(obj) - ); - let mut free_list = T::get_free_list(Python::assume_gil_acquired()) - .lock() - .unwrap(); - if let Some(obj) = free_list.insert(obj) { - drop(free_list); - let ty = ffi::Py_TYPE(obj); - - // Deduce appropriate inverse of PyType_GenericAlloc - let free = if ffi::PyType_IS_GC(ty) != 0 { - ffi::PyObject_GC_Del - } else { - ffi::PyObject_Free - }; - free(obj as *mut c_void); - - #[cfg(Py_3_8)] - if ffi::PyType_HasFeature(ty, ffi::Py_TPFLAGS_HEAPTYPE) != 0 { - ffi::Py_DECREF(ty as *mut ffi::PyObject); + unsafe { + debug_assert_eq!( + T::type_object_raw(Python::assume_gil_acquired()), + ffi::Py_TYPE(obj) + ); + let mut free_list = T::get_free_list(Python::assume_gil_acquired()) + .lock() + .unwrap(); + if let Some(obj) = free_list.insert(obj) { + drop(free_list); + let ty = ffi::Py_TYPE(obj); + + // Deduce appropriate inverse of PyType_GenericAlloc + let free = if ffi::PyType_IS_GC(ty) != 0 { + ffi::PyObject_GC_Del + } else { + ffi::PyObject_Free + }; + free(obj as *mut c_void); + + #[cfg(Py_3_8)] + if ffi::PyType_HasFeature(ty, ffi::Py_TPFLAGS_HEAPTYPE) != 0 { + ffi::Py_DECREF(ty as *mut ffi::PyObject); + } } } } @@ -1000,7 +1020,7 @@ unsafe fn bpo_35810_workaround(py: Python<'_>, ty: *mut ffi::PyTypeObject) { let _ = py; } - ffi::Py_INCREF(ty as *mut ffi::PyObject); + unsafe { ffi::Py_INCREF(ty as *mut ffi::PyObject) }; } /// Method storage for `#[pyclass]`. @@ -1150,28 +1170,28 @@ pub trait PyClassBaseType: Sized { /// Implementation of tp_dealloc for pyclasses without gc pub(crate) unsafe extern "C" fn tp_dealloc(obj: *mut ffi::PyObject) { - crate::impl_::trampoline::dealloc(obj, PyClassObject::::tp_dealloc) + unsafe { crate::impl_::trampoline::dealloc(obj, PyClassObject::::tp_dealloc) } } /// Implementation of tp_dealloc for pyclasses with gc pub(crate) unsafe extern "C" fn tp_dealloc_with_gc(obj: *mut ffi::PyObject) { #[cfg(not(PyPy))] - { + unsafe { ffi::PyObject_GC_UnTrack(obj.cast()); } - crate::impl_::trampoline::dealloc(obj, PyClassObject::::tp_dealloc) + unsafe { crate::impl_::trampoline::dealloc(obj, PyClassObject::::tp_dealloc) } } pub(crate) unsafe extern "C" fn get_sequence_item_from_mapping( obj: *mut ffi::PyObject, index: ffi::Py_ssize_t, ) -> *mut ffi::PyObject { - let index = ffi::PyLong_FromSsize_t(index); + let index = unsafe { ffi::PyLong_FromSsize_t(index) }; if index.is_null() { return std::ptr::null_mut(); } - let result = ffi::PyObject_GetItem(obj, index); - ffi::Py_DECREF(index); + let result = unsafe { ffi::PyObject_GetItem(obj, index) }; + unsafe { ffi::Py_DECREF(index) }; result } @@ -1180,17 +1200,19 @@ pub(crate) unsafe extern "C" fn assign_sequence_item_from_mapping( index: ffi::Py_ssize_t, value: *mut ffi::PyObject, ) -> c_int { - let index = ffi::PyLong_FromSsize_t(index); - if index.is_null() { - return -1; + unsafe { + let index = ffi::PyLong_FromSsize_t(index); + if index.is_null() { + return -1; + } + let result = if value.is_null() { + ffi::PyObject_DelItem(obj, index) + } else { + ffi::PyObject_SetItem(obj, index, value) + }; + ffi::Py_DECREF(index); + result } - let result = if value.is_null() { - ffi::PyObject_DelItem(obj, index) - } else { - ffi::PyObject_SetItem(obj, index, value) - }; - ffi::Py_DECREF(index); - result } /// Helper trait to locate field within a `#[pyclass]` for a `#[pyo3(get)]`. @@ -1437,9 +1459,11 @@ unsafe fn ensure_no_mutable_alias<'py, ClassT: PyClass>( py: Python<'py>, obj: &*mut ffi::PyObject, ) -> Result, PyBorrowError> { - BoundRef::ref_from_ptr(py, obj) - .downcast_unchecked::() - .try_borrow() + unsafe { + BoundRef::ref_from_ptr(py, obj) + .downcast_unchecked::() + .try_borrow() + } } /// calculates the field pointer from an PyObject pointer diff --git a/src/impl_/pyclass_init.rs b/src/impl_/pyclass_init.rs index 7242b6186d9..ff0e4e80e96 100644 --- a/src/impl_/pyclass_init.rs +++ b/src/impl_/pyclass_init.rs @@ -39,17 +39,19 @@ impl PyObjectInit for PyNativeTypeInitializer { ) -> PyResult<*mut ffi::PyObject> { // HACK (due to FIXME below): PyBaseObject_Type's tp_new isn't happy with NULL arguments let is_base_object = type_object == std::ptr::addr_of_mut!(ffi::PyBaseObject_Type); - let subtype_borrowed: Borrowed<'_, '_, PyType> = subtype - .cast::() - .assume_borrowed_unchecked(py) - .downcast_unchecked(); + let subtype_borrowed: Borrowed<'_, '_, PyType> = unsafe { + subtype + .cast::() + .assume_borrowed_unchecked(py) + .downcast_unchecked() + }; if is_base_object { let alloc = subtype_borrowed .get_slot(TP_ALLOC) .unwrap_or(ffi::PyType_GenericAlloc); - let obj = alloc(subtype, 0); + let obj = unsafe { alloc(subtype, 0) }; return if obj.is_null() { Err(PyErr::fetch(py)) } else { @@ -62,10 +64,11 @@ impl PyObjectInit for PyNativeTypeInitializer { #[cfg(not(Py_LIMITED_API))] { - match (*type_object).tp_new { + match unsafe { (*type_object).tp_new } { // FIXME: Call __new__ with actual arguments Some(newfunc) => { - let obj = newfunc(subtype, std::ptr::null_mut(), std::ptr::null_mut()); + let obj = + unsafe { newfunc(subtype, std::ptr::null_mut(), std::ptr::null_mut()) }; if obj.is_null() { Err(PyErr::fetch(py)) } else { @@ -79,7 +82,7 @@ impl PyObjectInit for PyNativeTypeInitializer { } } let type_object = T::type_object_raw(py); - inner(py, type_object, subtype) + unsafe { inner(py, type_object, subtype) } } #[inline] diff --git a/src/impl_/pymethods.rs b/src/impl_/pymethods.rs index e6cbaec86f5..8f869068463 100644 --- a/src/impl_/pymethods.rs +++ b/src/impl_/pymethods.rs @@ -295,14 +295,14 @@ where let trap = PanicTrap::new("uncaught panic inside __traverse__ handler"); let lock = LockGIL::during_traverse(); - let super_retval = call_super_traverse(slf, visit, arg, current_traverse); + let super_retval = unsafe { call_super_traverse(slf, visit, arg, current_traverse) }; if super_retval != 0 { return super_retval; } // SAFETY: `slf` is a valid Python object pointer to a class object of type T, and // traversal is running so no mutations can occur. - let class_object: &PyClassObject = &*slf.cast(); + let class_object: &PyClassObject = unsafe { &*slf.cast() }; let retval = // `#[pyclass(unsendable)]` types can only be deallocated by their own thread, so @@ -320,7 +320,7 @@ where // `.try_borrow()` above created a borrow, we need to release it when we're done // traversing the object. This allows us to read `instance` safely. let _guard = TraverseGuard(class_object); - let instance = &*class_object.contents.value.get(); + let instance = unsafe {&*class_object.contents.value.get()}; let visit = PyVisit { visit, arg, _guard: PhantomData }; @@ -359,16 +359,16 @@ unsafe fn call_super_traverse( // because the GC is running and so // - (a) we cannot do refcounting and // - (b) the type of the object cannot change. - let mut ty = ffi::Py_TYPE(obj); + let mut ty = unsafe { ffi::Py_TYPE(obj) }; let mut traverse: Option; // First find the current type by the current_traverse function loop { - traverse = get_slot(ty, TP_TRAVERSE); + traverse = unsafe { get_slot(ty, TP_TRAVERSE) }; if traverse_eq(traverse, current_traverse) { break; } - ty = get_slot(ty, TP_BASE); + ty = unsafe { get_slot(ty, TP_BASE) }; if ty.is_null() { // FIXME: return an error if current type not in the MRO? Should be impossible. return 0; @@ -377,16 +377,16 @@ unsafe fn call_super_traverse( // Get first base which has a different traverse function while traverse_eq(traverse, current_traverse) { - ty = get_slot(ty, TP_BASE); + ty = unsafe { get_slot(ty, TP_BASE) }; if ty.is_null() { break; } - traverse = get_slot(ty, TP_TRAVERSE); + traverse = unsafe { get_slot(ty, TP_TRAVERSE) }; } // If we found a type with a different traverse function, call it if let Some(traverse) = traverse { - return traverse(obj, visit, arg); + return unsafe { traverse(obj, visit, arg) }; } // FIXME same question as cython: what if the current type is not in the MRO? @@ -399,14 +399,16 @@ pub unsafe fn _call_clear( impl_: for<'py> unsafe fn(Python<'py>, *mut ffi::PyObject) -> PyResult<()>, current_clear: ffi::inquiry, ) -> c_int { - trampoline::trampoline(move |py| { - let super_retval = call_super_clear(py, slf, current_clear); - if super_retval != 0 { - return Err(PyErr::fetch(py)); - } - impl_(py, slf)?; - Ok(0) - }) + unsafe { + trampoline::trampoline(move |py| { + let super_retval = call_super_clear(py, slf, current_clear); + if super_retval != 0 { + return Err(PyErr::fetch(py)); + } + impl_(py, slf)?; + Ok(0) + }) + } } /// Call super-type traverse method, if necessary. @@ -424,7 +426,7 @@ unsafe fn call_super_clear( obj: *mut ffi::PyObject, current_clear: ffi::inquiry, ) -> c_int { - let mut ty = PyType::from_borrowed_type_ptr(py, ffi::Py_TYPE(obj)); + let mut ty = unsafe { PyType::from_borrowed_type_ptr(py, ffi::Py_TYPE(obj)) }; let mut clear: Option; // First find the current type by the current_clear function @@ -438,7 +440,7 @@ unsafe fn call_super_clear( // FIXME: return an error if current type not in the MRO? Should be impossible. return 0; } - ty = PyType::from_borrowed_type_ptr(py, base); + ty = unsafe { PyType::from_borrowed_type_ptr(py, base) }; } // Get first base which has a different clear function @@ -447,13 +449,13 @@ unsafe fn call_super_clear( if base.is_null() { break; } - ty = PyType::from_borrowed_type_ptr(py, base); + ty = unsafe { PyType::from_borrowed_type_ptr(py, base) }; clear = ty.get_slot(TP_CLEAR); } // If we found a type with a different clear function, call it if let Some(clear) = clear { - return clear(obj); + return unsafe { clear(obj) }; } // FIXME same question as cython: what if the current type is not in the MRO? @@ -633,14 +635,14 @@ pub struct BoundRef<'a, 'py, T>(pub &'a Bound<'py, T>); impl<'a, 'py> BoundRef<'a, 'py, PyAny> { pub unsafe fn ref_from_ptr(py: Python<'py>, ptr: &'a *mut ffi::PyObject) -> Self { - BoundRef(Bound::ref_from_ptr(py, ptr)) + unsafe { BoundRef(Bound::ref_from_ptr(py, ptr)) } } pub unsafe fn ref_from_ptr_or_opt( py: Python<'py>, ptr: &'a *mut ffi::PyObject, ) -> Option { - Bound::ref_from_ptr_or_opt(py, ptr).as_ref().map(BoundRef) + unsafe { Bound::ref_from_ptr_or_opt(py, ptr).as_ref().map(BoundRef) } } pub fn downcast(self) -> Result, DowncastError<'a, 'py>> { @@ -648,7 +650,7 @@ impl<'a, 'py> BoundRef<'a, 'py, PyAny> { } pub unsafe fn downcast_unchecked(self) -> BoundRef<'a, 'py, T> { - BoundRef(self.0.downcast_unchecked::()) + unsafe { BoundRef(self.0.downcast_unchecked::()) } } } @@ -702,9 +704,11 @@ pub unsafe fn tp_new_impl( initializer: PyClassInitializer, target_type: *mut ffi::PyTypeObject, ) -> PyResult<*mut ffi::PyObject> { - initializer - .create_class_object_of_type(py, target_type) - .map(Bound::into_ptr) + unsafe { + initializer + .create_class_object_of_type(py, target_type) + .map(Bound::into_ptr) + } } #[cfg(test)] @@ -725,7 +729,7 @@ mod tests { ) -> *mut ffi::PyObject { assert_eq!(nargs, 0); assert!(kwargs.is_null()); - Python::assume_gil_acquired().None().into_ptr() + unsafe { Python::assume_gil_acquired().None().into_ptr() } } let f = PyCFunction::internal_new( diff --git a/src/impl_/trampoline.rs b/src/impl_/trampoline.rs index 7ffad8abdcd..01f8f5e8b0e 100644 --- a/src/impl_/trampoline.rs +++ b/src/impl_/trampoline.rs @@ -19,7 +19,7 @@ use crate::{ pub unsafe fn module_init( f: for<'py> unsafe fn(Python<'py>) -> PyResult>, ) -> *mut ffi::PyObject { - trampoline(|py| f(py).map(|module| module.into_ptr())) + unsafe { trampoline(|py| f(py).map(|module| module.into_ptr())) } } #[inline] @@ -31,7 +31,7 @@ pub unsafe fn noargs( ) -> *mut ffi::PyObject { #[cfg(not(GraalPy))] // this is not specified and GraalPy does not pass null here debug_assert!(_args.is_null()); - trampoline(|py| f(py, slf)) + unsafe { trampoline(|py| f(py, slf)) } } macro_rules! trampoline { @@ -41,7 +41,7 @@ macro_rules! trampoline { $($arg_names: $arg_types,)* f: for<'py> unsafe fn (Python<'py>, $($arg_types),*) -> PyResult<$ret>, ) -> $ret { - trampoline(|py| f(py, $($arg_names,)*)) + unsafe {trampoline(|py| f(py, $($arg_names,)*))} } } } @@ -134,7 +134,7 @@ pub unsafe fn releasebufferproc( buf: *mut ffi::Py_buffer, f: for<'py> unsafe fn(Python<'py>, *mut ffi::PyObject, *mut ffi::Py_buffer) -> PyResult<()>, ) { - trampoline_unraisable(|py| f(py, slf, buf), slf) + unsafe { trampoline_unraisable(|py| f(py, slf, buf), slf) } } #[inline] @@ -146,13 +146,15 @@ pub(crate) unsafe fn dealloc( // so pass null_mut() to the context. // // (Note that we don't allow the implementation `f` to fail.) - trampoline_unraisable( - |py| { - f(py, slf); - Ok(()) - }, - std::ptr::null_mut(), - ) + unsafe { + trampoline_unraisable( + |py| { + f(py, slf); + Ok(()) + }, + std::ptr::null_mut(), + ) + } } // Ipowfunc is a unique case where PyO3 has its own type @@ -181,7 +183,7 @@ where let trap = PanicTrap::new("uncaught panic at ffi boundary"); // SAFETY: This function requires the GIL to already be held. - let guard = GILGuard::assume(); + let guard = unsafe { GILGuard::assume() }; let py = guard.python(); let out = panic_result_into_callback_output( py, @@ -229,13 +231,13 @@ where let trap = PanicTrap::new("uncaught panic at ffi boundary"); // SAFETY: The GIL is already held. - let guard = GILGuard::assume(); + let guard = unsafe { GILGuard::assume() }; let py = guard.python(); if let Err(py_err) = panic::catch_unwind(move || body(py)) .unwrap_or_else(|payload| Err(PanicException::from_panic_payload(payload))) { - py_err.write_unraisable(py, ctx.assume_borrowed_or_opt(py).as_deref()); + py_err.write_unraisable(py, unsafe { ctx.assume_borrowed_or_opt(py) }.as_deref()); } trap.disarm(); } diff --git a/src/instance.rs b/src/instance.rs index f3fe7a91783..7a32f4c9c30 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -107,7 +107,10 @@ impl<'py> Bound<'py, PyAny> { #[inline] #[track_caller] pub unsafe fn from_owned_ptr(py: Python<'py>, ptr: *mut ffi::PyObject) -> Self { - Self(py, ManuallyDrop::new(Py::from_owned_ptr(py, ptr))) + Self( + py, + ManuallyDrop::new(unsafe { Py::from_owned_ptr(py, ptr) }), + ) } /// Constructs a new `Bound<'py, PyAny>` from a pointer. Returns `None` if `ptr` is null. @@ -118,7 +121,7 @@ impl<'py> Bound<'py, PyAny> { /// - `ptr` must be an owned Python reference, as the `Bound<'py, PyAny>` will assume ownership #[inline] pub unsafe fn from_owned_ptr_or_opt(py: Python<'py>, ptr: *mut ffi::PyObject) -> Option { - Py::from_owned_ptr_or_opt(py, ptr).map(|obj| Self(py, ManuallyDrop::new(obj))) + unsafe { Py::from_owned_ptr_or_opt(py, ptr) }.map(|obj| Self(py, ManuallyDrop::new(obj))) } /// Constructs a new `Bound<'py, PyAny>` from a pointer. Returns an `Err` by calling `PyErr::fetch` @@ -133,7 +136,7 @@ impl<'py> Bound<'py, PyAny> { py: Python<'py>, ptr: *mut ffi::PyObject, ) -> PyResult { - Py::from_owned_ptr_or_err(py, ptr).map(|obj| Self(py, ManuallyDrop::new(obj))) + unsafe { Py::from_owned_ptr_or_err(py, ptr) }.map(|obj| Self(py, ManuallyDrop::new(obj))) } /// Constructs a new `Bound<'py, PyAny>` from a pointer without checking for null. @@ -146,7 +149,10 @@ impl<'py> Bound<'py, PyAny> { py: Python<'py>, ptr: *mut ffi::PyObject, ) -> Self { - Self(py, ManuallyDrop::new(Py::from_owned_ptr_unchecked(ptr))) + Self( + py, + ManuallyDrop::new(unsafe { Py::from_owned_ptr_unchecked(ptr) }), + ) } /// Constructs a new `Bound<'py, PyAny>` from a pointer by creating a new Python reference. @@ -158,7 +164,7 @@ impl<'py> Bound<'py, PyAny> { #[inline] #[track_caller] pub unsafe fn from_borrowed_ptr(py: Python<'py>, ptr: *mut ffi::PyObject) -> Self { - Self(py, ManuallyDrop::new(Py::from_borrowed_ptr(py, ptr))) + unsafe { Self(py, ManuallyDrop::new(Py::from_borrowed_ptr(py, ptr))) } } /// Constructs a new `Bound<'py, PyAny>` from a pointer by creating a new Python reference. @@ -172,7 +178,7 @@ impl<'py> Bound<'py, PyAny> { py: Python<'py>, ptr: *mut ffi::PyObject, ) -> Option { - Py::from_borrowed_ptr_or_opt(py, ptr).map(|obj| Self(py, ManuallyDrop::new(obj))) + unsafe { Py::from_borrowed_ptr_or_opt(py, ptr).map(|obj| Self(py, ManuallyDrop::new(obj))) } } /// Constructs a new `Bound<'py, PyAny>` from a pointer by creating a new Python reference. @@ -186,7 +192,7 @@ impl<'py> Bound<'py, PyAny> { py: Python<'py>, ptr: *mut ffi::PyObject, ) -> PyResult { - Py::from_borrowed_ptr_or_err(py, ptr).map(|obj| Self(py, ManuallyDrop::new(obj))) + unsafe { Py::from_borrowed_ptr_or_err(py, ptr).map(|obj| Self(py, ManuallyDrop::new(obj))) } } /// This slightly strange method is used to obtain `&Bound` from a pointer in macro code @@ -204,7 +210,7 @@ impl<'py> Bound<'py, PyAny> { _py: Python<'py>, ptr: &'a *mut ffi::PyObject, ) -> &'a Self { - &*ptr_from_ref(ptr).cast::>() + unsafe { &*ptr_from_ref(ptr).cast::>() } } /// Variant of the above which returns `None` for null pointers. @@ -216,7 +222,7 @@ impl<'py> Bound<'py, PyAny> { _py: Python<'py>, ptr: &'a *mut ffi::PyObject, ) -> &'a Option { - &*ptr_from_ref(ptr).cast::>>() + unsafe { &*ptr_from_ref(ptr).cast::>>() } } } @@ -762,7 +768,7 @@ impl<'a, 'py> Borrowed<'a, 'py, PyAny> { /// derived from is valid for the lifetime `'a`. #[inline] pub(crate) unsafe fn from_ptr_unchecked(py: Python<'py>, ptr: *mut ffi::PyObject) -> Self { - Self(NonNull::new_unchecked(ptr), PhantomData, py) + Self(unsafe { NonNull::new_unchecked(ptr) }, PhantomData, py) } #[inline] @@ -1675,7 +1681,7 @@ impl Py { /// /// - `ptr` must be a non-null pointer to a Python object or type `T`. pub(crate) unsafe fn from_owned_ptr_unchecked(ptr: *mut ffi::PyObject) -> Self { - Py(NonNull::new_unchecked(ptr), PhantomData) + Py(unsafe { NonNull::new_unchecked(ptr) }, PhantomData) } /// Create a `Py` instance by creating a new reference from the given FFI pointer. @@ -1688,7 +1694,7 @@ impl Py { #[inline] #[track_caller] pub unsafe fn from_borrowed_ptr(py: Python<'_>, ptr: *mut ffi::PyObject) -> Py { - match Self::from_borrowed_ptr_or_opt(py, ptr) { + match unsafe { Self::from_borrowed_ptr_or_opt(py, ptr) } { Some(slf) => slf, None => crate::err::panic_after_error(py), } @@ -1705,7 +1711,7 @@ impl Py { py: Python<'_>, ptr: *mut ffi::PyObject, ) -> PyResult { - Self::from_borrowed_ptr_or_opt(py, ptr).ok_or_else(|| PyErr::fetch(py)) + unsafe { Self::from_borrowed_ptr_or_opt(py, ptr).ok_or_else(|| PyErr::fetch(py)) } } /// Create a `Py` instance by creating a new reference from the given FFI pointer. @@ -1719,10 +1725,12 @@ impl Py { _py: Python<'_>, ptr: *mut ffi::PyObject, ) -> Option { - NonNull::new(ptr).map(|nonnull_ptr| { - ffi::Py_INCREF(ptr); - Py(nonnull_ptr, PhantomData) - }) + unsafe { + NonNull::new(ptr).map(|nonnull_ptr| { + ffi::Py_INCREF(ptr); + Py(nonnull_ptr, PhantomData) + }) + } } /// For internal conversions. @@ -1985,7 +1993,7 @@ impl PyObject { /// Callers must ensure that the type is valid or risk type confusion. #[inline] pub unsafe fn downcast_bound_unchecked<'py, T>(&self, py: Python<'py>) -> &Bound<'py, T> { - self.bind(py).downcast_unchecked() + unsafe { self.bind(py).downcast_unchecked() } } } diff --git a/src/internal/get_slot.rs b/src/internal/get_slot.rs index 260893d4204..611644cb6bc 100644 --- a/src/internal/get_slot.rs +++ b/src/internal/get_slot.rs @@ -51,12 +51,14 @@ pub(crate) unsafe fn get_slot( where Slot: GetSlotImpl, { - slot.get_slot( - ty, - // SAFETY: the Python runtime is initialized - #[cfg(all(Py_LIMITED_API, not(Py_3_10)))] - is_runtime_3_10(crate::Python::assume_gil_acquired()), - ) + unsafe { + slot.get_slot( + ty, + // SAFETY: the Python runtime is initialized + #[cfg(all(Py_LIMITED_API, not(Py_3_10)))] + is_runtime_3_10(crate::Python::assume_gil_acquired()), + ) + } } pub(crate) trait GetSlotImpl { @@ -93,7 +95,7 @@ macro_rules! impl_slots { ) -> Self::Type { #[cfg(not(Py_LIMITED_API))] { - (*ty).$field + unsafe {(*ty).$field } } #[cfg(Py_LIMITED_API)] @@ -105,14 +107,14 @@ macro_rules! impl_slots { // (3.7, 3.8, 3.9) and then look in the type object anyway. This is only ok // because we know that the interpreter is not going to change the size // of the type objects for these historical versions. - if !is_runtime_3_10 && ffi::PyType_HasFeature(ty, ffi::Py_TPFLAGS_HEAPTYPE) == 0 + if !is_runtime_3_10 && unsafe {ffi::PyType_HasFeature(ty, ffi::Py_TPFLAGS_HEAPTYPE)} == 0 { - return (*ty.cast::()).$field; + return unsafe {(*ty.cast::()).$field}; } } // SAFETY: slot type is set carefully to be valid - std::mem::transmute(ffi::PyType_GetSlot(ty, ffi::$slot)) + unsafe {std::mem::transmute(ffi::PyType_GetSlot(ty, ffi::$slot))} } } } diff --git a/src/lib.rs b/src/lib.rs index 2a0c289204c..863fd010ade 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,6 +4,8 @@ feature(auto_traits, negative_impls, try_trait_v2, iter_advance_by) )] #![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] +#![cfg_attr(not(cargo_toml_lints), warn(unsafe_op_in_unsafe_fn))] +// necessary for MSRV 1.63 to build // Deny some lints in doctests. // Use `#[allow(...)]` locally to override. #![doc(test(attr( diff --git a/src/marker.rs b/src/marker.rs index f98a725da0e..4fe91464244 100644 --- a/src/marker.rs +++ b/src/marker.rs @@ -439,7 +439,7 @@ impl Python<'_> { where F: for<'py> FnOnce(Python<'py>) -> R, { - let guard = GILGuard::acquire_unchecked(); + let guard = unsafe { GILGuard::acquire_unchecked() }; f(guard.python()) } diff --git a/src/py_result_ext.rs b/src/py_result_ext.rs index 2ad079ed7ac..bcb152e7ef0 100644 --- a/src/py_result_ext.rs +++ b/src/py_result_ext.rs @@ -13,6 +13,6 @@ impl<'py> PyResultExt<'py> for PyResult> { #[inline] unsafe fn downcast_into_unchecked(self) -> PyResult> { - self.map(|instance| instance.downcast_into_unchecked()) + self.map(|instance| unsafe { instance.downcast_into_unchecked() }) } } diff --git a/src/pycell/impl_.rs b/src/pycell/impl_.rs index 1b1344f8aa8..6fe0545d529 100644 --- a/src/pycell/impl_.rs +++ b/src/pycell/impl_.rs @@ -233,42 +233,44 @@ where Ok(()) } unsafe fn tp_dealloc(py: Python<'_>, slf: *mut ffi::PyObject) { - // FIXME: there is potentially subtle issues here if the base is overwritten - // at runtime? To be investigated. - let type_obj = T::type_object(py); - let type_ptr = type_obj.as_type_ptr(); - let actual_type = PyType::from_borrowed_type_ptr(py, ffi::Py_TYPE(slf)); - - // For `#[pyclass]` types which inherit from PyAny, we can just call tp_free - if type_ptr == std::ptr::addr_of_mut!(ffi::PyBaseObject_Type) { - let tp_free = actual_type - .get_slot(TP_FREE) - .expect("PyBaseObject_Type should have tp_free"); - return tp_free(slf.cast()); - } + unsafe { + // FIXME: there is potentially subtle issues here if the base is overwritten + // at runtime? To be investigated. + let type_obj = T::type_object(py); + let type_ptr = type_obj.as_type_ptr(); + let actual_type = PyType::from_borrowed_type_ptr(py, ffi::Py_TYPE(slf)); + + // For `#[pyclass]` types which inherit from PyAny, we can just call tp_free + if type_ptr == std::ptr::addr_of_mut!(ffi::PyBaseObject_Type) { + let tp_free = actual_type + .get_slot(TP_FREE) + .expect("PyBaseObject_Type should have tp_free"); + return tp_free(slf.cast()); + } - // More complex native types (e.g. `extends=PyDict`) require calling the base's dealloc. - #[cfg(not(Py_LIMITED_API))] - { - // FIXME: should this be using actual_type.tp_dealloc? - if let Some(dealloc) = (*type_ptr).tp_dealloc { - // Before CPython 3.11 BaseException_dealloc would use Py_GC_UNTRACK which - // assumes the exception is currently GC tracked, so we have to re-track - // before calling the dealloc so that it can safely call Py_GC_UNTRACK. - #[cfg(not(any(Py_3_11, PyPy)))] - if ffi::PyType_FastSubclass(type_ptr, ffi::Py_TPFLAGS_BASE_EXC_SUBCLASS) == 1 { - ffi::PyObject_GC_Track(slf.cast()); + // More complex native types (e.g. `extends=PyDict`) require calling the base's dealloc. + #[cfg(not(Py_LIMITED_API))] + { + // FIXME: should this be using actual_type.tp_dealloc? + if let Some(dealloc) = (*type_ptr).tp_dealloc { + // Before CPython 3.11 BaseException_dealloc would use Py_GC_UNTRACK which + // assumes the exception is currently GC tracked, so we have to re-track + // before calling the dealloc so that it can safely call Py_GC_UNTRACK. + #[cfg(not(any(Py_3_11, PyPy)))] + if ffi::PyType_FastSubclass(type_ptr, ffi::Py_TPFLAGS_BASE_EXC_SUBCLASS) == 1 { + ffi::PyObject_GC_Track(slf.cast()); + } + dealloc(slf); + } else { + (*actual_type.as_type_ptr()) + .tp_free + .expect("type missing tp_free")(slf.cast()); } - dealloc(slf); - } else { - (*actual_type.as_type_ptr()) - .tp_free - .expect("type missing tp_free")(slf.cast()); } - } - #[cfg(Py_LIMITED_API)] - unreachable!("subclassing native types is not possible with the `abi3` feature"); + #[cfg(Py_LIMITED_API)] + unreachable!("subclassing native types is not possible with the `abi3` feature"); + } } } @@ -343,13 +345,15 @@ where } unsafe fn tp_dealloc(py: Python<'_>, slf: *mut ffi::PyObject) { // Safety: Python only calls tp_dealloc when no references to the object remain. - let class_object = &mut *(slf.cast::>()); + let class_object = unsafe { &mut *(slf.cast::>()) }; if class_object.contents.thread_checker.can_drop(py) { - ManuallyDrop::drop(&mut class_object.contents.value); + unsafe { ManuallyDrop::drop(&mut class_object.contents.value) }; } class_object.contents.dict.clear_dict(py); - class_object.contents.weakref.clear_weakrefs(slf, py); - ::LayoutAsBase::tp_dealloc(py, slf) + unsafe { + class_object.contents.weakref.clear_weakrefs(slf, py); + ::LayoutAsBase::tp_dealloc(py, slf) + } } } diff --git a/src/pyclass/create_type_object.rs b/src/pyclass/create_type_object.rs index 8a02baa8ad1..bbc3e465772 100644 --- a/src/pyclass/create_type_object.rs +++ b/src/pyclass/create_type_object.rs @@ -49,33 +49,35 @@ where module: Option<&'static str>, size_of: usize, ) -> PyResult { - PyTypeBuilder { - slots: Vec::new(), - method_defs: Vec::new(), - member_defs: Vec::new(), - getset_builders: HashMap::new(), - cleanup: Vec::new(), - 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, - has_setitem: false, - has_traverse: false, - has_clear: false, - dict_offset: None, - class_flags: 0, - #[cfg(all(not(Py_3_9), not(Py_LIMITED_API)))] - buffer_procs: Default::default(), + unsafe { + PyTypeBuilder { + slots: Vec::new(), + method_defs: Vec::new(), + member_defs: Vec::new(), + getset_builders: HashMap::new(), + cleanup: Vec::new(), + 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, + has_setitem: false, + has_traverse: false, + has_clear: false, + dict_offset: None, + class_flags: 0, + #[cfg(all(not(Py_3_9), not(Py_LIMITED_API)))] + buffer_procs: Default::default(), + } + .type_doc(doc) + .offsets(dict_offset, weaklist_offset) + .set_is_basetype(is_basetype) + .class_items(items_iter) + .build(py, name, module, 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 { @@ -145,13 +147,13 @@ impl PyTypeBuilder { ffi::Py_bf_getbuffer => { // Safety: slot.pfunc is a valid function pointer self.buffer_procs.bf_getbuffer = - Some(std::mem::transmute::<*mut T, ffi::getbufferproc>(pfunc)); + Some(unsafe { std::mem::transmute::<*mut T, ffi::getbufferproc>(pfunc) }); } #[cfg(all(not(Py_3_9), not(Py_LIMITED_API)))] ffi::Py_bf_releasebuffer => { // Safety: slot.pfunc is a valid function pointer self.buffer_procs.bf_releasebuffer = - Some(std::mem::transmute::<*mut T, ffi::releasebufferproc>(pfunc)); + Some(unsafe { std::mem::transmute::<*mut T, ffi::releasebufferproc>(pfunc) }); } _ => {} } @@ -167,8 +169,10 @@ impl PyTypeBuilder { unsafe fn push_raw_vec_slot(&mut self, slot: c_int, mut data: Vec) { if !data.is_empty() { // Python expects a zeroed entry to mark the end of the defs - data.push(std::mem::zeroed()); - self.push_slot(slot, Box::into_raw(data.into_boxed_slice()) as *mut c_void); + unsafe { + data.push(std::mem::zeroed()); + self.push_slot(slot, Box::into_raw(data.into_boxed_slice()) as *mut c_void); + } } } @@ -314,7 +318,7 @@ impl PyTypeBuilder { unsafe fn class_items(mut self, iter: PyClassItemsIter) -> Self { for items in iter { for slot in items.slots { - self.push_slot(slot.slot, slot.pfunc); + unsafe { self.push_slot(slot.slot, slot.pfunc) }; } for method in items.methods { let built_method; @@ -545,20 +549,22 @@ unsafe extern "C" fn no_constructor_defined( _args: *mut ffi::PyObject, _kwds: *mut ffi::PyObject, ) -> *mut ffi::PyObject { - trampoline(|py| { - let tpobj = PyType::from_borrowed_type_ptr(py, subtype); - let name = tpobj - .name() - .map_or_else(|_| "".into(), |name| name.to_string()); - Err(crate::exceptions::PyTypeError::new_err(format!( - "No constructor defined for {}", - name - ))) - }) + unsafe { + trampoline(|py| { + let tpobj = PyType::from_borrowed_type_ptr(py, subtype); + let name = tpobj + .name() + .map_or_else(|_| "".into(), |name| name.to_string()); + Err(crate::exceptions::PyTypeError::new_err(format!( + "No constructor defined for {}", + name + ))) + }) + } } unsafe extern "C" fn call_super_clear(slf: *mut ffi::PyObject) -> c_int { - _call_clear(slf, |_, _| Ok(()), call_super_clear) + unsafe { _call_clear(slf, |_, _| Ok(()), call_super_clear) } } #[derive(Default)] @@ -643,8 +649,8 @@ impl GetSetDefType { closure: *mut c_void, ) -> *mut ffi::PyObject { // Safety: PyO3 sets the closure when constructing the ffi getter so this cast should always be valid - let getter: Getter = std::mem::transmute(closure); - trampoline(|py| getter(py, slf)) + let getter: Getter = unsafe { std::mem::transmute(closure) }; + unsafe { trampoline(|py| getter(py, slf)) } } (Some(getter), None, closure as Getter as _) } @@ -655,8 +661,8 @@ impl GetSetDefType { closure: *mut c_void, ) -> c_int { // Safety: PyO3 sets the closure when constructing the ffi setter so this cast should always be valid - let setter: Setter = std::mem::transmute(closure); - trampoline(|py| setter(py, slf, value)) + let setter: Setter = unsafe { std::mem::transmute(closure) }; + unsafe { trampoline(|py| setter(py, slf, value)) } } (None, Some(setter), closure as Setter as _) } @@ -665,8 +671,8 @@ impl GetSetDefType { slf: *mut ffi::PyObject, closure: *mut c_void, ) -> *mut ffi::PyObject { - let getset: &GetterAndSetter = &*closure.cast(); - trampoline(|py| (getset.getter)(py, slf)) + let getset: &GetterAndSetter = unsafe { &*closure.cast() }; + unsafe { trampoline(|py| (getset.getter)(py, slf)) } } unsafe extern "C" fn getset_setter( @@ -674,8 +680,8 @@ impl GetSetDefType { value: *mut ffi::PyObject, closure: *mut c_void, ) -> c_int { - let getset: &GetterAndSetter = &*closure.cast(); - trampoline(|py| (getset.setter)(py, slf, value)) + let getset: &GetterAndSetter = unsafe { &*closure.cast() }; + unsafe { trampoline(|py| (getset.setter)(py, slf, value)) } } ( Some(getset_getter), diff --git a/src/pyclass_init.rs b/src/pyclass_init.rs index 6dc6ec12c6b..3a3253fffbc 100644 --- a/src/pyclass_init.rs +++ b/src/pyclass_init.rs @@ -178,23 +178,25 @@ impl PyClassInitializer { PyClassInitializerImpl::New { init, super_init } => (init, super_init), }; - let obj = super_init.into_new_object(py, target_type)?; + let obj = unsafe { super_init.into_new_object(py, target_type)? }; let part_init: *mut PartiallyInitializedClassObject = obj.cast(); - std::ptr::write( - (*part_init).contents.as_mut_ptr(), - PyClassObjectContents { - value: ManuallyDrop::new(UnsafeCell::new(init)), - borrow_checker: ::Storage::new(), - thread_checker: T::ThreadChecker::new(), - dict: T::Dict::INIT, - weakref: T::WeakRef::INIT, - }, - ); + unsafe { + std::ptr::write( + (*part_init).contents.as_mut_ptr(), + PyClassObjectContents { + value: ManuallyDrop::new(UnsafeCell::new(init)), + borrow_checker: ::Storage::new(), + thread_checker: T::ThreadChecker::new(), + dict: T::Dict::INIT, + weakref: T::WeakRef::INIT, + }, + ); + } // Safety: obj is a valid pointer to an object of type `target_type`, which` is a known // subclass of `T` - Ok(obj.assume_owned(py).downcast_into_unchecked()) + Ok(unsafe { obj.assume_owned(py).downcast_into_unchecked() }) } } @@ -204,8 +206,10 @@ impl PyObjectInit for PyClassInitializer { py: Python<'_>, subtype: *mut PyTypeObject, ) -> PyResult<*mut ffi::PyObject> { - self.create_class_object_of_type(py, subtype) - .map(Bound::into_ptr) + unsafe { + self.create_class_object_of_type(py, subtype) + .map(Bound::into_ptr) + } } #[inline] diff --git a/src/types/any.rs b/src/types/any.rs index b6ee6fdd532..7e86401ed9b 100644 --- a/src/types/any.rs +++ b/src/types/any.rs @@ -1510,12 +1510,12 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { #[inline] unsafe fn downcast_unchecked(&self) -> &Bound<'py, T> { - &*ptr_from_ref(self).cast() + unsafe { &*ptr_from_ref(self).cast() } } #[inline] unsafe fn downcast_into_unchecked(self) -> Bound<'py, T> { - std::mem::transmute(self) + unsafe { std::mem::transmute(self) } } fn extract<'a, T>(&'a self) -> PyResult diff --git a/src/types/bytearray.rs b/src/types/bytearray.rs index d1bbd0ac7e4..094cdf3144b 100644 --- a/src/types/bytearray.rs +++ b/src/types/bytearray.rs @@ -286,12 +286,12 @@ impl<'py> PyByteArrayMethods<'py> for Bound<'py, PyByteArray> { } unsafe fn as_bytes(&self) -> &[u8] { - self.as_borrowed().as_bytes() + unsafe { self.as_borrowed().as_bytes() } } #[allow(clippy::mut_from_ref)] unsafe fn as_bytes_mut(&self) -> &mut [u8] { - self.as_borrowed().as_bytes_mut() + unsafe { self.as_borrowed().as_bytes_mut() } } fn to_vec(&self) -> Vec { @@ -317,12 +317,12 @@ impl<'a> Borrowed<'a, '_, PyByteArray> { #[allow(clippy::wrong_self_convention)] unsafe fn as_bytes(self) -> &'a [u8] { - slice::from_raw_parts(self.data(), self.len()) + unsafe { slice::from_raw_parts(self.data(), self.len()) } } #[allow(clippy::wrong_self_convention)] unsafe fn as_bytes_mut(self) -> &'a mut [u8] { - slice::from_raw_parts_mut(self.data(), self.len()) + unsafe { slice::from_raw_parts_mut(self.data(), self.len()) } } } diff --git a/src/types/bytes.rs b/src/types/bytes.rs index 77b1d2b735d..a820ece3b45 100644 --- a/src/types/bytes.rs +++ b/src/types/bytes.rs @@ -135,9 +135,11 @@ impl PyBytes { /// `std::slice::from_raw_parts`, this is /// unsafe](https://doc.rust-lang.org/std/slice/fn.from_raw_parts.html#safety). pub unsafe fn from_ptr(py: Python<'_>, ptr: *const u8, len: usize) -> Bound<'_, PyBytes> { - ffi::PyBytes_FromStringAndSize(ptr.cast(), len as isize) - .assume_owned(py) - .downcast_into_unchecked() + unsafe { + ffi::PyBytes_FromStringAndSize(ptr.cast(), len as isize) + .assume_owned(py) + .downcast_into_unchecked() + } } /// Deprecated name for [`PyBytes::from_ptr`]. @@ -151,7 +153,7 @@ impl PyBytes { #[deprecated(since = "0.23.0", note = "renamed to `PyBytes::from_ptr`")] #[inline] pub unsafe fn bound_from_ptr(py: Python<'_>, ptr: *const u8, len: usize) -> Bound<'_, PyBytes> { - Self::from_ptr(py, ptr, len) + unsafe { Self::from_ptr(py, ptr, len) } } } diff --git a/src/types/capsule.rs b/src/types/capsule.rs index 9d9e6e4eb72..5f97aeda5c5 100644 --- a/src/types/capsule.rs +++ b/src/types/capsule.rs @@ -163,11 +163,11 @@ impl PyCapsule { /// /// It must be known that the capsule imported by `name` contains an item of type `T`. pub unsafe fn import<'py, T>(py: Python<'py>, name: &CStr) -> PyResult<&'py T> { - let ptr = ffi::PyCapsule_Import(name.as_ptr(), false as c_int); + let ptr = unsafe { ffi::PyCapsule_Import(name.as_ptr(), false as c_int) }; if ptr.is_null() { Err(PyErr::fetch(py)) } else { - Ok(&*ptr.cast::()) + Ok(unsafe { &*ptr.cast::() }) } } } @@ -267,7 +267,7 @@ impl<'py> PyCapsuleMethods<'py> for Bound<'py, PyCapsule> { } unsafe fn reference(&self) -> &'py T { - &*self.pointer().cast() + unsafe { &*self.pointer().cast() } } fn pointer(&self) -> *mut c_void { @@ -316,12 +316,14 @@ struct CapsuleContents { unsafe extern "C" fn capsule_destructor( capsule: *mut ffi::PyObject, ) { - let ptr = ffi::PyCapsule_GetPointer(capsule, ffi::PyCapsule_GetName(capsule)); - let ctx = ffi::PyCapsule_GetContext(capsule); - let CapsuleContents { - value, destructor, .. - } = *Box::from_raw(ptr.cast::>()); - destructor(value, ctx) + unsafe { + let ptr = ffi::PyCapsule_GetPointer(capsule, ffi::PyCapsule_GetName(capsule)); + let ctx = ffi::PyCapsule_GetContext(capsule); + let CapsuleContents { + value, destructor, .. + } = *Box::from_raw(ptr.cast::>()); + destructor(value, ctx) + } } /// Guarantee `T` is not zero sized at compile time. diff --git a/src/types/datetime.rs b/src/types/datetime.rs index e091ace2591..0b0a7324c4b 100644 --- a/src/types/datetime.rs +++ b/src/types/datetime.rs @@ -68,8 +68,8 @@ macro_rules! ffi_fun_with_autoinit { /// Must only be called while the GIL is held unsafe fn $name($arg: *mut crate::ffi::PyObject) -> $ret { - let _ = ensure_datetime_api(Python::assume_gil_acquired()); - crate::ffi::$name($arg) + let _ = ensure_datetime_api(unsafe { Python::assume_gil_acquired() }); + unsafe { crate::ffi::$name($arg) } } )* diff --git a/src/types/function.rs b/src/types/function.rs index 039e2774546..2f4da950b0a 100644 --- a/src/types/function.rs +++ b/src/types/function.rs @@ -189,22 +189,24 @@ where { use crate::types::any::PyAnyMethods; - crate::impl_::trampoline::cfunction_with_keywords( - capsule_ptr, - args, - kwargs, - |py, capsule_ptr, args, kwargs| { - let boxed_fn: &ClosureDestructor = - &*(ffi::PyCapsule_GetPointer(capsule_ptr, CLOSURE_CAPSULE_NAME.as_ptr()) - as *mut ClosureDestructor); - let args = Bound::ref_from_ptr(py, &args).downcast_unchecked::(); - let kwargs = Bound::ref_from_ptr_or_opt(py, &kwargs) - .as_ref() - .map(|b| b.downcast_unchecked::()); - let result = (boxed_fn.closure)(args, kwargs); - crate::impl_::callback::convert(py, result) - }, - ) + unsafe { + crate::impl_::trampoline::cfunction_with_keywords( + capsule_ptr, + args, + kwargs, + |py, capsule_ptr, args, kwargs| { + let boxed_fn: &ClosureDestructor = + &*(ffi::PyCapsule_GetPointer(capsule_ptr, CLOSURE_CAPSULE_NAME.as_ptr()) + as *mut ClosureDestructor); + let args = Bound::ref_from_ptr(py, &args).downcast_unchecked::(); + let kwargs = Bound::ref_from_ptr_or_opt(py, &kwargs) + .as_ref() + .map(|b| b.downcast_unchecked::()); + let result = (boxed_fn.closure)(args, kwargs); + crate::impl_::callback::convert(py, result) + }, + ) + } } struct ClosureDestructor { diff --git a/src/types/list.rs b/src/types/list.rs index ead22315f05..bd770aaeca7 100644 --- a/src/types/list.rs +++ b/src/types/list.rs @@ -318,9 +318,11 @@ impl<'py> PyListMethods<'py> for Bound<'py, PyList> { #[cfg(not(Py_LIMITED_API))] unsafe fn get_item_unchecked(&self, index: usize) -> Bound<'py, PyAny> { // PyList_GET_ITEM return borrowed ptr; must make owned for safety (see #890). - ffi::PyList_GET_ITEM(self.as_ptr(), index as Py_ssize_t) - .assume_borrowed(self.py()) - .to_owned() + unsafe { + ffi::PyList_GET_ITEM(self.as_ptr(), index as Py_ssize_t) + .assume_borrowed(self.py()) + .to_owned() + } } /// Takes the slice `self[low:high]` and returns it as a new list. diff --git a/src/types/mappingproxy.rs b/src/types/mappingproxy.rs index 5a0b1537cb0..436a52dc3e9 100644 --- a/src/types/mappingproxy.rs +++ b/src/types/mappingproxy.rs @@ -16,7 +16,7 @@ pub struct PyMappingProxy(PyAny); #[inline] unsafe fn dict_proxy_check(op: *mut ffi::PyObject) -> c_int { - ffi::Py_IS_TYPE(op, std::ptr::addr_of_mut!(ffi::PyDictProxy_Type)) + unsafe { ffi::Py_IS_TYPE(op, std::ptr::addr_of_mut!(ffi::PyDictProxy_Type)) } } pyobject_native_type_core!( diff --git a/src/types/string.rs b/src/types/string.rs index 65a9e85fa3e..0b0de39c681 100644 --- a/src/types/string.rs +++ b/src/types/string.rs @@ -310,7 +310,7 @@ impl<'py> PyStringMethods<'py> for Bound<'py, PyString> { #[cfg(not(any(Py_LIMITED_API, GraalPy, PyPy)))] unsafe fn data(&self) -> PyResult> { - self.as_borrowed().data() + unsafe { self.as_borrowed().data() } } } @@ -373,39 +373,41 @@ impl<'a> Borrowed<'a, '_, PyString> { #[cfg(not(any(Py_LIMITED_API, GraalPy, PyPy)))] unsafe fn data(self) -> PyResult> { - let ptr = self.as_ptr(); - - #[cfg(not(Py_3_12))] - #[allow(deprecated)] - { - let ready = ffi::PyUnicode_READY(ptr); - if ready != 0 { - // Exception was created on failure. - return Err(crate::PyErr::fetch(self.py())); + unsafe { + let ptr = self.as_ptr(); + + #[cfg(not(Py_3_12))] + #[allow(deprecated)] + { + let ready = ffi::PyUnicode_READY(ptr); + if ready != 0 { + // Exception was created on failure. + return Err(crate::PyErr::fetch(self.py())); + } } - } - // The string should be in its canonical form after calling `PyUnicode_READY()`. - // And non-canonical form not possible after Python 3.12. So it should be safe - // to call these APIs. - let length = ffi::PyUnicode_GET_LENGTH(ptr) as usize; - let raw_data = ffi::PyUnicode_DATA(ptr); - let kind = ffi::PyUnicode_KIND(ptr); - - match kind { - ffi::PyUnicode_1BYTE_KIND => Ok(PyStringData::Ucs1(std::slice::from_raw_parts( - raw_data as *const u8, - length, - ))), - ffi::PyUnicode_2BYTE_KIND => Ok(PyStringData::Ucs2(std::slice::from_raw_parts( - raw_data as *const u16, - length, - ))), - ffi::PyUnicode_4BYTE_KIND => Ok(PyStringData::Ucs4(std::slice::from_raw_parts( - raw_data as *const u32, - length, - ))), - _ => unreachable!(), + // The string should be in its canonical form after calling `PyUnicode_READY()`. + // And non-canonical form not possible after Python 3.12. So it should be safe + // to call these APIs. + let length = ffi::PyUnicode_GET_LENGTH(ptr) as usize; + let raw_data = ffi::PyUnicode_DATA(ptr); + let kind = ffi::PyUnicode_KIND(ptr); + + match kind { + ffi::PyUnicode_1BYTE_KIND => Ok(PyStringData::Ucs1(std::slice::from_raw_parts( + raw_data as *const u8, + length, + ))), + ffi::PyUnicode_2BYTE_KIND => Ok(PyStringData::Ucs2(std::slice::from_raw_parts( + raw_data as *const u16, + length, + ))), + ffi::PyUnicode_4BYTE_KIND => Ok(PyStringData::Ucs4(std::slice::from_raw_parts( + raw_data as *const u32, + length, + ))), + _ => unreachable!(), + } } } } diff --git a/src/types/tuple.rs b/src/types/tuple.rs index 81a7ad911e9..a35f5805c15 100644 --- a/src/types/tuple.rs +++ b/src/types/tuple.rs @@ -272,12 +272,12 @@ impl<'py> PyTupleMethods<'py> for Bound<'py, PyTuple> { #[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))] unsafe fn get_item_unchecked(&self, index: usize) -> Bound<'py, PyAny> { - self.get_borrowed_item_unchecked(index).to_owned() + unsafe { self.get_borrowed_item_unchecked(index).to_owned() } } #[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))] unsafe fn get_borrowed_item_unchecked<'a>(&'a self, index: usize) -> Borrowed<'a, 'py, PyAny> { - self.as_borrowed().get_borrowed_item_unchecked(index) + unsafe { self.as_borrowed().get_borrowed_item_unchecked(index) } } #[cfg(not(any(Py_LIMITED_API, GraalPy)))] @@ -329,7 +329,9 @@ impl<'a, 'py> Borrowed<'a, 'py, PyTuple> { #[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))] unsafe fn get_borrowed_item_unchecked(self, index: usize) -> Borrowed<'a, 'py, PyAny> { - ffi::PyTuple_GET_ITEM(self.as_ptr(), index as Py_ssize_t).assume_borrowed(self.py()) + unsafe { + ffi::PyTuple_GET_ITEM(self.as_ptr(), index as Py_ssize_t).assume_borrowed(self.py()) + } } pub(crate) fn iter_borrowed(self) -> BorrowedTupleIterator<'a, 'py> { @@ -540,7 +542,7 @@ impl<'a, 'py> BorrowedTupleIterator<'a, 'py> { #[cfg(any(Py_LIMITED_API, PyPy, GraalPy))] let item = tuple.get_borrowed_item(index).expect("tuple.get failed"); #[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))] - let item = tuple.get_borrowed_item_unchecked(index); + let item = unsafe { tuple.get_borrowed_item_unchecked(index) }; item } } diff --git a/src/types/typeobject.rs b/src/types/typeobject.rs index 7a66b7ad0df..7ab3690b90b 100644 --- a/src/types/typeobject.rs +++ b/src/types/typeobject.rs @@ -46,9 +46,11 @@ impl PyType { py: Python<'_>, p: *mut ffi::PyTypeObject, ) -> Bound<'_, PyType> { - Borrowed::from_ptr_unchecked(py, p.cast()) - .downcast_unchecked() - .to_owned() + unsafe { + Borrowed::from_ptr_unchecked(py, p.cast()) + .downcast_unchecked() + .to_owned() + } } } diff --git a/src/types/weakref/anyref.rs b/src/types/weakref/anyref.rs index d5af956ac9e..741a638c295 100644 --- a/src/types/weakref/anyref.rs +++ b/src/types/weakref/anyref.rs @@ -181,7 +181,7 @@ pub trait PyWeakrefMethods<'py>: crate::sealed::Sealed { /// [`weakref.ReferenceType`]: https://docs.python.org/3/library/weakref.html#weakref.ReferenceType /// [`weakref.ref`]: https://docs.python.org/3/library/weakref.html#weakref.ref unsafe fn upgrade_as_unchecked(&self) -> Option> { - Some(self.upgrade()?.downcast_into_unchecked()) + Some(unsafe { self.upgrade()?.downcast_into_unchecked() }) } /// Upgrade the weakref to a exact direct Bound object reference. diff --git a/tests/test_buffer.rs b/tests/test_buffer.rs index 60db80b81c8..8591b6a0e1f 100644 --- a/tests/test_buffer.rs +++ b/tests/test_buffer.rs @@ -1,5 +1,6 @@ #![cfg(feature = "macros")] #![cfg(any(not(Py_LIMITED_API), Py_3_11))] +#![cfg_attr(not(cargo_toml_lints), warn(unsafe_op_in_unsafe_fn))] use pyo3::{buffer::PyBuffer, exceptions::PyBufferError, ffi, prelude::*}; use std::{ @@ -42,42 +43,44 @@ impl TestBufferErrors { let bytes = &slf.buf; - (*view).buf = bytes.as_ptr() as *mut c_void; - (*view).len = bytes.len() as isize; - (*view).readonly = 1; - (*view).itemsize = std::mem::size_of::() as isize; - - let msg = ffi::c_str!("I"); - (*view).format = msg.as_ptr() as *mut _; - - (*view).ndim = 1; - (*view).shape = &mut (*view).len; - - (*view).strides = &mut (*view).itemsize; - - (*view).suboffsets = ptr::null_mut(); - (*view).internal = ptr::null_mut(); - - if let Some(err) = &slf.error { - use TestGetBufferError::*; - match err { - NullShape => { - (*view).shape = std::ptr::null_mut(); + unsafe { + (*view).buf = bytes.as_ptr() as *mut c_void; + (*view).len = bytes.len() as isize; + (*view).readonly = 1; + (*view).itemsize = std::mem::size_of::() as isize; + + let msg = ffi::c_str!("I"); + (*view).format = msg.as_ptr() as *mut _; + + (*view).ndim = 1; + (*view).shape = &mut (*view).len; + + (*view).strides = &mut (*view).itemsize; + + (*view).suboffsets = ptr::null_mut(); + (*view).internal = ptr::null_mut(); + + if let Some(err) = &slf.error { + use TestGetBufferError::*; + match err { + NullShape => { + (*view).shape = std::ptr::null_mut(); + } + NullStrides => { + (*view).strides = std::ptr::null_mut(); + } + IncorrectItemSize => { + (*view).itemsize += 1; + } + IncorrectFormat => { + (*view).format = ffi::c_str!("B").as_ptr() as _; + } + IncorrectAlignment => (*view).buf = (*view).buf.add(1), } - NullStrides => { - (*view).strides = std::ptr::null_mut(); - } - IncorrectItemSize => { - (*view).itemsize += 1; - } - IncorrectFormat => { - (*view).format = ffi::c_str!("B").as_ptr() as _; - } - IncorrectAlignment => (*view).buf = (*view).buf.add(1), } - } - (*view).obj = slf.into_ptr(); + (*view).obj = slf.into_ptr(); + } Ok(()) } diff --git a/tests/test_buffer_protocol.rs b/tests/test_buffer_protocol.rs index 1f15e34b384..f3c316d50c7 100644 --- a/tests/test_buffer_protocol.rs +++ b/tests/test_buffer_protocol.rs @@ -1,5 +1,6 @@ #![cfg(feature = "macros")] #![cfg(any(not(Py_LIMITED_API), Py_3_11))] +#![cfg_attr(not(cargo_toml_lints), warn(unsafe_op_in_unsafe_fn))] use pyo3::buffer::PyBuffer; use pyo3::exceptions::PyBufferError; @@ -28,12 +29,12 @@ impl TestBufferClass { view: *mut ffi::Py_buffer, flags: c_int, ) -> PyResult<()> { - fill_view_from_readonly_data(view, flags, &slf.borrow().vec, slf.into_any()) + unsafe { fill_view_from_readonly_data(view, flags, &slf.borrow().vec, slf.into_any()) } } unsafe fn __releasebuffer__(&self, view: *mut ffi::Py_buffer) { // Release memory held by the format string - drop(CString::from_raw((*view).format)); + drop(unsafe { CString::from_raw((*view).format) }); } } @@ -111,7 +112,7 @@ fn test_releasebuffer_unraisable_error() { flags: c_int, ) -> PyResult<()> { static BUF_BYTES: &[u8] = b"hello world"; - fill_view_from_readonly_data(view, flags, BUF_BYTES, slf.into_any()) + unsafe { fill_view_from_readonly_data(view, flags, BUF_BYTES, slf.into_any()) } } unsafe fn __releasebuffer__(&self, _view: *mut ffi::Py_buffer) -> PyResult<()> { @@ -156,35 +157,36 @@ unsafe fn fill_view_from_readonly_data( return Err(PyBufferError::new_err("Object is not writable")); } - (*view).obj = owner.into_ptr(); - - (*view).buf = data.as_ptr() as *mut c_void; - (*view).len = data.len() as isize; - (*view).readonly = 1; - (*view).itemsize = 1; - - (*view).format = if (flags & ffi::PyBUF_FORMAT) == ffi::PyBUF_FORMAT { - let msg = CString::new("B").unwrap(); - msg.into_raw() - } else { - ptr::null_mut() - }; - - (*view).ndim = 1; - (*view).shape = if (flags & ffi::PyBUF_ND) == ffi::PyBUF_ND { - &mut (*view).len - } else { - ptr::null_mut() - }; - - (*view).strides = if (flags & ffi::PyBUF_STRIDES) == ffi::PyBUF_STRIDES { - &mut (*view).itemsize - } else { - ptr::null_mut() - }; - - (*view).suboffsets = ptr::null_mut(); - (*view).internal = ptr::null_mut(); - + unsafe { + (*view).obj = owner.into_ptr(); + + (*view).buf = data.as_ptr() as *mut c_void; + (*view).len = data.len() as isize; + (*view).readonly = 1; + (*view).itemsize = 1; + + (*view).format = if (flags & ffi::PyBUF_FORMAT) == ffi::PyBUF_FORMAT { + let msg = CString::new("B").unwrap(); + msg.into_raw() + } else { + ptr::null_mut() + }; + + (*view).ndim = 1; + (*view).shape = if (flags & ffi::PyBUF_ND) == ffi::PyBUF_ND { + &mut (*view).len + } else { + ptr::null_mut() + }; + + (*view).strides = if (flags & ffi::PyBUF_STRIDES) == ffi::PyBUF_STRIDES { + &mut (*view).itemsize + } else { + ptr::null_mut() + }; + + (*view).suboffsets = ptr::null_mut(); + (*view).internal = ptr::null_mut(); + } Ok(()) } diff --git a/tests/test_gc.rs b/tests/test_gc.rs index a88b9a21f91..8bc1e53cadb 100644 --- a/tests/test_gc.rs +++ b/tests/test_gc.rs @@ -1,4 +1,5 @@ #![cfg(feature = "macros")] +#![cfg_attr(not(cargo_toml_lints), warn(unsafe_op_in_unsafe_fn))] use pyo3::class::PyTraverseError; use pyo3::class::PyVisit; @@ -720,7 +721,7 @@ fn test_traverse_subclass_override_clear() { // Manual traversal utilities unsafe fn get_type_traverse(tp: *mut pyo3::ffi::PyTypeObject) -> Option { - std::mem::transmute(pyo3::ffi::PyType_GetSlot(tp, pyo3::ffi::Py_tp_traverse)) + unsafe { std::mem::transmute(pyo3::ffi::PyType_GetSlot(tp, pyo3::ffi::Py_tp_traverse)) } } // a dummy visitor function diff --git a/tests/test_pyfunction.rs b/tests/test_pyfunction.rs index 4e3bdce9e05..822e0200a89 100644 --- a/tests/test_pyfunction.rs +++ b/tests/test_pyfunction.rs @@ -1,4 +1,5 @@ #![cfg(feature = "macros")] +#![cfg_attr(not(cargo_toml_lints), warn(unsafe_op_in_unsafe_fn))] use std::collections::HashMap; @@ -339,7 +340,7 @@ fn test_pycfunction_new() { _self: *mut ffi::PyObject, _args: *mut ffi::PyObject, ) -> *mut ffi::PyObject { - ffi::PyLong_FromLong(4200) + unsafe { ffi::PyLong_FromLong(4200) } } let py_fn = PyCFunction::new( @@ -389,24 +390,26 @@ fn test_pycfunction_new_with_keywords() { ptr::null_mut(), ]; - ffi::PyArg_ParseTupleAndKeywords( - args, - kwds, - c_str!("l|l").as_ptr(), - #[cfg(Py_3_13)] - args_names.as_ptr(), - #[cfg(not(Py_3_13))] - args_names.as_mut_ptr(), - &mut foo, - &mut bar, - ); + unsafe { + ffi::PyArg_ParseTupleAndKeywords( + args, + kwds, + c_str!("l|l").as_ptr(), + #[cfg(Py_3_13)] + args_names.as_ptr(), + #[cfg(not(Py_3_13))] + args_names.as_mut_ptr(), + &mut foo, + &mut bar, + ) + }; #[cfg(not(Py_3_13))] - drop(std::ffi::CString::from_raw(args_names[0])); + drop(unsafe { std::ffi::CString::from_raw(args_names[0]) }); #[cfg(not(Py_3_13))] - drop(std::ffi::CString::from_raw(args_names[1])); + drop(unsafe { std::ffi::CString::from_raw(args_names[1]) }); - ffi::PyLong_FromLong(foo * bar) + unsafe { ffi::PyLong_FromLong(foo * bar) } } let py_fn = PyCFunction::new_with_keywords( From ccebbdeb236926a71d9c70586ed27e8f26397ee0 Mon Sep 17 00:00:00 2001 From: Xuehai Pan Date: Fri, 21 Mar 2025 22:53:59 +0800 Subject: [PATCH 484/495] Add `pyo3::sync::with_critical_section2` binding (#4992) * Add `pyo3::sync::with_critical_section2` binding * Add test for `pyo3::sync::with_critical_section2` binding * Add more tests for `pyo3::sync::with_critical_section2` --- newsfragments/4992.added.md | 1 + src/sync.rs | 190 ++++++++++++++++++++++++++++++++++++ 2 files changed, 191 insertions(+) create mode 100644 newsfragments/4992.added.md diff --git a/newsfragments/4992.added.md b/newsfragments/4992.added.md new file mode 100644 index 00000000000..36cb9a4e08e --- /dev/null +++ b/newsfragments/4992.added.md @@ -0,0 +1 @@ +Add `pyo3::sync::with_critical_section2` binding diff --git a/src/sync.rs b/src/sync.rs index 5dae1744584..193cc79b105 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -480,6 +480,53 @@ where } } +/// Executes a closure with a Python critical section held on two objects. +/// +/// Acquires the per-object lock for the objects `a` and `b` that are held +/// until the closure `f` is finished. +/// +/// This is structurally equivalent to the use of the paired +/// Py_BEGIN_CRITICAL_SECTION2 and Py_END_CRITICAL_SECTION2 C-API macros. +/// +/// A no-op on GIL-enabled builds, where the critical section API is exposed as +/// a no-op by the Python C API. +/// +/// Provides weaker locking guarantees than traditional locks, but can in some +/// cases be used to provide guarantees similar to the GIL without the risk of +/// deadlocks associated with traditional locks. +/// +/// Many CPython C API functions do not acquire the per-object lock on objects +/// passed to Python. You should not expect critical sections applied to +/// built-in types to prevent concurrent modification. This API is most useful +/// for user-defined types with full control over how the internal state for the +/// type is managed. +#[cfg_attr(not(Py_GIL_DISABLED), allow(unused_variables))] +pub fn with_critical_section2(a: &Bound<'_, PyAny>, b: &Bound<'_, PyAny>, f: F) -> R +where + F: FnOnce() -> R, +{ + #[cfg(Py_GIL_DISABLED)] + { + struct Guard(crate::ffi::PyCriticalSection2); + + impl Drop for Guard { + fn drop(&mut self) { + unsafe { + crate::ffi::PyCriticalSection2_End(&mut self.0); + } + } + } + + let mut guard = Guard(unsafe { std::mem::zeroed() }); + unsafe { crate::ffi::PyCriticalSection2_Begin(&mut guard.0, a.as_ptr(), b.as_ptr()) }; + f() + } + #[cfg(not(Py_GIL_DISABLED))] + { + f() + } +} + #[cfg(rustc_has_once_lock)] mod once_lock_ext_sealed { pub trait Sealed {} @@ -670,6 +717,11 @@ mod tests { #[crate::pyclass(crate = "crate")] struct BoolWrapper(AtomicBool); + #[cfg(not(target_arch = "wasm32"))] + #[cfg(feature = "macros")] + #[crate::pyclass(crate = "crate")] + struct VecWrapper(Vec); + #[test] fn test_intern() { Python::with_gil(|py| { @@ -773,6 +825,144 @@ mod tests { }); } + #[cfg(feature = "macros")] + #[cfg(not(target_arch = "wasm32"))] // We are building wasm Python with pthreads disabled + #[test] + fn test_critical_section2() { + let barrier = Barrier::new(3); + + let (bool_wrapper1, bool_wrapper2) = Python::with_gil(|py| { + ( + Py::new(py, BoolWrapper(AtomicBool::new(false))).unwrap(), + Py::new(py, BoolWrapper(AtomicBool::new(false))).unwrap(), + ) + }); + + std::thread::scope(|s| { + s.spawn(|| { + Python::with_gil(|py| { + let b1 = bool_wrapper1.bind(py); + let b2 = bool_wrapper2.bind(py); + with_critical_section2(b1, b2, || { + barrier.wait(); + std::thread::sleep(std::time::Duration::from_millis(10)); + b1.borrow().0.store(true, Ordering::Release); + b2.borrow().0.store(true, Ordering::Release); + }) + }); + }); + s.spawn(|| { + barrier.wait(); + Python::with_gil(|py| { + let b1 = bool_wrapper1.bind(py); + // this blocks until the other thread's critical section finishes + with_critical_section(b1, || { + assert!(b1.borrow().0.load(Ordering::Acquire)); + }); + }); + }); + s.spawn(|| { + barrier.wait(); + Python::with_gil(|py| { + let b2 = bool_wrapper2.bind(py); + // this blocks until the other thread's critical section finishes + with_critical_section(b2, || { + assert!(b2.borrow().0.load(Ordering::Acquire)); + }); + }); + }); + }); + } + + #[cfg(feature = "macros")] + #[cfg(not(target_arch = "wasm32"))] // We are building wasm Python with pthreads disabled + #[test] + fn test_critical_section2_same_object_no_deadlock() { + let barrier = Barrier::new(2); + + let bool_wrapper = Python::with_gil(|py| -> Py { + Py::new(py, BoolWrapper(AtomicBool::new(false))).unwrap() + }); + + std::thread::scope(|s| { + s.spawn(|| { + Python::with_gil(|py| { + let b = bool_wrapper.bind(py); + with_critical_section2(b, b, || { + barrier.wait(); + std::thread::sleep(std::time::Duration::from_millis(10)); + b.borrow().0.store(true, Ordering::Release); + }) + }); + }); + s.spawn(|| { + barrier.wait(); + Python::with_gil(|py| { + let b = bool_wrapper.bind(py); + // this blocks until the other thread's critical section finishes + with_critical_section(b, || { + assert!(b.borrow().0.load(Ordering::Acquire)); + }); + }); + }); + }); + } + + #[cfg(feature = "macros")] + #[cfg(not(target_arch = "wasm32"))] // We are building wasm Python with pthreads disabled + #[test] + fn test_critical_section2_two_containers() { + let (vec1, vec2) = Python::with_gil(|py| { + ( + Py::new(py, VecWrapper(vec![1, 2, 3])).unwrap(), + Py::new(py, VecWrapper(vec![4, 5])).unwrap(), + ) + }); + + std::thread::scope(|s| { + s.spawn(|| { + Python::with_gil(|py| { + let v1 = vec1.bind(py); + let v2 = vec2.bind(py); + with_critical_section2(v1, v2, || { + // v2.extend(v1) + v2.borrow_mut().0.extend(v1.borrow().0.iter()); + }) + }); + }); + s.spawn(|| { + Python::with_gil(|py| { + let v1 = vec1.bind(py); + let v2 = vec2.bind(py); + with_critical_section2(v1, v2, || { + // v1.extend(v2) + v1.borrow_mut().0.extend(v2.borrow().0.iter()); + }) + }); + }); + }); + + Python::with_gil(|py| { + let v1 = vec1.bind(py); + let v2 = vec2.bind(py); + // execution order is not guaranteed, so we need to check both + // NB: extend should be atomic, items must not be interleaved + // v1.extend(v2) + // v2.extend(v1) + let expected1_vec1 = vec![1, 2, 3, 4, 5]; + let expected1_vec2 = vec![4, 5, 1, 2, 3, 4, 5]; + // v2.extend(v1) + // v1.extend(v2) + let expected2_vec1 = vec![1, 2, 3, 4, 5, 1, 2, 3]; + let expected2_vec2 = vec![4, 5, 1, 2, 3]; + + assert!( + (v1.borrow().0.eq(&expected1_vec1) && v2.borrow().0.eq(&expected1_vec2)) + || (v1.borrow().0.eq(&expected2_vec1) && v2.borrow().0.eq(&expected2_vec2)) + ); + }); + } + #[test] #[cfg(not(target_arch = "wasm32"))] // We are building wasm Python with pthreads disabled fn test_once_ext() { From ea5adbf90413d524fe89ffe00c4ed6f219dafe09 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Sat, 22 Mar 2025 04:03:16 -0600 Subject: [PATCH 485/495] docs: TSAN with PyO3 projects (#4962) * add docs on how to use TSAN * add sample rustup commands * fix markdown * reword and clarify --- guide/src/debugging.md | 59 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/guide/src/debugging.md b/guide/src/debugging.md index 02f1e9951de..2cbf867d438 100644 --- a/guide/src/debugging.md +++ b/guide/src/debugging.md @@ -334,3 +334,62 @@ To use these functions: 1. Run the cell containing these functions in your Jupyter notebook 2. Run `update_launch_json()` in a cell 3. In VS Code, select the "Debug PyO3 (Jupyter)" configuration and start debugging + + +## Thread Safety and Compiler Sanitizers + +PyO3 attempts to match the Rust language-level guarantees for thread safety, but +that does not preclude other code outside of the control of PyO3 or buggy code +managed by a PyO3 extension from creating a thread safety issue. Analyzing +whether or not a piece of Rust code that uses the CPython C API is thread safe +can be quite complicated, since many Python operations can lead to arbitrary +Python code execution. Automated ways to discover thread safety issues can often +be more fruitful than code analysis. + +[ThreadSanitizer](https://clang.llvm.org/docs/ThreadSanitizer.html) is a thread +safety checking runtime that can be used to detect data races triggered by +thread safety bugs or incorrect use of thread-unsafe data structures. While it +can only detect data races triggered by code at runtime, if it does detect +something the reports often point to exactly where the problem is happening. + +To use `ThreadSanitizer` with a library that depends on PyO3, you will need to +install a nightly Rust toolchain, along with the `rust-src` component, since you +will need to compile the Rust standard library: + +```bash +rustup install nightly +rustup override set nighty +rustup component add rust-src +``` + +You will also need a version of CPython compiled using LLVM/Clang with the same +major version of LLVM as is currently used to compile nightly Rust. As of March +2025, Rust nightly uses LLVM 20. + +The [cpython_sanity docker images](https://github.com/nascheme/cpython_sanity) +contain a development environment with a pre-compiled version of CPython 3.13 or +3.14 as well as optionally NumPy and SciPy, all compiled using LLVM 20 and +ThreadSanitizer. + +After activating a nightly Rust toolchain, you can build your project using +`ThreadSanitizer` with the following command: + +```bash +RUSTFLAGS="-Zsanitizer=thread" maturin develop -Zbuild-std --target x86_64-unknown-linux-gnu +``` + +If you are not running on an x86_64 Linux machine, you should replace +`x86_64-unknown-linux-gnu` with the [target +triple](https://doc.rust-lang.org/rustc/platform-support.html#tier-1-with-host-tools) +that is appropriate for your system. You can also replace `maturin develop` with +`cargo test` to run `cargo` tests. Note that `cargo` runs tests in a thread +pool, so `cargo` tests can be a good way to find thread safety issues. + +You can also replace `-Zsanitizer=thread` with `-Zsanitizer=address` or any of +the other sanitizers that are [supported by +Rust](https://doc.rust-lang.org/beta/unstable-book/compiler-flags/sanitizer.html). Note +that you'll need to build CPython from source with the appropriate [configure +script +flags](https://docs.python.org/3/using/configure.html#cmdoption-with-address-sanitizer) +to use the same sanitizer environment as you want to use for your Rust +code. \ No newline at end of file From 0452c0ee5299a1af42f9d966ba3d136a79edb15d Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Tue, 25 Mar 2025 08:23:37 -0600 Subject: [PATCH 486/495] replace quansight-labs/setup-python with actions/setup-python (#5007) --- .github/workflows/ci.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0bb2aacecad..18367f353a0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -592,8 +592,7 @@ jobs: - uses: dtolnay/rust-toolchain@stable with: components: rust-src - # TODO: replace with actions/setup-python when there is support - - uses: quansight-labs/setup-python@v5.4.0 + - uses: actions/setup-python@v5.5.0 with: python-version: "3.13t" - name: Install cargo-llvm-cov From 4aca459fd30441fa006c3eb388c812047f5465ce Mon Sep 17 00:00:00 2001 From: Jonas Pleyer <59249415+jonaspleyer@users.noreply.github.com> Date: Tue, 25 Mar 2025 22:15:18 +0100 Subject: [PATCH 487/495] docs: guide - add link to tables and traits (#5001) * add link to tables and traits * link to md file rather than html Co-authored-by: Icxolu <10486322+Icxolu@users.noreply.github.com> --------- Co-authored-by: Icxolu <10486322+Icxolu@users.noreply.github.com> --- guide/src/conversions.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/guide/src/conversions.md b/guide/src/conversions.md index 991c2061042..ee8dfddebb0 100644 --- a/guide/src/conversions.md +++ b/guide/src/conversions.md @@ -1,3 +1,5 @@ # Type conversions In this portion of the guide we'll talk about the mapping of Python types to Rust types offered by PyO3, as well as the traits available to perform conversions between them. + +See also the conversion [tables](conversions/tables.md) and [traits](conversions/traits.md). From 5caaa371dce8fe8a93c64d7a465c3c2c80ce6e2f Mon Sep 17 00:00:00 2001 From: Zachary Dremann Date: Fri, 28 Mar 2025 04:59:36 -0400 Subject: [PATCH 488/495] fix: convert to cstrings in PyString::from_object (#5008) fixes #5005 This only fixes the API, and adds a test of the API, it does not deprecate the API or introduce a version which takes `&CStr` directly, this can be done later. --- newsfragments/5008.fixed.md | 1 + src/types/string.rs | 28 ++++++++++++++++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 newsfragments/5008.fixed.md diff --git a/newsfragments/5008.fixed.md b/newsfragments/5008.fixed.md new file mode 100644 index 00000000000..6b431d6e437 --- /dev/null +++ b/newsfragments/5008.fixed.md @@ -0,0 +1 @@ +Fix `PyString::from_object`, avoid out of bounds reads by null terminating the `encoding` and `errors` parameters \ No newline at end of file diff --git a/src/types/string.rs b/src/types/string.rs index 0b0de39c681..a389f0df234 100644 --- a/src/types/string.rs +++ b/src/types/string.rs @@ -10,6 +10,7 @@ use crate::types::PyBytes; use crate::IntoPy; use crate::{ffi, Bound, Py, PyAny, PyResult, Python}; use std::borrow::Cow; +use std::ffi::CString; use std::str; /// Deprecated alias for [`PyString`]. @@ -216,6 +217,8 @@ impl PyString { encoding: &str, errors: &str, ) -> PyResult> { + let encoding = CString::new(encoding)?; + let errors = CString::new(errors)?; unsafe { ffi::PyUnicode_FromEncodedObject( src.as_ptr(), @@ -670,6 +673,31 @@ mod tests { }) } + #[test] + fn test_string_from_object() { + Python::with_gil(|py| { + let py_bytes = PyBytes::new(py, b"ab\xFFcd"); + + let py_string = PyString::from_object(&py_bytes, "utf-8", "ignore").unwrap(); + + let result = py_string.to_cow().unwrap(); + assert_eq!(result, "abcd"); + }); + } + + #[test] + fn test_string_from_obect_with_invalid_encoding_errors() { + Python::with_gil(|py| { + let py_bytes = PyBytes::new(py, b"abcd"); + + let result = PyString::from_object(&py_bytes, "utf\0-8", "ignore"); + assert!(result.is_err()); + + let result = PyString::from_object(&py_bytes, "utf-8", "ign\0ore"); + assert!(result.is_err()); + }); + } + #[test] #[cfg(not(any(Py_LIMITED_API, PyPy)))] fn test_string_data_ucs1() { From 1b00b0d27f1b49d4b4237bc616d99016b06c1bd8 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Fri, 28 Mar 2025 09:44:13 +0000 Subject: [PATCH 489/495] implement `PyCallArgs` for borrowed types (#5013) * implement `PyCallArgs` for borrowed types * pass token * clippy --- newsfragments/5013.added.md | 1 + src/call.rs | 191 +++++++++++++++++++++++++++++++++--- 2 files changed, 179 insertions(+), 13 deletions(-) create mode 100644 newsfragments/5013.added.md diff --git a/newsfragments/5013.added.md b/newsfragments/5013.added.md new file mode 100644 index 00000000000..0becc3ee1e9 --- /dev/null +++ b/newsfragments/5013.added.md @@ -0,0 +1 @@ +Implement `PyCallArgs` for `Borrowed<'_, 'py, PyTuple>`, `&Bound<'py, PyTuple>`, and `&Py`. diff --git a/src/call.rs b/src/call.rs index 1da1b67530b..cf9bb16ca3d 100644 --- a/src/call.rs +++ b/src/call.rs @@ -11,8 +11,10 @@ pub(crate) mod private { impl Sealed for () {} impl Sealed for Bound<'_, PyTuple> {} + impl Sealed for &'_ Bound<'_, PyTuple> {} impl Sealed for Py {} - + impl Sealed for &'_ Py {} + impl Sealed for Borrowed<'_, '_, PyTuple> {} pub struct Token; } @@ -100,35 +102,99 @@ impl<'py> PyCallArgs<'py> for () { } impl<'py> PyCallArgs<'py> for Bound<'py, PyTuple> { + #[inline] fn call( self, function: Borrowed<'_, 'py, PyAny>, - kwargs: Borrowed<'_, '_, PyDict>, - _: private::Token, + kwargs: Borrowed<'_, 'py, PyDict>, + token: private::Token, ) -> PyResult> { - unsafe { - ffi::PyObject_Call(function.as_ptr(), self.as_ptr(), kwargs.as_ptr()) - .assume_owned_or_err(function.py()) - } + self.as_borrowed().call(function, kwargs, token) } + #[inline] fn call_positional( self, function: Borrowed<'_, 'py, PyAny>, - _: private::Token, + token: private::Token, ) -> PyResult> { - unsafe { - ffi::PyObject_Call(function.as_ptr(), self.as_ptr(), std::ptr::null_mut()) - .assume_owned_or_err(function.py()) - } + self.as_borrowed().call_positional(function, token) + } +} + +impl<'py> PyCallArgs<'py> for &'_ Bound<'py, PyTuple> { + #[inline] + fn call( + self, + function: Borrowed<'_, 'py, PyAny>, + kwargs: Borrowed<'_, 'py, PyDict>, + token: private::Token, + ) -> PyResult> { + self.as_borrowed().call(function, kwargs, token) + } + + #[inline] + fn call_positional( + self, + function: Borrowed<'_, 'py, PyAny>, + token: private::Token, + ) -> PyResult> { + self.as_borrowed().call_positional(function, token) } } impl<'py> PyCallArgs<'py> for Py { + #[inline] fn call( self, function: Borrowed<'_, 'py, PyAny>, - kwargs: Borrowed<'_, '_, PyDict>, + kwargs: Borrowed<'_, 'py, PyDict>, + token: private::Token, + ) -> PyResult> { + self.bind_borrowed(function.py()) + .call(function, kwargs, token) + } + + #[inline] + fn call_positional( + self, + function: Borrowed<'_, 'py, PyAny>, + token: private::Token, + ) -> PyResult> { + self.bind_borrowed(function.py()) + .call_positional(function, token) + } +} + +impl<'py> PyCallArgs<'py> for &'_ Py { + #[inline] + fn call( + self, + function: Borrowed<'_, 'py, PyAny>, + kwargs: Borrowed<'_, 'py, PyDict>, + token: private::Token, + ) -> PyResult> { + self.bind_borrowed(function.py()) + .call(function, kwargs, token) + } + + #[inline] + fn call_positional( + self, + function: Borrowed<'_, 'py, PyAny>, + token: private::Token, + ) -> PyResult> { + self.bind_borrowed(function.py()) + .call_positional(function, token) + } +} + +impl<'py> PyCallArgs<'py> for Borrowed<'_, 'py, PyTuple> { + #[inline] + fn call( + self, + function: Borrowed<'_, 'py, PyAny>, + kwargs: Borrowed<'_, 'py, PyDict>, _: private::Token, ) -> PyResult> { unsafe { @@ -137,6 +203,7 @@ impl<'py> PyCallArgs<'py> for Py { } } + #[inline] fn call_positional( self, function: Borrowed<'_, 'py, PyAny>, @@ -148,3 +215,101 @@ impl<'py> PyCallArgs<'py> for Py { } } } + +#[cfg(test)] +#[cfg(feature = "macros")] +mod tests { + use crate::{ + pyfunction, + types::{PyDict, PyTuple}, + Py, + }; + + #[pyfunction(signature = (*args, **kwargs), crate = "crate")] + fn args_kwargs( + args: Py, + kwargs: Option>, + ) -> (Py, Option>) { + (args, kwargs) + } + + #[test] + fn test_call() { + use crate::{ + types::{IntoPyDict, PyAnyMethods, PyDict, PyTuple}, + wrap_pyfunction, Py, Python, + }; + + Python::with_gil(|py| { + let f = wrap_pyfunction!(args_kwargs, py).unwrap(); + + let args = PyTuple::new(py, [1, 2, 3]).unwrap(); + let kwargs = &[("foo", 1), ("bar", 2)].into_py_dict(py).unwrap(); + + macro_rules! check_call { + ($args:expr, $kwargs:expr) => { + let (a, k): (Py, Py) = f + .call(args.clone(), Some(kwargs)) + .unwrap() + .extract() + .unwrap(); + assert!(a.is(&args)); + assert!(k.is(kwargs)); + }; + } + + // Bound<'py, PyTuple> + check_call!(args.clone(), kwargs); + + // &Bound<'py, PyTuple> + check_call!(&args, kwargs); + + // Py + check_call!(args.clone().unbind(), kwargs); + + // &Py + check_call!(&args.as_unbound(), kwargs); + + // Borrowed<'_, '_, PyTuple> + check_call!(args.as_borrowed(), kwargs); + }) + } + + #[test] + fn test_call_positional() { + use crate::{ + types::{PyAnyMethods, PyNone, PyTuple}, + wrap_pyfunction, Py, Python, + }; + + Python::with_gil(|py| { + let f = wrap_pyfunction!(args_kwargs, py).unwrap(); + + let args = PyTuple::new(py, [1, 2, 3]).unwrap(); + + macro_rules! check_call { + ($args:expr, $kwargs:expr) => { + let (a, k): (Py, Py) = + f.call1(args.clone()).unwrap().extract().unwrap(); + assert!(a.is(&args)); + assert!(k.is_none(py)); + }; + } + + // Bound<'py, PyTuple> + check_call!(args.clone(), kwargs); + + // &Bound<'py, PyTuple> + check_call!(&args, kwargs); + + // Py + check_call!(args.clone().unbind(), kwargs); + + // &Py + check_call!(args.as_unbound(), kwargs); + + // Borrowed<'_, '_, PyTuple> + check_call!(args.as_borrowed(), kwargs); + }) + } +} From 0f49eb14b0358a8fe85c5930db84c5c404f97dd7 Mon Sep 17 00:00:00 2001 From: Ivan Carvalho <8753214+IvanIsCoding@users.noreply.github.com> Date: Fri, 28 Mar 2025 11:53:52 -0400 Subject: [PATCH 490/495] docs: Remove examples with outdated PyO3 and unmaintained projects (#4952) --- README.md | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 18d6389ff1b..55558594495 100644 --- a/README.md +++ b/README.md @@ -185,13 +185,10 @@ about this topic. ## Examples -- [autopy](https://github.com/autopilot-rs/autopy) _A simple, cross-platform GUI automation library for Python and Rust._ - - Contains an example of building wheels on TravisCI and appveyor using [cibuildwheel](https://github.com/pypa/cibuildwheel) -- [ballista-python](https://github.com/apache/arrow-ballista-python) _A Python library that binds to Apache Arrow distributed query engine Ballista._ - [bed-reader](https://github.com/fastlmm/bed-reader) _Read and write the PLINK BED format, simply and efficiently._ - Shows Rayon/ndarray::parallel (including capturing errors, controlling thread num), Python types to Rust generics, Github Actions - [cellular_raza](https://cellular-raza.com) _A cellular agent-based simulation framework for building complex models from a clean slate._ -- [connector-x](https://github.com/sfu-db/connector-x) _Fastest library to load data from DB to DataFrames in Rust and Python._ +- [connector-x](https://github.com/sfu-db/connector-x/tree/main/connectorx-python) _Fastest library to load data from DB to DataFrames in Rust and Python._ - [cryptography](https://github.com/pyca/cryptography/tree/main/src/rust) _Python cryptography library with some functionality in Rust._ - [css-inline](https://github.com/Stranger6667/css-inline/tree/master/bindings/python) _CSS inlining for Python implemented in Rust._ - [datafusion-python](https://github.com/apache/arrow-datafusion-python) _A Python library that binds to Apache Arrow in-memory query engine DataFusion._ @@ -201,28 +198,20 @@ about this topic. - [feos](https://github.com/feos-org/feos) _Lightning fast thermodynamic modeling in Rust with fully developed Python interface._ - [forust](https://github.com/jinlow/forust) _A lightweight gradient boosted decision tree library written in Rust._ - [granian](https://github.com/emmett-framework/granian) _A Rust HTTP server for Python applications._ -- [greptimedb](https://github.com/GreptimeTeam/greptimedb/tree/main/src/script) _Support [Python scripting](https://docs.greptime.com/user-guide/python-scripts/overview) in the database_ - [haem](https://github.com/BooleanCat/haem) _A Python library for working on Bioinformatics problems._ - [html2text-rs](https://github.com/deedy5/html2text_rs) _Python library for converting HTML to markup or plain text._ - [html-py-ever](https://github.com/PyO3/setuptools-rust/tree/main/examples/html-py-ever) _Using [html5ever](https://github.com/servo/html5ever) through [kuchiki](https://github.com/kuchiki-rs/kuchiki) to speed up html parsing and css-selecting._ -- [hyperjson](https://github.com/mre/hyperjson) _A hyper-fast Python module for reading/writing JSON data using Rust's serde-json._ -- [inline-python](https://github.com/fusion-engineering/inline-python) _Inline Python code directly in your Rust code._ +- [inline-python](https://github.com/m-ou-se/inline-python) _Inline Python code directly in your Rust code._ - [johnnycanencrypt](https://github.com/kushaldas/johnnycanencrypt) OpenPGP library with Yubikey support. - [jsonschema](https://github.com/Stranger6667/jsonschema/tree/master/crates/jsonschema-py) _A high-performance JSON Schema validator for Python._ - [mocpy](https://github.com/cds-astro/mocpy) _Astronomical Python library offering data structures for describing any arbitrary coverage regions on the unit sphere._ - [opendal](https://github.com/apache/opendal/tree/main/bindings/python) _A data access layer that allows users to easily and efficiently retrieve data from various storage services in a unified way._ - [orjson](https://github.com/ijl/orjson) _Fast Python JSON library._ - [ormsgpack](https://github.com/aviramha/ormsgpack) _Fast Python msgpack library._ -- [point-process](https://github.com/ManifoldFR/point-process-rust/tree/master/pylib) _High level API for pointprocesses as a Python library._ -- [polaroid](https://github.com/daggy1234/polaroid) _Hyper Fast and safe image manipulation library for Python written in Rust._ - [polars](https://github.com/pola-rs/polars) _Fast multi-threaded DataFrame library in Rust | Python | Node.js._ - [pydantic-core](https://github.com/pydantic/pydantic-core) _Core validation logic for pydantic written in Rust._ -- [pyheck](https://github.com/kevinheavey/pyheck) _Fast case conversion library, built by wrapping [heck](https://github.com/withoutboats/heck)._ - - Quite easy to follow as there's not much code. -- [pyre](https://github.com/Project-Dream-Weaver/pyre-http) _Fast Python HTTP server written in Rust._ - [primp](https://github.com/deedy5/primp) _The fastest python HTTP client that can impersonate web browsers by mimicking their headers and TLS/JA3/JA4/HTTP2 fingerprints._ - [rateslib](https://github.com/attack68/rateslib) _A fixed income library for Python using Rust extensions._ -- [ril-py](https://github.com/Cryptex-github/ril-py) _A performant and high-level image processing library for Python written in Rust._ - [river](https://github.com/online-ml/river) _Online machine learning in python, the computationally heavy statistics algorithms are implemented in Rust._ - [robyn](https://github.com/sparckles/Robyn) A Super Fast Async Python Web Framework with a Rust runtime. - [rust-python-coverage](https://github.com/cjermain/rust-python-coverage) _Example PyO3 project with automated test coverage for Rust and Python._ @@ -232,7 +221,6 @@ about this topic. - [tokenizers](https://github.com/huggingface/tokenizers/tree/main/bindings/python) _Python bindings to the Hugging Face tokenizers (NLP) written in Rust._ - [tzfpy](http://github.com/ringsaturn/tzfpy) _A fast package to convert longitude/latitude to timezone name._ - [utiles](https://github.com/jessekrubin/utiles) _Fast Python web-map tile utilities_ -- [wasmer-python](https://github.com/wasmerio/wasmer-python) _Python library to run WebAssembly binaries._ ## Articles and other media From 03c31c5c7affdd8805957b5944bd8ca05d1bdec8 Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Fri, 28 Mar 2025 17:03:42 +0100 Subject: [PATCH 491/495] fix `#[pyfunction]` option parsing (#5015) * fix `#[pyfunction]` option parsing * simplify --------- Co-authored-by: David Hewitt --- newsfragments/5015.fixed.md | 1 + pyo3-macros-backend/src/pyfunction.rs | 26 ++++---------------------- src/tests/hygiene/pyfunction.rs | 6 ++++++ 3 files changed, 11 insertions(+), 22 deletions(-) create mode 100644 newsfragments/5015.fixed.md diff --git a/newsfragments/5015.fixed.md b/newsfragments/5015.fixed.md new file mode 100644 index 00000000000..b1da048828f --- /dev/null +++ b/newsfragments/5015.fixed.md @@ -0,0 +1 @@ +Fixes compile error if more options followed after `crate` for `#[pyfunction]`. \ No newline at end of file diff --git a/pyo3-macros-backend/src/pyfunction.rs b/pyo3-macros-backend/src/pyfunction.rs index f28fa795177..c87492f095c 100644 --- a/pyo3-macros-backend/src/pyfunction.rs +++ b/pyo3-macros-backend/src/pyfunction.rs @@ -9,11 +9,9 @@ use crate::{ }; use proc_macro2::TokenStream; use quote::{format_ident, quote}; +use syn::parse::{Parse, ParseStream}; +use syn::punctuated::Punctuated; use syn::{ext::IdentExt, spanned::Spanned, Result}; -use syn::{ - parse::{Parse, ParseStream}, - token::Comma, -}; mod signature; @@ -96,24 +94,8 @@ impl Parse for PyFunctionOptions { fn parse(input: ParseStream<'_>) -> Result { let mut options = PyFunctionOptions::default(); - while !input.is_empty() { - let lookahead = input.lookahead1(); - if lookahead.peek(attributes::kw::name) - || lookahead.peek(attributes::kw::pass_module) - || lookahead.peek(attributes::kw::signature) - || lookahead.peek(attributes::kw::text_signature) - { - options.add_attributes(std::iter::once(input.parse()?))?; - if !input.is_empty() { - let _: Comma = input.parse()?; - } - } else if lookahead.peek(syn::Token![crate]) { - // TODO needs duplicate check? - options.krate = Some(input.parse()?); - } else { - return Err(lookahead.error()); - } - } + let attrs = Punctuated::::parse_terminated(input)?; + options.add_attributes(attrs)?; Ok(options) } diff --git a/src/tests/hygiene/pyfunction.rs b/src/tests/hygiene/pyfunction.rs index 8dcdc369c47..2aabd111dd8 100644 --- a/src/tests/hygiene/pyfunction.rs +++ b/src/tests/hygiene/pyfunction.rs @@ -4,6 +4,12 @@ fn do_something(x: i32) -> crate::PyResult { ::std::result::Result::Ok(x) } +#[crate::pyfunction] +#[pyo3(crate = "crate", name = "check5012")] +fn check_5012(x: i32) -> crate::PyResult { + ::std::result::Result::Ok(x) +} + #[test] fn invoke_wrap_pyfunction() { crate::Python::with_gil(|py| { From dcacb9bbbc8c130238bd88480fc53074e445b4fc Mon Sep 17 00:00:00 2001 From: Thomas Tanon Date: Fri, 28 Mar 2025 17:08:35 +0100 Subject: [PATCH 492/495] Simplify PyFunctionArgument impl on &Bound (#5018) --- src/impl_/extract_argument.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/impl_/extract_argument.rs b/src/impl_/extract_argument.rs index f06298dface..860cce1dfb6 100644 --- a/src/impl_/extract_argument.rs +++ b/src/impl_/extract_argument.rs @@ -41,10 +41,10 @@ impl<'a, 'py, T: 'py> PyFunctionArgument<'a, 'py> for &'a Bound<'py, T> where T: PyTypeCheck, { - type Holder = Option<()>; + type Holder = (); #[inline] - fn extract(obj: &'a Bound<'py, PyAny>, _: &'a mut Option<()>) -> PyResult { + fn extract(obj: &'a Bound<'py, PyAny>, _: &'a mut ()) -> PyResult { obj.downcast().map_err(Into::into) } } From c37a50a7a33e145f6bb87f40cb89cf85f9e5fac7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jelmer=20Vernoo=C4=B3?= Date: Fri, 28 Mar 2025 18:37:16 +0000 Subject: [PATCH 493/495] Add example of more complex exceptions (#5014) --- guide/src/exception.md | 47 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/guide/src/exception.md b/guide/src/exception.md index fd427a34fb2..8de04ba986a 100644 --- a/guide/src/exception.md +++ b/guide/src/exception.md @@ -136,3 +136,50 @@ defines exceptions for several standard library modules. [`PyErr::from_value`]: {{#PYO3_DOCS_URL}}/pyo3/struct.PyErr.html#method.from_value [`PyAny::is_instance`]: {{#PYO3_DOCS_URL}}/pyo3/types/trait.PyAnyMethods.html#tymethod.is_instance [`PyAny::is_instance_of`]: {{#PYO3_DOCS_URL}}/pyo3/types/trait.PyAnyMethods.html#tymethod.is_instance_of + +## Creating more complex exceptions + +If you need to create an exception with more complex behavior, you can also manually create a subclass of `PyException`: + +```rust +#![allow(dead_code)] +# #[cfg(any(not(feature = "abi3")))] { +use pyo3::prelude::*; +use pyo3::types::IntoPyDict; +use pyo3::exceptions::PyException; + +#[pyclass(extends=PyException)] +struct CustomError { + #[pyo3(get)] + url: String, + + #[pyo3(get)] + message: String, +} + +#[pymethods] +impl CustomError { + #[new] + fn new(url: String, message: String) -> Self { + Self { url, message } + } +} + +# fn main() -> PyResult<()> { +Python::with_gil(|py| { + let ctx = [("CustomError", py.get_type::())].into_py_dict(py)?; + pyo3::py_run!( + py, + *ctx, + "assert str(CustomError) == \"\", repr(CustomError)" + ); + pyo3::py_run!(py, *ctx, "assert CustomError('https://example.com', 'something went bad').args == ('https://example.com', 'something went bad')"); + pyo3::py_run!(py, *ctx, "assert CustomError('https://example.com', 'something went bad').url == 'https://example.com'"); +# Ok(()) +}) +# } +# } + +``` + +Note that this is not possible when the ``abi3`` feature is enabled, as that prevents subclassing ``PyException``. From d85a02d9b11f7c057e3627a0393d5d9b876dbc0a Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Fri, 28 Mar 2025 19:47:58 +0100 Subject: [PATCH 494/495] split `PyFunctionArgument` to specialize `Option` (#5002) * split `PyFunctionArgument` to specialize `Option` Adds const generic parameter to `PyFunctionArgument` to allow specialization. This allows `Option` wrapped extraction of some types that can't implement `FromPyObject(Bound)` because they require a `holder` to borrow from, most notably `&T` for `T: PyClass`. Closes #4965 * tests and changelog * clippy --------- Co-authored-by: David Hewitt --- guide/src/class.md | 4 +- newsfragments/5002.fixed.md | 1 + pyo3-macros-backend/src/params.rs | 44 +++++++++++------ pyo3-macros-backend/src/pyclass.rs | 6 +-- pyo3-macros-backend/src/pymethod.rs | 20 ++++++-- pyo3-macros-backend/src/utils.rs | 70 +++++++++++++++++++++++++++ src/impl_/extract_argument.rs | 30 ++++++------ src/impl_/pyclass/probes.rs | 6 +++ tests/test_pyfunction.rs | 39 +++++++++++++++ tests/ui/invalid_cancel_handle.stderr | 30 ++++++------ 10 files changed, 194 insertions(+), 56 deletions(-) create mode 100644 newsfragments/5002.fixed.md diff --git a/guide/src/class.md b/guide/src/class.md index 543435a3de9..90991328fe6 100644 --- a/guide/src/class.md +++ b/guide/src/class.md @@ -1390,7 +1390,7 @@ impl pyo3::PyClass for MyClass { type Frozen = pyo3::pyclass::boolean_struct::False; } -impl<'a, 'py> pyo3::impl_::extract_argument::PyFunctionArgument<'a, 'py> for &'a MyClass +impl<'a, 'py> pyo3::impl_::extract_argument::PyFunctionArgument<'a, 'py, false> for &'a MyClass { type Holder = ::std::option::Option>; @@ -1400,7 +1400,7 @@ impl<'a, 'py> pyo3::impl_::extract_argument::PyFunctionArgument<'a, 'py> for &'a } } -impl<'a, 'py> pyo3::impl_::extract_argument::PyFunctionArgument<'a, 'py> for &'a mut MyClass +impl<'a, 'py> pyo3::impl_::extract_argument::PyFunctionArgument<'a, 'py, false> for &'a mut MyClass { type Holder = ::std::option::Option>; diff --git a/newsfragments/5002.fixed.md b/newsfragments/5002.fixed.md new file mode 100644 index 00000000000..ad9b0091a79 --- /dev/null +++ b/newsfragments/5002.fixed.md @@ -0,0 +1 @@ +Fix compile failure with required `#[pyfunction]` arguments taking `Option<&str>` and `Option<&T>` (for `#[pyclass]` types). diff --git a/pyo3-macros-backend/src/params.rs b/pyo3-macros-backend/src/params.rs index 806e9f57ad9..ae7a6c916a8 100644 --- a/pyo3-macros-backend/src/params.rs +++ b/pyo3-macros-backend/src/params.rs @@ -1,4 +1,4 @@ -use crate::utils::{deprecated_from_py_with, Ctx}; +use crate::utils::{deprecated_from_py_with, Ctx, TypeExt as _}; use crate::{ attributes::FromPyWithAttribute, method::{FnArg, FnSpec, RegularArg}, @@ -200,7 +200,7 @@ fn impl_arg_param( let holder = holders.push_holder(arg.name.span()); let name_str = arg.name.to_string(); quote_spanned! { arg.name.span() => - #pyo3_path::impl_::extract_argument::extract_argument( + #pyo3_path::impl_::extract_argument::extract_argument::<_, false>( &_args, &mut #holder, #name_str @@ -211,7 +211,7 @@ fn impl_arg_param( let holder = holders.push_holder(arg.name.span()); let name_str = arg.name.to_string(); quote_spanned! { arg.name.span() => - #pyo3_path::impl_::extract_argument::extract_optional_argument( + #pyo3_path::impl_::extract_argument::extract_optional_argument::<_, false>( _kwargs.as_deref(), &mut #holder, #name_str, @@ -238,8 +238,9 @@ pub(crate) fn impl_regular_arg_param( // Use this macro inside this function, to ensure that all code generated here is associated // with the function argument + let use_probe = quote!(use #pyo3_path::impl_::pyclass::Probe as _;); macro_rules! quote_arg_span { - ($($tokens:tt)*) => { quote_spanned!(arg.ty.span() => $($tokens)*) } + ($($tokens:tt)*) => { quote_spanned!(arg.ty.span() => { #use_probe $($tokens)* }) } } let name_str = arg.name.to_string(); @@ -251,6 +252,7 @@ pub(crate) fn impl_regular_arg_param( default = default.map(|tokens| some_wrap(tokens, ctx)); } + let arg_ty = arg.ty.clone().elide_lifetimes(); if let Some(FromPyWithAttribute { kw, .. }) = arg.from_py_with { let extractor = quote_spanned! { kw.span => { let from_py_with: fn(_) -> _ = #from_py_with; from_py_with } @@ -279,9 +281,13 @@ pub(crate) fn impl_regular_arg_param( } } else if let Some(default) = default { let holder = holders.push_holder(arg.name.span()); - if arg.option_wrapped_type.is_some() { + if let Some(arg_ty) = arg.option_wrapped_type { + let arg_ty = arg_ty.clone().elide_lifetimes(); quote_arg_span! { - #pyo3_path::impl_::extract_argument::extract_optional_argument( + #pyo3_path::impl_::extract_argument::extract_optional_argument::< + _, + { #pyo3_path::impl_::pyclass::IsOption::<#arg_ty>::VALUE } + >( #arg_value, &mut #holder, #name_str, @@ -293,22 +299,28 @@ pub(crate) fn impl_regular_arg_param( } } else { quote_arg_span! { - #pyo3_path::impl_::extract_argument::extract_argument_with_default( - #arg_value, - &mut #holder, - #name_str, - #[allow(clippy::redundant_closure)] - { - || #default - } - )? + #pyo3_path::impl_::extract_argument::extract_argument_with_default::< + _, + { #pyo3_path::impl_::pyclass::IsOption::<#arg_ty>::VALUE } + >( + #arg_value, + &mut #holder, + #name_str, + #[allow(clippy::redundant_closure)] + { + || #default + } + )? } } } else { let holder = holders.push_holder(arg.name.span()); let unwrap = quote! {unsafe { #pyo3_path::impl_::extract_argument::unwrap_required_argument(#arg_value) }}; quote_arg_span! { - #pyo3_path::impl_::extract_argument::extract_argument( + #pyo3_path::impl_::extract_argument::extract_argument::< + _, + { #pyo3_path::impl_::pyclass::IsOption::<#arg_ty>::VALUE } + >( #unwrap, &mut #holder, #name_str diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index de11b0604ad..7512d63c9fb 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -2097,7 +2097,7 @@ impl<'a> PyClassImplsBuilder<'a> { let cls = self.cls; if self.attr.options.frozen.is_some() { quote! { - impl<'a, 'py> #pyo3_path::impl_::extract_argument::PyFunctionArgument<'a, 'py> for &'a #cls + impl<'a, 'py> #pyo3_path::impl_::extract_argument::PyFunctionArgument<'a, 'py, false> for &'a #cls { type Holder = ::std::option::Option<#pyo3_path::PyRef<'py, #cls>>; @@ -2109,7 +2109,7 @@ impl<'a> PyClassImplsBuilder<'a> { } } else { quote! { - impl<'a, 'py> #pyo3_path::impl_::extract_argument::PyFunctionArgument<'a, 'py> for &'a #cls + impl<'a, 'py> #pyo3_path::impl_::extract_argument::PyFunctionArgument<'a, 'py, false> for &'a #cls { type Holder = ::std::option::Option<#pyo3_path::PyRef<'py, #cls>>; @@ -2119,7 +2119,7 @@ impl<'a> PyClassImplsBuilder<'a> { } } - impl<'a, 'py> #pyo3_path::impl_::extract_argument::PyFunctionArgument<'a, 'py> for &'a mut #cls + impl<'a, 'py> #pyo3_path::impl_::extract_argument::PyFunctionArgument<'a, 'py, false> for &'a mut #cls { type Holder = ::std::option::Option<#pyo3_path::PyRefMut<'py, #cls>>; diff --git a/pyo3-macros-backend/src/pymethod.rs b/pyo3-macros-backend/src/pymethod.rs index a116acf69a3..a1689e4e75c 100644 --- a/pyo3-macros-backend/src/pymethod.rs +++ b/pyo3-macros-backend/src/pymethod.rs @@ -4,7 +4,7 @@ use std::ffi::CString; use crate::attributes::{FromPyWithAttribute, NameAttribute, RenamingRule}; use crate::method::{CallingConvention, ExtractErrorMode, PyArg}; use crate::params::{impl_regular_arg_param, Holders}; -use crate::utils::{deprecated_from_py_with, PythonDoc}; +use crate::utils::{deprecated_from_py_with, PythonDoc, TypeExt as _}; use crate::utils::{Ctx, LitCStr}; use crate::{ method::{FnArg, FnSpec, FnType, SelfType}, @@ -699,8 +699,13 @@ pub fn impl_py_setter_def( .unwrap_or_default(); let holder = holders.push_holder(span); + let ty = field.ty.clone().elide_lifetimes(); quote! { - let _val = #pyo3_path::impl_::extract_argument::extract_argument(_value.into(), &mut #holder, #name)?; + use #pyo3_path::impl_::pyclass::Probe as _; + let _val = #pyo3_path::impl_::extract_argument::extract_argument::< + _, + { #pyo3_path::impl_::pyclass::IsOption::<#ty>::VALUE } + >(_value.into(), &mut #holder, #name)?; } } }; @@ -1198,13 +1203,18 @@ fn extract_object( } } else { let holder = holders.push_holder(Span::call_site()); - quote! { - #pyo3_path::impl_::extract_argument::extract_argument( + let ty = arg.ty().clone().elide_lifetimes(); + quote! {{ + use #pyo3_path::impl_::pyclass::Probe as _; + #pyo3_path::impl_::extract_argument::extract_argument::< + _, + { #pyo3_path::impl_::pyclass::IsOption::<#ty>::VALUE } + >( unsafe { #pyo3_path::impl_::pymethods::BoundRef::ref_from_ptr(py, &#source_ptr).0 }, &mut #holder, #name ) - } + }} }; let extracted = extract_error_mode.handle_error(extract, ctx); diff --git a/pyo3-macros-backend/src/utils.rs b/pyo3-macros-backend/src/utils.rs index d2f1eb84c6f..bdec23388df 100644 --- a/pyo3-macros-backend/src/utils.rs +++ b/pyo3-macros-backend/src/utils.rs @@ -337,3 +337,73 @@ pub(crate) fn deprecated_from_py_with(expr_path: &ExprPathWrap) -> Option Self; +} + +impl TypeExt for syn::Type { + fn elide_lifetimes(mut self) -> Self { + fn elide_lifetimes(ty: &mut syn::Type) { + match ty { + syn::Type::Path(type_path) => { + if let Some(qself) = &mut type_path.qself { + elide_lifetimes(&mut qself.ty) + } + for seg in &mut type_path.path.segments { + if let syn::PathArguments::AngleBracketed(args) = &mut seg.arguments { + for generic_arg in &mut args.args { + match generic_arg { + syn::GenericArgument::Lifetime(lt) => { + *lt = syn::Lifetime::new("'_", lt.span()); + } + syn::GenericArgument::Type(ty) => elide_lifetimes(ty), + syn::GenericArgument::AssocType(assoc) => { + elide_lifetimes(&mut assoc.ty) + } + + syn::GenericArgument::Const(_) + | syn::GenericArgument::AssocConst(_) + | syn::GenericArgument::Constraint(_) + | _ => {} + } + } + } + } + } + syn::Type::Reference(type_ref) => { + if let Some(lt) = type_ref.lifetime.as_mut() { + *lt = syn::Lifetime::new("'_", lt.span()); + } + elide_lifetimes(&mut type_ref.elem); + } + syn::Type::Tuple(type_tuple) => { + for ty in &mut type_tuple.elems { + elide_lifetimes(ty); + } + } + syn::Type::Array(type_array) => elide_lifetimes(&mut type_array.elem), + syn::Type::Slice(ty) => elide_lifetimes(&mut ty.elem), + syn::Type::Group(ty) => elide_lifetimes(&mut ty.elem), + syn::Type::Paren(ty) => elide_lifetimes(&mut ty.elem), + syn::Type::Ptr(ty) => elide_lifetimes(&mut ty.elem), + + syn::Type::BareFn(_) + | syn::Type::ImplTrait(_) + | syn::Type::Infer(_) + | syn::Type::Macro(_) + | syn::Type::Never(_) + | syn::Type::TraitObject(_) + | syn::Type::Verbatim(_) + | _ => {} + } + } + + elide_lifetimes(&mut self); + self + } +} diff --git a/src/impl_/extract_argument.rs b/src/impl_/extract_argument.rs index 860cce1dfb6..025f050c8a9 100644 --- a/src/impl_/extract_argument.rs +++ b/src/impl_/extract_argument.rs @@ -20,12 +20,12 @@ type PyArg<'py> = Borrowed<'py, 'py, PyAny>; /// will be dropped as soon as the pyfunction call ends. /// /// There exists a trivial blanket implementation for `T: FromPyObject` with `Holder = ()`. -pub trait PyFunctionArgument<'a, 'py>: Sized + 'a { +pub trait PyFunctionArgument<'a, 'py, const IS_OPTION: bool>: Sized + 'a { type Holder: FunctionArgumentHolder; fn extract(obj: &'a Bound<'py, PyAny>, holder: &'a mut Self::Holder) -> PyResult; } -impl<'a, 'py, T> PyFunctionArgument<'a, 'py> for T +impl<'a, 'py, T> PyFunctionArgument<'a, 'py, false> for T where T: FromPyObjectBound<'a, 'py> + 'a, { @@ -37,7 +37,7 @@ where } } -impl<'a, 'py, T: 'py> PyFunctionArgument<'a, 'py> for &'a Bound<'py, T> +impl<'a, 'py, T: 'py> PyFunctionArgument<'a, 'py, false> for &'a Bound<'py, T> where T: PyTypeCheck, { @@ -49,24 +49,24 @@ where } } -impl<'a, 'py, T: 'py> PyFunctionArgument<'a, 'py> for Option<&'a Bound<'py, T>> +impl<'a, 'py, T> PyFunctionArgument<'a, 'py, true> for Option where - T: PyTypeCheck, + T: PyFunctionArgument<'a, 'py, false>, // inner `Option`s will use `FromPyObject` { - type Holder = (); + type Holder = T::Holder; #[inline] - fn extract(obj: &'a Bound<'py, PyAny>, _: &'a mut ()) -> PyResult { + fn extract(obj: &'a Bound<'py, PyAny>, holder: &'a mut T::Holder) -> PyResult { if obj.is_none() { Ok(None) } else { - Ok(Some(obj.downcast()?)) + Ok(Some(T::extract(obj, holder)?)) } } } #[cfg(all(Py_LIMITED_API, not(Py_3_10)))] -impl<'a> PyFunctionArgument<'a, '_> for &'a str { +impl<'a> PyFunctionArgument<'a, '_, false> for &'a str { type Holder = Option>; #[inline] @@ -110,13 +110,13 @@ pub fn extract_pyclass_ref_mut<'a, 'py: 'a, T: PyClass>( /// The standard implementation of how PyO3 extracts a `#[pyfunction]` or `#[pymethod]` function argument. #[doc(hidden)] -pub fn extract_argument<'a, 'py, T>( +pub fn extract_argument<'a, 'py, T, const IS_OPTION: bool>( obj: &'a Bound<'py, PyAny>, holder: &'a mut T::Holder, arg_name: &str, ) -> PyResult where - T: PyFunctionArgument<'a, 'py>, + T: PyFunctionArgument<'a, 'py, IS_OPTION>, { match PyFunctionArgument::extract(obj, holder) { Ok(value) => Ok(value), @@ -127,14 +127,14 @@ where /// Alternative to [`extract_argument`] used for `Option` arguments. This is necessary because Option<&T> /// does not implement `PyFunctionArgument` for `T: PyClass`. #[doc(hidden)] -pub fn extract_optional_argument<'a, 'py, T>( +pub fn extract_optional_argument<'a, 'py, T, const IS_OPTION: bool>( obj: Option<&'a Bound<'py, PyAny>>, holder: &'a mut T::Holder, arg_name: &str, default: fn() -> Option, ) -> PyResult> where - T: PyFunctionArgument<'a, 'py>, + T: PyFunctionArgument<'a, 'py, IS_OPTION>, { match obj { Some(obj) => { @@ -151,14 +151,14 @@ where /// Alternative to [`extract_argument`] used when the argument has a default value provided by an annotation. #[doc(hidden)] -pub fn extract_argument_with_default<'a, 'py, T>( +pub fn extract_argument_with_default<'a, 'py, T, const IS_OPTION: bool>( obj: Option<&'a Bound<'py, PyAny>>, holder: &'a mut T::Holder, arg_name: &str, default: fn() -> T, ) -> PyResult where - T: PyFunctionArgument<'a, 'py>, + T: PyFunctionArgument<'a, 'py, IS_OPTION>, { match obj { Some(obj) => extract_argument(obj, holder, arg_name), diff --git a/src/impl_/pyclass/probes.rs b/src/impl_/pyclass/probes.rs index f1c3468cf9b..719976e89c7 100644 --- a/src/impl_/pyclass/probes.rs +++ b/src/impl_/pyclass/probes.rs @@ -70,3 +70,9 @@ probe!(IsSync); impl IsSync { pub const VALUE: bool = true; } + +probe!(IsOption); + +impl IsOption> { + pub const VALUE: bool = true; +} diff --git a/tests/test_pyfunction.rs b/tests/test_pyfunction.rs index 822e0200a89..77983653c89 100644 --- a/tests/test_pyfunction.rs +++ b/tests/test_pyfunction.rs @@ -46,6 +46,45 @@ fn test_optional_bool() { }); } +#[pyfunction] +#[pyo3(signature=(arg))] +fn required_optional_str(arg: Option<&str>) -> &str { + arg.unwrap_or("") +} + +#[test] +fn test_optional_str() { + // Regression test for issue #4965 + Python::with_gil(|py| { + let f = wrap_pyfunction!(required_optional_str)(py).unwrap(); + + py_assert!(py, f, "f('') == ''"); + py_assert!(py, f, "f('foo') == 'foo'"); + py_assert!(py, f, "f(None) == ''"); + }); +} + +#[pyclass] +struct MyClass(); + +#[pyfunction] +#[pyo3(signature=(arg))] +fn required_optional_class(arg: Option<&MyClass>) { + let _ = arg; +} + +#[test] +fn test_required_optional_class() { + // Regression test for issue #4965 + Python::with_gil(|py| { + let f = wrap_pyfunction!(required_optional_class)(py).unwrap(); + let val = Bound::new(py, MyClass()).unwrap(); + + py_assert!(py, f val, "f(val) is None"); + py_assert!(py, f, "f(None) is None"); + }); +} + #[cfg(not(Py_LIMITED_API))] #[pyfunction] fn buffer_inplace_add(py: Python<'_>, x: PyBuffer, y: PyBuffer) { diff --git a/tests/ui/invalid_cancel_handle.stderr b/tests/ui/invalid_cancel_handle.stderr index 90a8caa4229..134b44ed7cf 100644 --- a/tests/ui/invalid_cancel_handle.stderr +++ b/tests/ui/invalid_cancel_handle.stderr @@ -38,7 +38,7 @@ note: function defined here | ^^^^^^^^^^^^^^^^^^^^^^^^ -------------- = note: this error originates in the attribute macro `pyfunction` (in Nightly builds, run with -Z macro-backtrace for more info) -error[E0277]: the trait bound `CancelHandle: PyFunctionArgument<'_, '_>` is not satisfied +error[E0277]: the trait bound `CancelHandle: PyFunctionArgument<'_, '_, false>` is not satisfied --> tests/ui/invalid_cancel_handle.rs:20:50 | 20 | async fn missing_cancel_handle_attribute(_param: pyo3::coroutine::CancelHandle) {} @@ -47,35 +47,35 @@ error[E0277]: the trait bound `CancelHandle: PyFunctionArgument<'_, '_>` is not = help: the trait `PyClass` is implemented for `pyo3::coroutine::Coroutine` = note: required for `CancelHandle` to implement `FromPyObject<'_>` = note: required for `CancelHandle` to implement `FromPyObjectBound<'_, '_>` - = note: required for `CancelHandle` to implement `PyFunctionArgument<'_, '_>` + = note: required for `CancelHandle` to implement `PyFunctionArgument<'_, '_, false>` note: required by a bound in `extract_argument` --> src/impl_/extract_argument.rs | - | pub fn extract_argument<'a, 'py, T>( + | pub fn extract_argument<'a, 'py, T, const IS_OPTION: bool>( | ---------------- required by a bound in this function ... - | T: PyFunctionArgument<'a, 'py>, - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `extract_argument` + | T: PyFunctionArgument<'a, 'py, IS_OPTION>, + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `extract_argument` -error[E0277]: the trait bound `CancelHandle: PyFunctionArgument<'_, '_>` is not satisfied +error[E0277]: the trait bound `CancelHandle: PyFunctionArgument<'_, '_, false>` is not satisfied --> tests/ui/invalid_cancel_handle.rs:20:50 | 20 | async fn missing_cancel_handle_attribute(_param: pyo3::coroutine::CancelHandle) {} | ^^^^ the trait `Clone` is not implemented for `CancelHandle` | - = help: the following other types implement trait `PyFunctionArgument<'a, 'py>`: - &'a mut pyo3::coroutine::Coroutine - &'a pyo3::Bound<'py, T> - &'a pyo3::coroutine::Coroutine - Option<&'a pyo3::Bound<'py, T>> + = help: the following other types implement trait `PyFunctionArgument<'a, 'py, IS_OPTION>`: + `&'a mut pyo3::coroutine::Coroutine` implements `PyFunctionArgument<'a, 'py, false>` + `&'a pyo3::Bound<'py, T>` implements `PyFunctionArgument<'a, 'py, false>` + `&'a pyo3::coroutine::Coroutine` implements `PyFunctionArgument<'a, 'py, false>` + `Option` implements `PyFunctionArgument<'a, 'py, true>` = note: required for `CancelHandle` to implement `FromPyObject<'_>` = note: required for `CancelHandle` to implement `FromPyObjectBound<'_, '_>` - = note: required for `CancelHandle` to implement `PyFunctionArgument<'_, '_>` + = note: required for `CancelHandle` to implement `PyFunctionArgument<'_, '_, false>` note: required by a bound in `extract_argument` --> src/impl_/extract_argument.rs | - | pub fn extract_argument<'a, 'py, T>( + | pub fn extract_argument<'a, 'py, T, const IS_OPTION: bool>( | ---------------- required by a bound in this function ... - | T: PyFunctionArgument<'a, 'py>, - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `extract_argument` + | T: PyFunctionArgument<'a, 'py, IS_OPTION>, + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `extract_argument` From a213b368bd5bf859c2acb655bfed029e17c3b447 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Mon, 31 Mar 2025 22:04:51 +0100 Subject: [PATCH 495/495] release: 0.24.1 (#5021) --- CHANGELOG.md | 25 ++++++++++++++++--- Cargo.toml | 8 +++--- README.md | 4 +-- 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/4969.added.md | 1 - newsfragments/4978.added.md | 1 - newsfragments/4981.fixed.md | 1 - newsfragments/4984.added.md | 1 - newsfragments/4988.fixed.md | 1 - newsfragments/4992.added.md | 1 - newsfragments/5002.fixed.md | 1 - newsfragments/5008.fixed.md | 1 - newsfragments/5013.added.md | 1 - newsfragments/5015.fixed.md | 1 - pyo3-build-config/Cargo.toml | 2 +- pyo3-ffi/Cargo.toml | 4 +-- pyo3-ffi/README.md | 4 +-- pyo3-macros-backend/Cargo.toml | 6 ++--- pyo3-macros/Cargo.toml | 4 +-- pyproject.toml | 2 +- tests/ui/reject_generics.stderr | 4 +-- 25 files changed, 46 insertions(+), 37 deletions(-) delete mode 100644 newsfragments/4969.added.md delete mode 100644 newsfragments/4978.added.md delete mode 100644 newsfragments/4981.fixed.md delete mode 100644 newsfragments/4984.added.md delete mode 100644 newsfragments/4988.fixed.md delete mode 100644 newsfragments/4992.added.md delete mode 100644 newsfragments/5002.fixed.md delete mode 100644 newsfragments/5008.fixed.md delete mode 100644 newsfragments/5013.added.md delete mode 100644 newsfragments/5015.fixed.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 1532d3a7e9f..57196517016 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.24.1] - 2025-03-31 + +### Added + +- Add `abi3-py313` feature. [#4969](https://github.com/PyO3/pyo3/pull/4969) +- Add `PyAnyMethods::getattr_opt`. [#4978](https://github.com/PyO3/pyo3/pull/4978) +- Add `PyInt::new` constructor for all supported number types (i32, u32, i64, u64, isize, usize). [#4984](https://github.com/PyO3/pyo3/pull/4984) +- Add `pyo3::sync::with_critical_section2`. [#4992](https://github.com/PyO3/pyo3/pull/4992) +- Implement `PyCallArgs` for `Borrowed<'_, 'py, PyTuple>`, `&Bound<'py, PyTuple>`, and `&Py`. [#5013](https://github.com/PyO3/pyo3/pull/5013) + +### Fixed + +- Fix `is_type_of` for native types not using same specialized check as `is_type_of_bound`. [#4981](https://github.com/PyO3/pyo3/pull/4981) +- Fix `Probe` class naming issue with `#[pymethods]`. [#4988](https://github.com/PyO3/pyo3/pull/4988) +- Fix compile failure with required `#[pyfunction]` arguments taking `Option<&str>` and `Option<&T>` (for `#[pyclass]` types). [#5002](https://github.com/PyO3/pyo3/pull/5002) +- Fix `PyString::from_object` causing of bounds reads whith `encoding` and `errors` parameters which are not nul-terminated. [#5008](https://github.com/PyO3/pyo3/pull/5008) +- Fix compile error when additional options follow after `crate` for `#[pyfunction]`. [#5015](https://github.com/PyO3/pyo3/pull/5015) + ## [0.24.0] - 2025-03-09 ### Packaging @@ -17,6 +35,7 @@ To see unreleased changes, please see the [CHANGELOG on the main branch guide](h - Add supported CPython/PyPy versions to cargo package metadata. [#4756](https://github.com/PyO3/pyo3/pull/4756) - Bump `target-lexicon` dependency to 0.13. [#4822](https://github.com/PyO3/pyo3/pull/4822) - Add optional `jiff` dependency to add conversions for `jiff` datetime types. [#4823](https://github.com/PyO3/pyo3/pull/4823) +- Add optional `uuid` dependency to add conversions for `uuid::Uuid`. [#4864](https://github.com/PyO3/pyo3/pull/4864) - Bump minimum supported `inventory` version to 0.3.5. [#4954](https://github.com/PyO3/pyo3/pull/4954) ### Added @@ -25,7 +44,6 @@ To see unreleased changes, please see the [CHANGELOG on the main branch guide](h - Add `PyCallArgs` trait for passing arguments into the Python calling protocol. This enabled using a faster calling convention for certain types, improving performance. [#4768](https://github.com/PyO3/pyo3/pull/4768) - Add `#[pyo3(default = ...']` option for `#[derive(FromPyObject)]` to set a default value for extracted fields of named structs. [#4829](https://github.com/PyO3/pyo3/pull/4829) - Add `#[pyo3(into_py_with = ...)]` option for `#[derive(IntoPyObject, IntoPyObjectRef)]`. [#4850](https://github.com/PyO3/pyo3/pull/4850) -- Add uuid to/from python conversions. [#4864](https://github.com/PyO3/pyo3/pull/4864) - Add FFI definitions `PyThreadState_GetFrame` and `PyFrame_GetBack`. [#4866](https://github.com/PyO3/pyo3/pull/4866) - Optimize `last` for `BoundListIterator`, `BoundTupleIterator` and `BorrowedTupleIterator`. [#4878](https://github.com/PyO3/pyo3/pull/4878) - Optimize `Iterator::count()` for `PyDict`, `PyList`, `PyTuple` & `PySet`. [#4878](https://github.com/PyO3/pyo3/pull/4878) @@ -60,6 +78,7 @@ To see unreleased changes, please see the [CHANGELOG on the main branch guide](h ## [0.23.5] - 2025-02-22 + ### Packaging - Add support for PyPy3.11 [#4760](https://github.com/PyO3/pyo3/pull/4760) @@ -109,7 +128,6 @@ To see unreleased changes, please see the [CHANGELOG on the main branch guide](h - Fix unresolved symbol link failures on Windows when compiling for Python 3.13t using the `generate-import-lib` feature. [#4749](https://github.com/PyO3/pyo3/pull/4749) - Fix compile-time regression in PyO3 0.23.0 where changing `PYO3_CONFIG_FILE` would not reconfigure PyO3 for the new interpreter. [#4758](https://github.com/PyO3/pyo3/pull/4758) - ## [0.23.2] - 2024-11-25 ### Added @@ -2113,7 +2131,8 @@ Yanked - Initial release -[Unreleased]: https://github.com/pyo3/pyo3/compare/v0.24.0...HEAD +[Unreleased]: https://github.com/pyo3/pyo3/compare/v0.24.1...HEAD +[0.24.1]: https://github.com/pyo3/pyo3/compare/v0.24.0...v0.24.1 [0.24.0]: https://github.com/pyo3/pyo3/compare/v0.23.5...v0.24.0 [0.23.5]: https://github.com/pyo3/pyo3/compare/v0.23.4...v0.23.5 [0.23.4]: https://github.com/pyo3/pyo3/compare/v0.23.3...v0.23.4 diff --git a/Cargo.toml b/Cargo.toml index 84c6daba30e..375b0dea024 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3" -version = "0.24.0" +version = "0.24.1" description = "Bindings to Python interpreter" authors = ["PyO3 Project and Contributors "] readme = "README.md" @@ -21,10 +21,10 @@ memoffset = "0.9" once_cell = "1.13" # ffi bindings to the python interpreter, split into a separate crate so they can be used independently -pyo3-ffi = { path = "pyo3-ffi", version = "=0.24.0" } +pyo3-ffi = { path = "pyo3-ffi", version = "=0.24.1" } # support crates for macros feature -pyo3-macros = { path = "pyo3-macros", version = "=0.24.0", optional = true } +pyo3-macros = { path = "pyo3-macros", version = "=0.24.1", optional = true } indoc = { version = "2.0.1", optional = true } unindent = { version = "0.2.1", optional = true } @@ -68,7 +68,7 @@ static_assertions = "1.1.0" uuid = { version = "1.10.0", features = ["v4"] } [build-dependencies] -pyo3-build-config = { path = "pyo3-build-config", version = "=0.24.0", features = ["resolve-config"] } +pyo3-build-config = { path = "pyo3-build-config", version = "=0.24.1", features = ["resolve-config"] } [features] default = ["macros"] diff --git a/README.md b/README.md index 55558594495..7ad5932b35d 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,7 @@ name = "string_sum" crate-type = ["cdylib"] [dependencies] -pyo3 = { version = "0.24.0", features = ["extension-module"] } +pyo3 = { version = "0.24.1", features = ["extension-module"] } ``` **`src/lib.rs`** @@ -140,7 +140,7 @@ Start a new project with `cargo new` and add `pyo3` to the `Cargo.toml` like th ```toml [dependencies.pyo3] -version = "0.24.0" +version = "0.24.1" features = ["auto-initialize"] ``` diff --git a/examples/decorator/.template/pre-script.rhai b/examples/decorator/.template/pre-script.rhai index 1ad80e9afbe..bb3c6cdbd7d 100644 --- a/examples/decorator/.template/pre-script.rhai +++ b/examples/decorator/.template/pre-script.rhai @@ -1,4 +1,4 @@ -variable::set("PYO3_VERSION", "0.24.0"); +variable::set("PYO3_VERSION", "0.24.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 1ad80e9afbe..bb3c6cdbd7d 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.24.0"); +variable::set("PYO3_VERSION", "0.24.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 ffd73d3a0fa..8f9d871b601 100644 --- a/examples/plugin/.template/pre-script.rhai +++ b/examples/plugin/.template/pre-script.rhai @@ -1,4 +1,4 @@ -variable::set("PYO3_VERSION", "0.24.0"); +variable::set("PYO3_VERSION", "0.24.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 fd6e6775627..1553f9cf931 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.24.0"); +variable::set("PYO3_VERSION", "0.24.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 1ad80e9afbe..bb3c6cdbd7d 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.24.0"); +variable::set("PYO3_VERSION", "0.24.1"); file::rename(".template/Cargo.toml", "Cargo.toml"); file::rename(".template/pyproject.toml", "pyproject.toml"); file::delete(".template"); diff --git a/newsfragments/4969.added.md b/newsfragments/4969.added.md deleted file mode 100644 index 7a59e9aef6f..00000000000 --- a/newsfragments/4969.added.md +++ /dev/null @@ -1 +0,0 @@ -Added `abi3-py313` feature \ No newline at end of file diff --git a/newsfragments/4978.added.md b/newsfragments/4978.added.md deleted file mode 100644 index 5518cee89d8..00000000000 --- a/newsfragments/4978.added.md +++ /dev/null @@ -1 +0,0 @@ -Implement getattr_opt in `PyAnyMethods` \ No newline at end of file diff --git a/newsfragments/4981.fixed.md b/newsfragments/4981.fixed.md deleted file mode 100644 index 0ac31b19a11..00000000000 --- a/newsfragments/4981.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Fix `is_type_of` for native types not using same specialized check as `is_type_of_bound`. diff --git a/newsfragments/4984.added.md b/newsfragments/4984.added.md deleted file mode 100644 index 63559617812..00000000000 --- a/newsfragments/4984.added.md +++ /dev/null @@ -1 +0,0 @@ -Added PyInt constructor for all supported number types (i32, u32, i64, u64, isize, usize) \ No newline at end of file diff --git a/newsfragments/4988.fixed.md b/newsfragments/4988.fixed.md deleted file mode 100644 index 1a050b905e3..00000000000 --- a/newsfragments/4988.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Fix `Probe` class naming issue with `#[pymethods]` diff --git a/newsfragments/4992.added.md b/newsfragments/4992.added.md deleted file mode 100644 index 36cb9a4e08e..00000000000 --- a/newsfragments/4992.added.md +++ /dev/null @@ -1 +0,0 @@ -Add `pyo3::sync::with_critical_section2` binding diff --git a/newsfragments/5002.fixed.md b/newsfragments/5002.fixed.md deleted file mode 100644 index ad9b0091a79..00000000000 --- a/newsfragments/5002.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Fix compile failure with required `#[pyfunction]` arguments taking `Option<&str>` and `Option<&T>` (for `#[pyclass]` types). diff --git a/newsfragments/5008.fixed.md b/newsfragments/5008.fixed.md deleted file mode 100644 index 6b431d6e437..00000000000 --- a/newsfragments/5008.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Fix `PyString::from_object`, avoid out of bounds reads by null terminating the `encoding` and `errors` parameters \ No newline at end of file diff --git a/newsfragments/5013.added.md b/newsfragments/5013.added.md deleted file mode 100644 index 0becc3ee1e9..00000000000 --- a/newsfragments/5013.added.md +++ /dev/null @@ -1 +0,0 @@ -Implement `PyCallArgs` for `Borrowed<'_, 'py, PyTuple>`, `&Bound<'py, PyTuple>`, and `&Py`. diff --git a/newsfragments/5015.fixed.md b/newsfragments/5015.fixed.md deleted file mode 100644 index b1da048828f..00000000000 --- a/newsfragments/5015.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Fixes compile error if more options followed after `crate` for `#[pyfunction]`. \ No newline at end of file diff --git a/pyo3-build-config/Cargo.toml b/pyo3-build-config/Cargo.toml index b8030cc4304..5abb24e86d6 100644 --- a/pyo3-build-config/Cargo.toml +++ b/pyo3-build-config/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3-build-config" -version = "0.24.0" +version = "0.24.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 16c8a3374cd..de7ce8b317a 100644 --- a/pyo3-ffi/Cargo.toml +++ b/pyo3-ffi/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3-ffi" -version = "0.24.0" +version = "0.24.1" description = "Python-API bindings for the PyO3 ecosystem" authors = ["PyO3 Project and Contributors "] keywords = ["pyo3", "python", "cpython", "ffi"] @@ -43,7 +43,7 @@ generate-import-lib = ["pyo3-build-config/python3-dll-a"] paste = "1" [build-dependencies] -pyo3-build-config = { path = "../pyo3-build-config", version = "=0.24.0", features = ["resolve-config"] } +pyo3-build-config = { path = "../pyo3-build-config", version = "=0.24.1", features = ["resolve-config"] } [lints] workspace = true diff --git a/pyo3-ffi/README.md b/pyo3-ffi/README.md index 3fada2ffab6..192b33a92be 100644 --- a/pyo3-ffi/README.md +++ b/pyo3-ffi/README.md @@ -41,13 +41,13 @@ name = "string_sum" crate-type = ["cdylib"] [dependencies.pyo3-ffi] -version = "0.24.0" +version = "0.24.1" features = ["extension-module"] [build-dependencies] # This is only necessary if you need to configure your build based on # the Python version or the compile-time configuration for the interpreter. -pyo3_build_config = "0.24.0" +pyo3_build_config = "0.24.1" ``` If you need to use conditional compilation based on Python version or how diff --git a/pyo3-macros-backend/Cargo.toml b/pyo3-macros-backend/Cargo.toml index 91e0009f9f6..f3de7abf71f 100644 --- a/pyo3-macros-backend/Cargo.toml +++ b/pyo3-macros-backend/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3-macros-backend" -version = "0.24.0" +version = "0.24.1" description = "Code generation for PyO3 package" authors = ["PyO3 Project and Contributors "] keywords = ["pyo3", "python", "cpython", "ffi"] @@ -17,7 +17,7 @@ rust-version = "1.63" [dependencies] heck = "0.5" proc-macro2 = { version = "1.0.60", default-features = false } -pyo3-build-config = { path = "../pyo3-build-config", version = "=0.24.0", features = ["resolve-config"] } +pyo3-build-config = { path = "../pyo3-build-config", version = "=0.24.1", features = ["resolve-config"] } quote = { version = "1", default-features = false } [dependencies.syn] @@ -26,7 +26,7 @@ default-features = false features = ["derive", "parsing", "printing", "clone-impls", "full", "extra-traits"] [build-dependencies] -pyo3-build-config = { path = "../pyo3-build-config", version = "=0.24.0" } +pyo3-build-config = { path = "../pyo3-build-config", version = "=0.24.1" } [lints] workspace = true diff --git a/pyo3-macros/Cargo.toml b/pyo3-macros/Cargo.toml index 25821b84e81..c089ce53215 100644 --- a/pyo3-macros/Cargo.toml +++ b/pyo3-macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3-macros" -version = "0.24.0" +version = "0.24.1" description = "Proc macros for PyO3 package" authors = ["PyO3 Project and Contributors "] keywords = ["pyo3", "python", "cpython", "ffi"] @@ -22,7 +22,7 @@ experimental-async = ["pyo3-macros-backend/experimental-async"] proc-macro2 = { version = "1.0.60", default-features = false } quote = "1" syn = { version = "2", features = ["full", "extra-traits"] } -pyo3-macros-backend = { path = "../pyo3-macros-backend", version = "=0.24.0" } +pyo3-macros-backend = { path = "../pyo3-macros-backend", version = "=0.24.1" } [lints] workspace = true diff --git a/pyproject.toml b/pyproject.toml index d757c927f4f..84f250e7863 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ [tool.towncrier] filename = "CHANGELOG.md" -version = "0.24.0" +version = "0.24.1" start_string = "\n" template = ".towncrier.template.md" title_format = "## [{version}] - {project_date}" diff --git a/tests/ui/reject_generics.stderr b/tests/ui/reject_generics.stderr index 9cb7e6e2068..732ac4c34e1 100644 --- a/tests/ui/reject_generics.stderr +++ b/tests/ui/reject_generics.stderr @@ -1,10 +1,10 @@ -error: #[pyclass] cannot have generic parameters. For an explanation, see https://pyo3.rs/v0.24.0/class.html#no-generic-parameters +error: #[pyclass] cannot have generic parameters. For an explanation, see https://pyo3.rs/v0.24.1/class.html#no-generic-parameters --> tests/ui/reject_generics.rs:4:25 | 4 | struct ClassWithGenerics { | ^ -error: #[pyclass] cannot have lifetime parameters. For an explanation, see https://pyo3.rs/v0.24.0/class.html#no-lifetime-parameters +error: #[pyclass] cannot have lifetime parameters. For an explanation, see https://pyo3.rs/v0.24.1/class.html#no-lifetime-parameters --> tests/ui/reject_generics.rs:9:27 | 9 | struct ClassWithLifetimes<'a> {
Click to expand -The `IntoPyDict::into_py_dict_bound` method has been renamed to `IntoPyDict::into_py_dict`. If you implemented `IntoPyDict` for your type, you should implement `into_py_dict` instead of `into_py_dict_bound`. The old name is still available but deprecated. +The `IntoPyDict::into_py_dict_bound` method has been renamed to `IntoPyDict::into_py_dict` and is now fallible. If you implemented `IntoPyDict` for your type, you should implement `into_py_dict` instead of `into_py_dict_bound`. The old name is still available but deprecated. Before: ```rust,ignore # use pyo3::prelude::*; # use pyo3::types::{PyDict, IntoPyDict}; -# use pyo3::types::dict::PyDictItem; -impl IntoPyDict for I +# use std::collections::HashMap; + +struct MyMap(HashMap); + +impl IntoPyDict for MyMap where - T: PyDictItem, - I: IntoIterator, + K: ToPyObject, + V: ToPyObject, { fn into_py_dict_bound(self, py: Python<'_>) -> Bound<'_, PyDict> { - let dict = PyDict::new(py); - for item in self { - dict.set_item(item.key(), item.value()) + let dict = PyDict::new_bound(py); + for (key, value) in self.0 { + dict.set_item(key, value) .expect("Failed to set_item on dict"); } dict @@ -71,22 +74,25 @@ where After: -```rust,ignore +```rust # use pyo3::prelude::*; # use pyo3::types::{PyDict, IntoPyDict}; -# use pyo3::types::dict::PyDictItem; -impl IntoPyDict for I +# use std::collections::HashMap; + +# #[allow(dead_code)] +# struct MyMap(HashMap); + +impl<'py, K, V> IntoPyDict<'py> for MyMap where - T: PyDictItem, - I: IntoIterator, + K: IntoPyObject<'py>, + V: IntoPyObject<'py>, { - fn into_py_dict(self, py: Python<'_>) -> Bound<'_, PyDict> { + fn into_py_dict(self, py: Python<'py>) -> PyResult> { let dict = PyDict::new(py); - for item in self { - dict.set_item(item.key(), item.value()) - .expect("Failed to set_item on dict"); + for (key, value) in self.0 { + dict.set_item(key, value)?; } - dict + Ok(dict) } } ``` diff --git a/guide/src/module.md b/guide/src/module.md index 1b2aa49d8c9..1e274c7c953 100644 --- a/guide/src/module.md +++ b/guide/src/module.md @@ -90,7 +90,7 @@ fn func() -> String { # use pyo3::types::IntoPyDict; # use pyo3::ffi::c_str; # let parent_module = wrap_pymodule!(parent_module)(py); -# let ctx = [("parent_module", parent_module)].into_py_dict(py); +# let ctx = [("parent_module", parent_module)].into_py_dict(py).unwrap(); # # py.run(c_str!("assert parent_module.child_module.func() == 'func'"), None, Some(&ctx)).unwrap(); # }) diff --git a/guide/src/python-from-rust/calling-existing-code.md b/guide/src/python-from-rust/calling-existing-code.md index 812fb20e7ae..4eba33f11c3 100644 --- a/guide/src/python-from-rust/calling-existing-code.md +++ b/guide/src/python-from-rust/calling-existing-code.md @@ -131,7 +131,7 @@ def leaky_relu(x, slope=0.01): let relu_result: f64 = activators.getattr("relu")?.call1((-1.0,))?.extract()?; assert_eq!(relu_result, 0.0); - let kwargs = [("slope", 0.2)].into_py_dict(py); + let kwargs = [("slope", 0.2)].into_py_dict(py)?; let lrelu_result: f64 = activators .getattr("leaky_relu")? .call((-1.0,), Some(&kwargs))? diff --git a/guide/src/python-from-rust/function-calls.md b/guide/src/python-from-rust/function-calls.md index f1eb8025431..2e5cf3c589e 100644 --- a/guide/src/python-from-rust/function-calls.md +++ b/guide/src/python-from-rust/function-calls.md @@ -90,17 +90,17 @@ fn main() -> PyResult<()> { .into(); // call object with PyDict - let kwargs = [(key1, val1)].into_py_dict(py); + let kwargs = [(key1, val1)].into_py_dict(py)?; fun.call(py, (), Some(&kwargs))?; // pass arguments as Vec let kwargs = vec![(key1, val1), (key2, val2)]; - fun.call(py, (), Some(&kwargs.into_py_dict(py)))?; + fun.call(py, (), Some(&kwargs.into_py_dict(py)?))?; // pass arguments as HashMap let mut kwargs = HashMap::<&str, i32>::new(); kwargs.insert(key1, 1); - fun.call(py, (), Some(&kwargs.into_py_dict(py)))?; + fun.call(py, (), Some(&kwargs.into_py_dict(py)?))?; Ok(()) }) diff --git a/newsfragments/4493.changed.md b/newsfragments/4493.changed.md new file mode 100644 index 00000000000..efff3b6fa6e --- /dev/null +++ b/newsfragments/4493.changed.md @@ -0,0 +1 @@ +`IntoPyDict::into_py_dict` is now fallible due to `IntoPyObject` migration. \ No newline at end of file diff --git a/src/conversions/anyhow.rs b/src/conversions/anyhow.rs index 9e27985c152..d2cb3f3eb60 100644 --- a/src/conversions/anyhow.rs +++ b/src/conversions/anyhow.rs @@ -145,7 +145,7 @@ mod test_anyhow { let pyerr = PyErr::from(err); Python::with_gil(|py| { - let locals = [("err", pyerr)].into_py_dict(py); + let locals = [("err", pyerr)].into_py_dict(py).unwrap(); let pyerr = py .run(ffi::c_str!("raise err"), None, Some(&locals)) .unwrap_err(); @@ -164,7 +164,7 @@ mod test_anyhow { let pyerr = PyErr::from(err); Python::with_gil(|py| { - let locals = [("err", pyerr)].into_py_dict(py); + let locals = [("err", pyerr)].into_py_dict(py).unwrap(); let pyerr = py .run(ffi::c_str!("raise err"), None, Some(&locals)) .unwrap_err(); diff --git a/src/conversions/chrono.rs b/src/conversions/chrono.rs index ddd4e39d9e3..7f36961b9ea 100644 --- a/src/conversions/chrono.rs +++ b/src/conversions/chrono.rs @@ -1365,7 +1365,7 @@ mod tests { fn test_pyo3_offset_fixed_frompyobject_created_in_python(timestamp in 0..(i32::MAX as i64), timedelta in -86399i32..=86399i32) { Python::with_gil(|py| { - let globals = [("datetime", py.import("datetime").unwrap())].into_py_dict(py); + let globals = [("datetime", py.import("datetime").unwrap())].into_py_dict(py).unwrap(); let code = format!("datetime.datetime.fromtimestamp({}).replace(tzinfo=datetime.timezone(datetime.timedelta(seconds={})))", timestamp, timedelta); let t = py.eval(&CString::new(code).unwrap(), Some(&globals), None).unwrap(); diff --git a/src/conversions/eyre.rs b/src/conversions/eyre.rs index 5bfaddcbdc6..42d7a12c872 100644 --- a/src/conversions/eyre.rs +++ b/src/conversions/eyre.rs @@ -151,7 +151,7 @@ mod tests { let pyerr = PyErr::from(err); Python::with_gil(|py| { - let locals = [("err", pyerr)].into_py_dict(py); + let locals = [("err", pyerr)].into_py_dict(py).unwrap(); let pyerr = py .run(ffi::c_str!("raise err"), None, Some(&locals)) .unwrap_err(); @@ -170,7 +170,7 @@ mod tests { let pyerr = PyErr::from(err); Python::with_gil(|py| { - let locals = [("err", pyerr)].into_py_dict(py); + let locals = [("err", pyerr)].into_py_dict(py).unwrap(); let pyerr = py .run(ffi::c_str!("raise err"), None, Some(&locals)) .unwrap_err(); diff --git a/src/conversions/hashbrown.rs b/src/conversions/hashbrown.rs index a3fd61a6412..db42075adeb 100644 --- a/src/conversions/hashbrown.rs +++ b/src/conversions/hashbrown.rs @@ -23,9 +23,9 @@ use crate::{ dict::PyDictMethods, frozenset::PyFrozenSetMethods, set::{new_from_iter, try_new_from_iter, PySetMethods}, - IntoPyDict, PyDict, PyFrozenSet, PySet, + PyDict, PyFrozenSet, PySet, }, - Bound, BoundObject, FromPyObject, IntoPy, PyAny, PyErr, PyObject, PyResult, Python, ToPyObject, + Bound, FromPyObject, IntoPy, PyAny, PyErr, PyObject, PyResult, Python, ToPyObject, }; use std::{cmp, hash}; @@ -36,7 +36,11 @@ where H: hash::BuildHasher, { fn to_object(&self, py: Python<'_>) -> PyObject { - IntoPyDict::into_py_dict(self, py).into() + let dict = PyDict::new(py); + for (k, v) in self { + dict.set_item(k.to_object(py), v.to_object(py)).unwrap(); + } + dict.into_any().unbind() } } @@ -47,10 +51,11 @@ where H: hash::BuildHasher, { fn into_py(self, py: Python<'_>) -> PyObject { - let iter = self - .into_iter() - .map(|(k, v)| (k.into_py(py), v.into_py(py))); - IntoPyDict::into_py_dict(iter, py).into() + let dict = PyDict::new(py); + for (k, v) in self { + dict.set_item(k.into_py(py), v.into_py(py)).unwrap(); + } + dict.into_any().unbind() } } @@ -67,10 +72,7 @@ where fn into_pyobject(self, py: Python<'py>) -> Result { let dict = PyDict::new(py); for (k, v) in self { - dict.set_item( - k.into_pyobject(py).map_err(Into::into)?.into_bound(), - v.into_pyobject(py).map_err(Into::into)?.into_bound(), - )?; + dict.set_item(k, v)?; } Ok(dict) } @@ -89,10 +91,7 @@ where fn into_pyobject(self, py: Python<'py>) -> Result { let dict = PyDict::new(py); for (k, v) in self { - dict.set_item( - k.into_pyobject(py).map_err(Into::into)?.into_bound(), - v.into_pyobject(py).map_err(Into::into)?.into_bound(), - )?; + dict.set_item(k, v)?; } Ok(dict) } @@ -187,6 +186,7 @@ where #[cfg(test)] mod tests { use super::*; + use crate::types::IntoPyDict; #[test] fn test_hashbrown_hashmap_to_python() { @@ -238,7 +238,7 @@ mod tests { let mut map = hashbrown::HashMap::::new(); map.insert(1, 1); - let py_map = map.into_py_dict(py); + let py_map = map.into_py_dict(py).unwrap(); assert_eq!(py_map.len(), 1); assert_eq!( diff --git a/src/conversions/indexmap.rs b/src/conversions/indexmap.rs index 03aba0e0fc3..acb466c3d0a 100644 --- a/src/conversions/indexmap.rs +++ b/src/conversions/indexmap.rs @@ -89,7 +89,7 @@ use crate::conversion::IntoPyObject; use crate::types::*; -use crate::{Bound, BoundObject, FromPyObject, IntoPy, PyErr, PyObject, Python, ToPyObject}; +use crate::{Bound, FromPyObject, IntoPy, PyErr, PyObject, Python, ToPyObject}; use std::{cmp, hash}; impl ToPyObject for indexmap::IndexMap @@ -99,7 +99,11 @@ where H: hash::BuildHasher, { fn to_object(&self, py: Python<'_>) -> PyObject { - IntoPyDict::into_py_dict(self, py).into() + let dict = PyDict::new(py); + for (k, v) in self { + dict.set_item(k.to_object(py), v.to_object(py)).unwrap(); + } + dict.into_any().unbind() } } @@ -110,10 +114,11 @@ where H: hash::BuildHasher, { fn into_py(self, py: Python<'_>) -> PyObject { - let iter = self - .into_iter() - .map(|(k, v)| (k.into_py(py), v.into_py(py))); - IntoPyDict::into_py_dict(iter, py).into() + let dict = PyDict::new(py); + for (k, v) in self { + dict.set_item(k.into_py(py), v.into_py(py)).unwrap(); + } + dict.into_any().unbind() } } @@ -130,10 +135,7 @@ where fn into_pyobject(self, py: Python<'py>) -> Result { let dict = PyDict::new(py); for (k, v) in self { - dict.set_item( - k.into_pyobject(py).map_err(Into::into)?.into_bound(), - v.into_pyobject(py).map_err(Into::into)?.into_bound(), - )?; + dict.set_item(k, v)?; } Ok(dict) } @@ -152,10 +154,7 @@ where fn into_pyobject(self, py: Python<'py>) -> Result { let dict = PyDict::new(py); for (k, v) in self { - dict.set_item( - k.into_pyobject(py).map_err(Into::into)?.into_bound(), - v.into_pyobject(py).map_err(Into::into)?.into_bound(), - )?; + dict.set_item(k, v)?; } Ok(dict) } @@ -237,7 +236,7 @@ mod test_indexmap { let mut map = indexmap::IndexMap::::new(); map.insert(1, 1); - let py_map = map.into_py_dict(py); + let py_map = map.into_py_dict(py).unwrap(); assert_eq!(py_map.len(), 1); assert_eq!( @@ -266,7 +265,7 @@ mod test_indexmap { } } - let py_map = map.clone().into_py_dict(py); + let py_map = (&map).into_py_dict(py).unwrap(); let trip_map = py_map.extract::>().unwrap(); diff --git a/src/conversions/std/map.rs b/src/conversions/std/map.rs index 4aba5066ffe..582c56b613f 100644 --- a/src/conversions/std/map.rs +++ b/src/conversions/std/map.rs @@ -5,8 +5,8 @@ use crate::inspect::types::TypeInfo; use crate::{ conversion::IntoPyObject, instance::Bound, - types::{any::PyAnyMethods, dict::PyDictMethods, IntoPyDict, PyDict}, - BoundObject, FromPyObject, IntoPy, PyAny, PyErr, PyObject, Python, ToPyObject, + types::{any::PyAnyMethods, dict::PyDictMethods, PyDict}, + FromPyObject, IntoPy, PyAny, PyErr, PyObject, Python, ToPyObject, }; impl ToPyObject for collections::HashMap @@ -16,7 +16,11 @@ where H: hash::BuildHasher, { fn to_object(&self, py: Python<'_>) -> PyObject { - IntoPyDict::into_py_dict(self, py).into() + let dict = PyDict::new(py); + for (k, v) in self { + dict.set_item(k.to_object(py), v.to_object(py)).unwrap(); + } + dict.into_any().unbind() } } @@ -26,7 +30,11 @@ where V: ToPyObject, { fn to_object(&self, py: Python<'_>) -> PyObject { - IntoPyDict::into_py_dict(self, py).into() + let dict = PyDict::new(py); + for (k, v) in self { + dict.set_item(k.to_object(py), v.to_object(py)).unwrap(); + } + dict.into_any().unbind() } } @@ -37,10 +45,11 @@ where H: hash::BuildHasher, { fn into_py(self, py: Python<'_>) -> PyObject { - let iter = self - .into_iter() - .map(|(k, v)| (k.into_py(py), v.into_py(py))); - IntoPyDict::into_py_dict(iter, py).into() + let dict = PyDict::new(py); + for (k, v) in self { + dict.set_item(k.into_py(py), v.into_py(py)).unwrap(); + } + dict.into_any().unbind() } #[cfg(feature = "experimental-inspect")] @@ -62,10 +71,7 @@ where fn into_pyobject(self, py: Python<'py>) -> Result { let dict = PyDict::new(py); for (k, v) in self { - dict.set_item( - k.into_pyobject(py).map_err(Into::into)?.into_bound(), - v.into_pyobject(py).map_err(Into::into)?.into_bound(), - )?; + dict.set_item(k, v)?; } Ok(dict) } @@ -84,10 +90,7 @@ where fn into_pyobject(self, py: Python<'py>) -> Result { let dict = PyDict::new(py); for (k, v) in self { - dict.set_item( - k.into_pyobject(py).map_err(Into::into)?.into_bound(), - v.into_pyobject(py).map_err(Into::into)?.into_bound(), - )?; + dict.set_item(k, v)?; } Ok(dict) } @@ -99,10 +102,11 @@ where V: IntoPy, { fn into_py(self, py: Python<'_>) -> PyObject { - let iter = self - .into_iter() - .map(|(k, v)| (k.into_py(py), v.into_py(py))); - IntoPyDict::into_py_dict(iter, py).into() + let dict = PyDict::new(py); + for (k, v) in self { + dict.set_item(k.into_py(py), v.into_py(py)).unwrap(); + } + dict.into_any().unbind() } #[cfg(feature = "experimental-inspect")] @@ -123,10 +127,7 @@ where fn into_pyobject(self, py: Python<'py>) -> Result { let dict = PyDict::new(py); for (k, v) in self { - dict.set_item( - k.into_pyobject(py).map_err(Into::into)?.into_bound(), - v.into_pyobject(py).map_err(Into::into)?.into_bound(), - )?; + dict.set_item(k, v)?; } Ok(dict) } @@ -144,10 +145,7 @@ where fn into_pyobject(self, py: Python<'py>) -> Result { let dict = PyDict::new(py); for (k, v) in self { - dict.set_item( - k.into_pyobject(py).map_err(Into::into)?.into_bound(), - v.into_pyobject(py).map_err(Into::into)?.into_bound(), - )?; + dict.set_item(k, v)?; } Ok(dict) } diff --git a/src/exceptions.rs b/src/exceptions.rs index 4239fae0e41..6f0fa3e674c 100644 --- a/src/exceptions.rs +++ b/src/exceptions.rs @@ -61,10 +61,13 @@ macro_rules! impl_exception_boilerplate_bound { /// /// import_exception!(socket, gaierror); /// +/// # fn main() -> pyo3::PyResult<()> { /// Python::with_gil(|py| { -/// let ctx = [("gaierror", py.get_type::())].into_py_dict(py); +/// let ctx = [("gaierror", py.get_type::())].into_py_dict(py)?; /// pyo3::py_run!(py, *ctx, "import socket; assert gaierror is socket.gaierror"); -/// }); +/// # Ok(()) +/// }) +/// # } /// /// ``` #[macro_export] @@ -905,7 +908,7 @@ mod tests { Python::with_gil(|py| { let error_type = py.get_type::(); - let ctx = [("CustomError", error_type)].into_py_dict(py); + let ctx = [("CustomError", error_type)].into_py_dict(py).unwrap(); let type_description: String = py .eval(ffi::c_str!("str(CustomError)"), None, Some(&ctx)) .unwrap() @@ -932,7 +935,7 @@ mod tests { create_exception!(mymodule.exceptions, CustomError, PyException); Python::with_gil(|py| { let error_type = py.get_type::(); - let ctx = [("CustomError", error_type)].into_py_dict(py); + let ctx = [("CustomError", error_type)].into_py_dict(py).unwrap(); let type_description: String = py .eval(ffi::c_str!("str(CustomError)"), None, Some(&ctx)) .unwrap() @@ -951,7 +954,7 @@ mod tests { Python::with_gil(|py| { let error_type = py.get_type::(); - let ctx = [("CustomError", error_type)].into_py_dict(py); + let ctx = [("CustomError", error_type)].into_py_dict(py).unwrap(); let type_description: String = py .eval(ffi::c_str!("str(CustomError)"), None, Some(&ctx)) .unwrap() @@ -984,7 +987,7 @@ mod tests { Python::with_gil(|py| { let error_type = py.get_type::(); - let ctx = [("CustomError", error_type)].into_py_dict(py); + let ctx = [("CustomError", error_type)].into_py_dict(py).unwrap(); let type_description: String = py .eval(ffi::c_str!("str(CustomError)"), None, Some(&ctx)) .unwrap() diff --git a/src/impl_/extract_argument.rs b/src/impl_/extract_argument.rs index 098722060c6..cbf79a14707 100644 --- a/src/impl_/extract_argument.rs +++ b/src/impl_/extract_argument.rs @@ -802,7 +802,7 @@ mod tests { Python::with_gil(|py| { let args = PyTuple::empty(py); - let kwargs = [("foo", 0u8)].into_py_dict(py); + let kwargs = [("foo", 0u8)].into_py_dict(py).unwrap(); let err = unsafe { function_description .extract_arguments_tuple_dict::( @@ -833,7 +833,7 @@ mod tests { Python::with_gil(|py| { let args = PyTuple::empty(py); - let kwargs = [(1u8, 1u8)].into_py_dict(py); + let kwargs = [(1u8, 1u8)].into_py_dict(py).unwrap(); let err = unsafe { function_description .extract_arguments_tuple_dict::( diff --git a/src/instance.rs b/src/instance.rs index 2b19fd10c43..1e15624929e 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -1960,7 +1960,7 @@ mod tests { assert_repr(obj.call1(py, ((('x', 1),),)).unwrap().bind(py), "{'x': 1}"); assert_repr( - obj.call(py, (), Some(&[('x', 1)].into_py_dict(py))) + obj.call(py, (), Some(&[('x', 1)].into_py_dict(py).unwrap())) .unwrap() .bind(py), "{'x': 1}", diff --git a/src/lib.rs b/src/lib.rs index 8181afb4347..8144ef740e0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -242,7 +242,7 @@ //! let sys = py.import("sys")?; //! let version: String = sys.getattr("version")?.extract()?; //! -//! let locals = [("os", py.import("os")?)].into_py_dict(py); +//! let locals = [("os", py.import("os")?)].into_py_dict(py)?; //! let code = c_str!("os.getenv('USER') or os.getenv('USERNAME') or 'Unknown'"); //! let user: String = py.eval(code, None, Some(&locals))?.extract()?; //! diff --git a/src/macros.rs b/src/macros.rs index 6148d9662c5..936dbd43af0 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -75,10 +75,13 @@ /// } /// } /// +/// # fn main() -> PyResult<()> { /// Python::with_gil(|py| { -/// let locals = [("C", py.get_type::())].into_py_dict(py); +/// let locals = [("C", py.get_type::())].into_py_dict(py)?; /// pyo3::py_run!(py, *locals, "c = C()"); -/// }); +/// # Ok(()) +/// }) +/// # } /// ``` #[macro_export] macro_rules! py_run { @@ -102,7 +105,7 @@ macro_rules! py_run_impl { ($py:expr, $($val:ident)+, $code:expr) => {{ use $crate::types::IntoPyDict; use $crate::ToPyObject; - let d = [$((stringify!($val), $val.to_object($py)),)+].into_py_dict($py); + let d = [$((stringify!($val), $val.to_object($py)),)+].into_py_dict($py).unwrap(); $crate::py_run_impl!($py, *d, $code) }}; ($py:expr, *$dict:expr, $code:expr) => {{ diff --git a/src/marker.rs b/src/marker.rs index 92a154b3694..a5ba0436287 100644 --- a/src/marker.rs +++ b/src/marker.rs @@ -872,7 +872,7 @@ mod tests { .unwrap(); assert_eq!(v, 1); - let d = [("foo", 13)].into_py_dict(py); + let d = [("foo", 13)].into_py_dict(py).unwrap(); // Inject our own global namespace let v: i32 = py diff --git a/src/tests/common.rs b/src/tests/common.rs index 4e4d7fe98ee..caa4120b720 100644 --- a/src/tests/common.rs +++ b/src/tests/common.rs @@ -40,7 +40,7 @@ mod inner { // Case1: idents & no err_msg ($py:expr, $($val:ident)+, $code:expr, $err:ident) => {{ use pyo3::types::IntoPyDict; - let d = [$((stringify!($val), $val.to_object($py)),)+].into_py_dict($py); + let d = [$((stringify!($val), $val.to_object($py)),)+].into_py_dict($py).unwrap(); py_expect_exception!($py, *d, $code, $err) }}; // Case2: dict & no err_msg @@ -126,7 +126,7 @@ mod inner { f: impl FnOnce(&Bound<'py, PyList>) -> PyResult, ) -> PyResult { let warnings = py.import("warnings")?; - let kwargs = [("record", true)].into_py_dict(py); + let kwargs = [("record", true)].into_py_dict(py)?; let catch_warnings = warnings .getattr("catch_warnings")? .call((), Some(&kwargs))?; diff --git a/src/types/any.rs b/src/types/any.rs index 37b79720730..409630e12b2 100644 --- a/src/types/any.rs +++ b/src/types/any.rs @@ -1701,7 +1701,7 @@ class NonHeapNonDescriptorInt: fn test_call_with_kwargs() { Python::with_gil(|py| { let list = vec![3, 6, 5, 4, 7].to_object(py); - let dict = vec![("reverse", true)].into_py_dict(py); + let dict = vec![("reverse", true)].into_py_dict(py).unwrap(); list.call_method(py, "sort", (), Some(&dict)).unwrap(); assert_eq!(list.extract::>(py).unwrap(), vec![7, 6, 5, 4, 3]); }); diff --git a/src/types/dict.rs b/src/types/dict.rs index 2136158e89e..c4de7bae46a 100644 --- a/src/types/dict.rs +++ b/src/types/dict.rs @@ -6,7 +6,7 @@ use crate::instance::{Borrowed, Bound}; use crate::py_result_ext::PyResultExt; use crate::types::any::PyAnyMethods; use crate::types::{PyAny, PyList}; -use crate::{ffi, Python, ToPyObject}; +use crate::{ffi, BoundObject, Python}; /// Represents a Python `dict`. /// @@ -130,7 +130,7 @@ pub trait PyDictMethods<'py>: crate::sealed::Sealed { /// This is equivalent to the Python expression `key in self`. fn contains(&self, key: K) -> PyResult where - K: ToPyObject; + K: IntoPyObject<'py>; /// Gets an item from the dictionary. /// @@ -139,22 +139,22 @@ pub trait PyDictMethods<'py>: crate::sealed::Sealed { /// To get a `KeyError` for non-existing keys, use `PyAny::get_item`. fn get_item(&self, key: K) -> PyResult>> where - K: ToPyObject; + K: IntoPyObject<'py>; /// Sets an item value. /// /// This is equivalent to the Python statement `self[key] = value`. fn set_item(&self, key: K, value: V) -> PyResult<()> where - K: ToPyObject, - V: ToPyObject; + K: IntoPyObject<'py>, + V: IntoPyObject<'py>; /// Deletes an item. /// /// This is equivalent to the Python statement `del self[key]`. fn del_item(&self, key: K) -> PyResult<()> where - K: ToPyObject; + K: IntoPyObject<'py>; /// Returns a list of dict keys. /// @@ -226,9 +226,9 @@ impl<'py> PyDictMethods<'py> for Bound<'py, PyDict> { fn contains(&self, key: K) -> PyResult where - K: ToPyObject, + K: IntoPyObject<'py>, { - fn inner(dict: &Bound<'_, PyDict>, key: Bound<'_, PyAny>) -> PyResult { + fn inner(dict: &Bound<'_, PyDict>, key: &Bound<'_, PyAny>) -> PyResult { match unsafe { ffi::PyDict_Contains(dict.as_ptr(), key.as_ptr()) } { 1 => Ok(true), 0 => Ok(false), @@ -237,16 +237,22 @@ impl<'py> PyDictMethods<'py> for Bound<'py, PyDict> { } let py = self.py(); - inner(self, key.to_object(py).into_bound(py)) + inner( + self, + &key.into_pyobject(py) + .map_err(Into::into)? + .into_any() + .as_borrowed(), + ) } fn get_item(&self, key: K) -> PyResult>> where - K: ToPyObject, + K: IntoPyObject<'py>, { fn inner<'py>( dict: &Bound<'py, PyDict>, - key: Bound<'_, PyAny>, + key: &Bound<'_, PyAny>, ) -> PyResult>> { let py = dict.py(); let mut result: *mut ffi::PyObject = std::ptr::null_mut(); @@ -260,18 +266,24 @@ impl<'py> PyDictMethods<'py> for Bound<'py, PyDict> { } let py = self.py(); - inner(self, key.to_object(py).into_bound(py)) + inner( + self, + &key.into_pyobject(py) + .map_err(Into::into)? + .into_any() + .as_borrowed(), + ) } fn set_item(&self, key: K, value: V) -> PyResult<()> where - K: ToPyObject, - V: ToPyObject, + K: IntoPyObject<'py>, + V: IntoPyObject<'py>, { fn inner( dict: &Bound<'_, PyDict>, - key: Bound<'_, PyAny>, - value: Bound<'_, PyAny>, + key: &Bound<'_, PyAny>, + value: &Bound<'_, PyAny>, ) -> PyResult<()> { err::error_on_minusone(dict.py(), unsafe { ffi::PyDict_SetItem(dict.as_ptr(), key.as_ptr(), value.as_ptr()) @@ -281,23 +293,36 @@ impl<'py> PyDictMethods<'py> for Bound<'py, PyDict> { let py = self.py(); inner( self, - key.to_object(py).into_bound(py), - value.to_object(py).into_bound(py), + &key.into_pyobject(py) + .map_err(Into::into)? + .into_any() + .as_borrowed(), + &value + .into_pyobject(py) + .map_err(Into::into)? + .into_any() + .as_borrowed(), ) } fn del_item(&self, key: K) -> PyResult<()> where - K: ToPyObject, + K: IntoPyObject<'py>, { - fn inner(dict: &Bound<'_, PyDict>, key: Bound<'_, PyAny>) -> PyResult<()> { + fn inner(dict: &Bound<'_, PyDict>, key: &Bound<'_, PyAny>) -> PyResult<()> { err::error_on_minusone(dict.py(), unsafe { ffi::PyDict_DelItem(dict.as_ptr(), key.as_ptr()) }) } let py = self.py(); - inner(self, key.to_object(py).into_bound(py)) + inner( + self, + &key.into_pyobject(py) + .map_err(Into::into)? + .into_any() + .as_borrowed(), + ) } fn keys(&self) -> Bound<'py, PyList> { @@ -529,73 +554,69 @@ mod borrowed_iter { } } +use crate::prelude::IntoPyObject; pub(crate) use borrowed_iter::BorrowedDictIter; /// Conversion trait that allows a sequence of tuples to be converted into `PyDict` /// Primary use case for this trait is `call` and `call_method` methods as keywords argument. -pub trait IntoPyDict: Sized { +pub trait IntoPyDict<'py>: Sized { /// Converts self into a `PyDict` object pointer. Whether pointer owned or borrowed /// depends on implementation. - fn into_py_dict(self, py: Python<'_>) -> Bound<'_, PyDict>; + fn into_py_dict(self, py: Python<'py>) -> PyResult>; /// Deprecated name for [`IntoPyDict::into_py_dict`]. #[deprecated(since = "0.23.0", note = "renamed to `IntoPyDict::into_py_dict`")] #[inline] - fn into_py_dict_bound(self, py: Python<'_>) -> Bound<'_, PyDict> { - self.into_py_dict(py) + fn into_py_dict_bound(self, py: Python<'py>) -> Bound<'py, PyDict> { + self.into_py_dict(py).unwrap() } } -impl IntoPyDict for I +impl<'py, T, I> IntoPyDict<'py> for I where - T: PyDictItem, + T: PyDictItem<'py>, I: IntoIterator, { - fn into_py_dict(self, py: Python<'_>) -> Bound<'_, PyDict> { + fn into_py_dict(self, py: Python<'py>) -> PyResult> { let dict = PyDict::new(py); for item in self { - dict.set_item(item.key(), item.value()) - .expect("Failed to set_item on dict"); + let (key, value) = item.unpack(); + dict.set_item(key, value)?; } - dict + Ok(dict) } } /// Represents a tuple which can be used as a PyDict item. -pub trait PyDictItem { - type K: ToPyObject; - type V: ToPyObject; - fn key(&self) -> &Self::K; - fn value(&self) -> &Self::V; +trait PyDictItem<'py> { + type K: IntoPyObject<'py>; + type V: IntoPyObject<'py>; + fn unpack(self) -> (Self::K, Self::V); } -impl PyDictItem for (K, V) +impl<'py, K, V> PyDictItem<'py> for (K, V) where - K: ToPyObject, - V: ToPyObject, + K: IntoPyObject<'py>, + V: IntoPyObject<'py>, { type K = K; type V = V; - fn key(&self) -> &Self::K { - &self.0 - } - fn value(&self) -> &Self::V { - &self.1 + + fn unpack(self) -> (Self::K, Self::V) { + (self.0, self.1) } } -impl PyDictItem for &(K, V) +impl<'a, 'py, K, V> PyDictItem<'py> for &'a (K, V) where - K: ToPyObject, - V: ToPyObject, + &'a K: IntoPyObject<'py>, + &'a V: IntoPyObject<'py>, { - type K = K; - type V = V; - fn key(&self) -> &Self::K { - &self.0 - } - fn value(&self) -> &Self::V { - &self.1 + type K = &'a K; + type V = &'a V; + + fn unpack(self) -> (Self::K, Self::V) { + (&self.0, &self.1) } } @@ -603,12 +624,13 @@ where mod tests { use super::*; use crate::types::PyTuple; + use crate::ToPyObject; use std::collections::{BTreeMap, HashMap}; #[test] fn test_new() { Python::with_gil(|py| { - let dict = [(7, 32)].into_py_dict(py); + let dict = [(7, 32)].into_py_dict(py).unwrap(); assert_eq!( 32, dict.get_item(7i32) @@ -668,7 +690,7 @@ mod tests { #[test] fn test_copy() { Python::with_gil(|py| { - let dict = [(7, 32)].into_py_dict(py); + let dict = [(7, 32)].into_py_dict(py).unwrap(); let ndict = dict.copy().unwrap(); assert_eq!( @@ -1065,7 +1087,7 @@ mod tests { let mut map = HashMap::::new(); map.insert(1, 1); - let py_map = map.into_py_dict(py); + let py_map = map.into_py_dict(py).unwrap(); assert_eq!(py_map.len(), 1); assert_eq!( @@ -1086,7 +1108,7 @@ mod tests { let mut map = BTreeMap::::new(); map.insert(1, 1); - let py_map = map.into_py_dict(py); + let py_map = map.into_py_dict(py).unwrap(); assert_eq!(py_map.len(), 1); assert_eq!( @@ -1105,7 +1127,7 @@ mod tests { fn test_vec_into_dict() { Python::with_gil(|py| { let vec = vec![("a", 1), ("b", 2), ("c", 3)]; - let py_map = vec.into_py_dict(py); + let py_map = vec.into_py_dict(py).unwrap(); assert_eq!(py_map.len(), 3); assert_eq!( @@ -1124,7 +1146,7 @@ mod tests { fn test_slice_into_dict() { Python::with_gil(|py| { let arr = [("a", 1), ("b", 2), ("c", 3)]; - let py_map = arr.into_py_dict(py); + let py_map = arr.into_py_dict(py).unwrap(); assert_eq!(py_map.len(), 3); assert_eq!( @@ -1145,7 +1167,7 @@ mod tests { let mut map = HashMap::::new(); map.insert(1, 1); - let py_map = map.into_py_dict(py); + let py_map = map.into_py_dict(py).unwrap(); assert_eq!(py_map.as_mapping().len().unwrap(), 1); assert_eq!( @@ -1166,7 +1188,7 @@ mod tests { let mut map = HashMap::::new(); map.insert(1, 1); - let py_map = map.into_py_dict(py); + let py_map = map.into_py_dict(py).unwrap(); let py_mapping = py_map.into_mapping(); assert_eq!(py_mapping.len().unwrap(), 1); @@ -1180,7 +1202,7 @@ mod tests { map.insert("a", 1); map.insert("b", 2); map.insert("c", 3); - map.into_py_dict(py) + map.into_py_dict(py).unwrap() } #[test] @@ -1216,8 +1238,8 @@ mod tests { #[test] fn dict_update() { Python::with_gil(|py| { - let dict = [("a", 1), ("b", 2), ("c", 3)].into_py_dict(py); - let other = [("b", 4), ("c", 5), ("d", 6)].into_py_dict(py); + let dict = [("a", 1), ("b", 2), ("c", 3)].into_py_dict(py).unwrap(); + let other = [("b", 4), ("c", 5), ("d", 6)].into_py_dict(py).unwrap(); dict.update(other.as_mapping()).unwrap(); assert_eq!(dict.len(), 4); assert_eq!( @@ -1287,8 +1309,8 @@ mod tests { #[test] fn dict_update_if_missing() { Python::with_gil(|py| { - let dict = [("a", 1), ("b", 2), ("c", 3)].into_py_dict(py); - let other = [("b", 4), ("c", 5), ("d", 6)].into_py_dict(py); + let dict = [("a", 1), ("b", 2), ("c", 3)].into_py_dict(py).unwrap(); + let other = [("b", 4), ("c", 5), ("d", 6)].into_py_dict(py).unwrap(); dict.update_if_missing(other.as_mapping()).unwrap(); assert_eq!(dict.len(), 4); assert_eq!( diff --git a/tests/test_buffer_protocol.rs b/tests/test_buffer_protocol.rs index 75549f39718..4d396f9be68 100644 --- a/tests/test_buffer_protocol.rs +++ b/tests/test_buffer_protocol.rs @@ -57,7 +57,7 @@ fn test_buffer() { }, ) .unwrap(); - let env = [("ob", instance)].into_py_dict(py); + let env = [("ob", instance)].into_py_dict(py).unwrap(); py_assert!(py, *env, "bytes(ob) == b' 23'"); }); @@ -122,7 +122,7 @@ fn test_releasebuffer_unraisable_error() { let capture = UnraisableCapture::install(py); let instance = Py::new(py, ReleaseBufferError {}).unwrap(); - let env = [("ob", instance.clone_ref(py))].into_py_dict(py); + let env = [("ob", instance.clone_ref(py))].into_py_dict(py).unwrap(); assert!(capture.borrow(py).capture.is_none()); diff --git a/tests/test_class_basics.rs b/tests/test_class_basics.rs index a6fb89831cc..a48354c47c8 100644 --- a/tests/test_class_basics.rs +++ b/tests/test_class_basics.rs @@ -249,7 +249,8 @@ fn class_with_hash() { ("obj", Py::new(py, class).unwrap().into_any()), ("hsh", hash.into_py(py)), ] - .into_py_dict(py); + .into_py_dict(py) + .unwrap(); py_assert!(py, *env, "hash(obj) == hsh"); }); diff --git a/tests/test_class_new.rs b/tests/test_class_new.rs index fb5ca91db81..75070bd274b 100644 --- a/tests/test_class_new.rs +++ b/tests/test_class_new.rs @@ -29,7 +29,7 @@ fn empty_class_with_new() { // Calling with arbitrary args or kwargs is not ok assert!(typeobj.call(("some", "args"), None).is_err()); assert!(typeobj - .call((), Some(&[("some", "kwarg")].into_py_dict(py))) + .call((), Some(&[("some", "kwarg")].into_py_dict(py).unwrap())) .is_err()); }); } diff --git a/tests/test_coroutine.rs b/tests/test_coroutine.rs index cdf01b8891e..89ab3d64a4b 100644 --- a/tests/test_coroutine.rs +++ b/tests/test_coroutine.rs @@ -72,7 +72,8 @@ fn test_coroutine_qualname() { ("my_fn", wrap_pyfunction!(my_fn, gil).unwrap().as_any()), ("MyClass", gil.get_type::().as_any()), ] - .into_py_dict(gil); + .into_py_dict(gil) + .unwrap(); py_run!(gil, *locals, &handle_windows(test)); }) } @@ -313,7 +314,9 @@ fn test_async_method_receiver() { assert False assert asyncio.run(coro3) == 1 "#; - let locals = [("Counter", gil.get_type::())].into_py_dict(gil); + let locals = [("Counter", gil.get_type::())] + .into_py_dict(gil) + .unwrap(); py_run!(gil, *locals, test); }); @@ -348,7 +351,9 @@ fn test_async_method_receiver_with_other_args() { assert asyncio.run(v.set_value(10)) == 10 assert asyncio.run(v.get_value_plus_with(1, 1)) == 12 "#; - let locals = [("Value", gil.get_type::())].into_py_dict(gil); + let locals = [("Value", gil.get_type::())] + .into_py_dict(gil) + .unwrap(); py_run!(gil, *locals, test); }); } diff --git a/tests/test_datetime.rs b/tests/test_datetime.rs index 2278d9ecc73..b1fad408dc5 100644 --- a/tests/test_datetime.rs +++ b/tests/test_datetime.rs @@ -13,7 +13,9 @@ fn _get_subclasses<'py>( // Import the class from Python and create some subclasses let datetime = py.import("datetime")?; - let locals = [(py_type, datetime.getattr(py_type)?)].into_py_dict(py); + let locals = [(py_type, datetime.getattr(py_type)?)] + .into_py_dict(py) + .unwrap(); let make_subclass_py = CString::new(format!("class Subklass({}):\n pass", py_type))?; @@ -135,7 +137,7 @@ fn test_datetime_utc() { let dt = PyDateTime::new(py, 2018, 1, 1, 0, 0, 0, 0, Some(&utc)).unwrap(); - let locals = [("dt", dt)].into_py_dict(py); + let locals = [("dt", dt)].into_py_dict(py).unwrap(); let offset: f32 = py .eval( diff --git a/tests/test_enum.rs b/tests/test_enum.rs index abe743ee9ca..5e994548edd 100644 --- a/tests/test_enum.rs +++ b/tests/test_enum.rs @@ -259,7 +259,8 @@ fn test_simple_enum_with_hash() { ("obj", Py::new(py, class).unwrap().into_any()), ("hsh", hash.into_py(py)), ] - .into_py_dict(py); + .into_py_dict(py) + .unwrap(); py_assert!(py, *env, "hash(obj) == hsh"); }); @@ -290,7 +291,8 @@ fn test_complex_enum_with_hash() { ("obj", Py::new(py, class).unwrap().into_any()), ("hsh", hash.into_py(py)), ] - .into_py_dict(py); + .into_py_dict(py) + .unwrap(); py_assert!(py, *env, "hash(obj) == hsh"); }); diff --git a/tests/test_getter_setter.rs b/tests/test_getter_setter.rs index 8966471abe2..d8cc3af20e6 100644 --- a/tests/test_getter_setter.rs +++ b/tests/test_getter_setter.rs @@ -85,7 +85,9 @@ fn class_with_properties() { py_run!(py, inst, "inst.from_any = 15"); py_run!(py, inst, "assert inst.get_num() == 15"); - let d = [("C", py.get_type::())].into_py_dict(py); + let d = [("C", py.get_type::())] + .into_py_dict(py) + .unwrap(); py_assert!(py, *d, "C.DATA.__doc__ == 'a getter for data'"); }); } diff --git a/tests/test_inheritance.rs b/tests/test_inheritance.rs index d3980152120..2cce50bba25 100644 --- a/tests/test_inheritance.rs +++ b/tests/test_inheritance.rs @@ -21,7 +21,9 @@ struct SubclassAble {} #[test] fn subclass() { Python::with_gil(|py| { - let d = [("SubclassAble", py.get_type::())].into_py_dict(py); + let d = [("SubclassAble", py.get_type::())] + .into_py_dict(py) + .unwrap(); py.run( ffi::c_str!("class A(SubclassAble): pass\nassert issubclass(A, SubclassAble)"), @@ -98,7 +100,7 @@ fn call_base_and_sub_methods() { fn mutation_fails() { Python::with_gil(|py| { let obj = Py::new(py, SubClass::new()).unwrap(); - let global = [("obj", obj)].into_py_dict(py); + let global = [("obj", obj)].into_py_dict(py).unwrap(); let e = py .run( ffi::c_str!("obj.base_set(lambda: obj.sub_set_and_ret(1))"), @@ -276,7 +278,7 @@ mod inheriting_native_type { fn custom_exception() { Python::with_gil(|py| { let cls = py.get_type::(); - let dict = [("cls", &cls)].into_py_dict(py); + let dict = [("cls", &cls)].into_py_dict(py).unwrap(); let res = py.run( ffi::c_str!("e = cls('hello'); assert str(e) == 'hello'; assert e.context == 'Hello :)'; raise e"), None, diff --git a/tests/test_macro_docs.rs b/tests/test_macro_docs.rs index 964e762886d..42159be9d3c 100644 --- a/tests/test_macro_docs.rs +++ b/tests/test_macro_docs.rs @@ -23,7 +23,9 @@ impl MacroDocs { #[test] fn meth_doc() { Python::with_gil(|py| { - let d = [("C", py.get_type::())].into_py_dict(py); + let d = [("C", py.get_type::())] + .into_py_dict(py) + .unwrap(); py_assert!( py, *d, diff --git a/tests/test_mapping.rs b/tests/test_mapping.rs index 1938c8837ff..ecb944e983e 100644 --- a/tests/test_mapping.rs +++ b/tests/test_mapping.rs @@ -71,7 +71,9 @@ impl Mapping { /// Return a dict with `m = Mapping(['1', '2', '3'])`. fn map_dict(py: Python<'_>) -> Bound<'_, pyo3::types::PyDict> { - let d = [("Mapping", py.get_type::())].into_py_dict(py); + let d = [("Mapping", py.get_type::())] + .into_py_dict(py) + .unwrap(); py_run!(py, *d, "m = Mapping(['1', '2', '3'])"); d } diff --git a/tests/test_methods.rs b/tests/test_methods.rs index 2a9ffbec788..eb099bc0aa5 100644 --- a/tests/test_methods.rs +++ b/tests/test_methods.rs @@ -86,7 +86,9 @@ impl ClassMethod { #[test] fn class_method() { Python::with_gil(|py| { - let d = [("C", py.get_type::())].into_py_dict(py); + let d = [("C", py.get_type::())] + .into_py_dict(py) + .unwrap(); py_assert!(py, *d, "C.method() == 'ClassMethod.method()!'"); py_assert!(py, *d, "C().method() == 'ClassMethod.method()!'"); py_assert!( @@ -113,7 +115,9 @@ impl ClassMethodWithArgs { #[test] fn class_method_with_args() { Python::with_gil(|py| { - let d = [("C", py.get_type::())].into_py_dict(py); + let d = [("C", py.get_type::())] + .into_py_dict(py) + .unwrap(); py_assert!( py, *d, @@ -144,7 +148,9 @@ fn static_method() { Python::with_gil(|py| { assert_eq!(StaticMethod::method(py), "StaticMethod.method()!"); - let d = [("C", py.get_type::())].into_py_dict(py); + let d = [("C", py.get_type::())] + .into_py_dict(py) + .unwrap(); py_assert!(py, *d, "C.method() == 'StaticMethod.method()!'"); py_assert!(py, *d, "C().method() == 'StaticMethod.method()!'"); py_assert!(py, *d, "C.method.__doc__ == 'Test static method.'"); @@ -168,7 +174,9 @@ fn static_method_with_args() { Python::with_gil(|py| { assert_eq!(StaticMethodWithArgs::method(py, 1234), "0x4d2"); - let d = [("C", py.get_type::())].into_py_dict(py); + let d = [("C", py.get_type::())] + .into_py_dict(py) + .unwrap(); py_assert!(py, *d, "C.method(1337) == '0x539'"); }); } @@ -677,7 +685,7 @@ impl MethDocs { #[test] fn meth_doc() { Python::with_gil(|py| { - let d = [("C", py.get_type::())].into_py_dict(py); + let d = [("C", py.get_type::())].into_py_dict(py).unwrap(); py_assert!(py, *d, "C.__doc__ == 'A class with \"documentation\".'"); py_assert!( py, @@ -764,7 +772,7 @@ fn method_with_pyclassarg() { Python::with_gil(|py| { let obj1 = Py::new(py, MethodWithPyClassArg { value: 10 }).unwrap(); let obj2 = Py::new(py, MethodWithPyClassArg { value: 10 }).unwrap(); - let d = [("obj1", obj1), ("obj2", obj2)].into_py_dict(py); + let d = [("obj1", obj1), ("obj2", obj2)].into_py_dict(py).unwrap(); py_run!(py, *d, "obj = obj1.add(obj2); assert obj.value == 20"); py_run!(py, *d, "obj = obj1.add_pyref(obj2); assert obj.value == 20"); py_run!(py, *d, "obj = obj1.optional_add(); assert obj.value == 20"); diff --git a/tests/test_module.rs b/tests/test_module.rs index 6f19ad5763c..c33a8a27ee5 100644 --- a/tests/test_module.rs +++ b/tests/test_module.rs @@ -76,7 +76,8 @@ fn test_module_with_functions() { "module_with_functions", wrap_pymodule!(module_with_functions)(py), )] - .into_py_dict(py); + .into_py_dict(py) + .unwrap(); py_assert!( py, @@ -133,7 +134,8 @@ fn test_module_with_explicit_py_arg() { "module_with_explicit_py_arg", wrap_pymodule!(module_with_explicit_py_arg)(py), )] - .into_py_dict(py); + .into_py_dict(py) + .unwrap(); py_assert!(py, *d, "module_with_explicit_py_arg.double(3) == 6"); }); @@ -150,7 +152,9 @@ fn test_module_renaming() { use pyo3::wrap_pymodule; Python::with_gil(|py| { - let d = [("different_name", wrap_pymodule!(some_name)(py))].into_py_dict(py); + let d = [("different_name", wrap_pymodule!(some_name)(py))] + .into_py_dict(py) + .unwrap(); py_run!(py, *d, "assert different_name.__name__ == 'other_name'"); }); diff --git a/tests/test_sequence.rs b/tests/test_sequence.rs index 9ac8d6dbe89..fd7e9eb01c6 100644 --- a/tests/test_sequence.rs +++ b/tests/test_sequence.rs @@ -107,7 +107,9 @@ impl ByteSequence { /// Return a dict with `s = ByteSequence([1, 2, 3])`. fn seq_dict(py: Python<'_>) -> Bound<'_, pyo3::types::PyDict> { - let d = [("ByteSequence", py.get_type::())].into_py_dict(py); + let d = [("ByteSequence", py.get_type::())] + .into_py_dict(py) + .unwrap(); // Though we can construct `s` in Rust, let's test `__new__` works. py_run!(py, *d, "s = ByteSequence([1, 2, 3])"); d @@ -139,7 +141,9 @@ fn test_setitem() { #[test] fn test_delitem() { Python::with_gil(|py| { - let d = [("ByteSequence", py.get_type::())].into_py_dict(py); + let d = [("ByteSequence", py.get_type::())] + .into_py_dict(py) + .unwrap(); py_run!( py, @@ -235,7 +239,9 @@ fn test_repeat() { #[test] fn test_inplace_repeat() { Python::with_gil(|py| { - let d = [("ByteSequence", py.get_type::())].into_py_dict(py); + let d = [("ByteSequence", py.get_type::())] + .into_py_dict(py) + .unwrap(); py_run!( py, diff --git a/tests/test_static_slots.rs b/tests/test_static_slots.rs index bd1cb0cdc8a..48d80a43856 100644 --- a/tests/test_static_slots.rs +++ b/tests/test_static_slots.rs @@ -38,7 +38,9 @@ impl Count5 { /// Return a dict with `s = Count5()`. fn test_dict(py: Python<'_>) -> Bound<'_, pyo3::types::PyDict> { - let d = [("Count5", py.get_type::())].into_py_dict(py); + let d = [("Count5", py.get_type::())] + .into_py_dict(py) + .unwrap(); // Though we can construct `s` in Rust, let's test `__new__` works. py_run!(py, *d, "s = Count5()"); d From caa3c517fec8cc415d5f4a6d123a07621c41a2bc Mon Sep 17 00:00:00 2001 From: Andrew Hlynskyi Date: Wed, 2 Oct 2024 01:44:07 +0300 Subject: [PATCH 294/495] Small fixes (#4567) * Fix misprint in PyClassArgs::parse_struct_args method name * Fix non snake case complains for __pymethod_* * add CHANGELOG --------- Co-authored-by: David Hewitt --- newsfragments/4567.fixed.md | 1 + pyo3-macros-backend/src/pyclass.rs | 2 +- pyo3-macros-backend/src/pymethod.rs | 2 ++ pyo3-macros/src/lib.rs | 2 +- 4 files changed, 5 insertions(+), 2 deletions(-) create mode 100644 newsfragments/4567.fixed.md diff --git a/newsfragments/4567.fixed.md b/newsfragments/4567.fixed.md new file mode 100644 index 00000000000..24507b81b41 --- /dev/null +++ b/newsfragments/4567.fixed.md @@ -0,0 +1 @@ +Fix compiler warning about non snake case method names inside `#[pymethods]` generated code. diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index 1da9cfa20ad..c9fe1956f66 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -48,7 +48,7 @@ impl PyClassArgs { }) } - pub fn parse_stuct_args(input: ParseStream<'_>) -> syn::Result { + pub fn parse_struct_args(input: ParseStream<'_>) -> syn::Result { Self::parse(input, PyClassKind::Struct) } diff --git a/pyo3-macros-backend/src/pymethod.rs b/pyo3-macros-backend/src/pymethod.rs index 567bd2f5ac8..e7e50145e04 100644 --- a/pyo3-macros-backend/src/pymethod.rs +++ b/pyo3-macros-backend/src/pymethod.rs @@ -1298,6 +1298,7 @@ impl SlotDef { let name = spec.name; let holders = holders.init_holders(ctx); let associated_method = quote! { + #[allow(non_snake_case)] unsafe fn #wrapper_ident( py: #pyo3_path::Python<'_>, _raw_slf: *mut #pyo3_path::ffi::PyObject, @@ -1421,6 +1422,7 @@ impl SlotFragmentDef { let holders = holders.init_holders(ctx); Ok(quote! { impl #cls { + #[allow(non_snake_case)] unsafe fn #wrapper_ident( py: #pyo3_path::Python, _raw_slf: *mut #pyo3_path::ffi::PyObject, diff --git a/pyo3-macros/src/lib.rs b/pyo3-macros/src/lib.rs index c4263a512d3..08b2af3cd6f 100644 --- a/pyo3-macros/src/lib.rs +++ b/pyo3-macros/src/lib.rs @@ -168,7 +168,7 @@ fn pyclass_impl( mut ast: syn::ItemStruct, methods_type: PyClassMethodsType, ) -> TokenStream { - let args = parse_macro_input!(attrs with PyClassArgs::parse_stuct_args); + let args = parse_macro_input!(attrs with PyClassArgs::parse_struct_args); let expanded = build_py_class(&mut ast, args, methods_type).unwrap_or_compile_error(); quote!( From 26abde5f858286b78c755da3b2627e4d13b4b234 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Tue, 1 Oct 2024 23:24:29 -0600 Subject: [PATCH 295/495] Add PyWeakref_GetRef and use it in weakref wrappers. (#4528) * add FFI bindings and a compat definition for PyWeakref_GetRef * implement get_object method of weakref wrappers using PyWeakref_GetRef * add changelog * rename _ref argument to reference * mark PyWeakref_GetObject as deprecated * mark test as using deprecated API item * update docs to reference PyWeakref_GetRef semantics * remove weakref methods that return borrowed references * fix lints about unused imports --- newsfragments/4528.added.md | 2 + newsfragments/4528.removed.md | 7 + pyo3-ffi/src/compat/py_3_13.rs | 33 ++ pyo3-ffi/src/weakrefobject.rs | 9 +- src/types/weakref/anyref.rs | 761 +++------------------------------ src/types/weakref/proxy.rs | 405 +----------------- src/types/weakref/reference.rs | 217 +--------- 7 files changed, 116 insertions(+), 1318 deletions(-) create mode 100644 newsfragments/4528.added.md create mode 100644 newsfragments/4528.removed.md diff --git a/newsfragments/4528.added.md b/newsfragments/4528.added.md new file mode 100644 index 00000000000..0991f9a5261 --- /dev/null +++ b/newsfragments/4528.added.md @@ -0,0 +1,2 @@ +* Added bindings for `pyo3_ffi::PyWeakref_GetRef` on Python 3.13 and newer and + `py03_ffi::compat::PyWeakref_GetRef` for older Python versions. diff --git a/newsfragments/4528.removed.md b/newsfragments/4528.removed.md new file mode 100644 index 00000000000..79c66b98818 --- /dev/null +++ b/newsfragments/4528.removed.md @@ -0,0 +1,7 @@ +* Removed the `get_object_borrowed`, `upgrade_borrowed`, `upgrade_borrowed_as`, +`upgrade_borrowed_as_unchecked`, `upgrade_borrowed_as_exact` methods of +`PyWeakref` and `PyWeakrefProxy`. These returned borrowed references to weakly +referenced data, and in principle if the GIL is released the last strong +reference could be released, allowing a possible use-after-free error. If you +are using these functions, you should change to the equivalent function that +returns a `Bound<'py, T>` reference. diff --git a/pyo3-ffi/src/compat/py_3_13.rs b/pyo3-ffi/src/compat/py_3_13.rs index 94778802987..9f44ced6f3f 100644 --- a/pyo3-ffi/src/compat/py_3_13.rs +++ b/pyo3-ffi/src/compat/py_3_13.rs @@ -50,3 +50,36 @@ compat_function!( Py_XNewRef(PyImport_AddModule(name)) } ); + +compat_function!( + originally_defined_for(Py_3_13); + + #[inline] + pub unsafe fn PyWeakref_GetRef( + reference: *mut crate::PyObject, + pobj: *mut *mut crate::PyObject, + ) -> std::os::raw::c_int { + use crate::{ + compat::Py_NewRef, PyErr_SetString, PyExc_TypeError, PyWeakref_Check, + PyWeakref_GetObject, Py_None, + }; + + if !reference.is_null() && PyWeakref_Check(reference) == 0 { + *pobj = std::ptr::null_mut(); + PyErr_SetString(PyExc_TypeError, c_str!("expected a weakref").as_ptr()); + return -1; + } + let obj = PyWeakref_GetObject(reference); + if obj.is_null() { + // SystemError if reference is NULL + *pobj = std::ptr::null_mut(); + return -1; + } + if obj == Py_None() { + *pobj = std::ptr::null_mut(); + return 0; + } + *pobj = Py_NewRef(obj); + 1 + } +); diff --git a/pyo3-ffi/src/weakrefobject.rs b/pyo3-ffi/src/weakrefobject.rs index 7e11a9012e7..305dc290fa8 100644 --- a/pyo3-ffi/src/weakrefobject.rs +++ b/pyo3-ffi/src/weakrefobject.rs @@ -58,5 +58,12 @@ extern "C" { #[cfg_attr(PyPy, link_name = "PyPyWeakref_NewProxy")] pub fn PyWeakref_NewProxy(ob: *mut PyObject, callback: *mut PyObject) -> *mut PyObject; #[cfg_attr(PyPy, link_name = "PyPyWeakref_GetObject")] - pub fn PyWeakref_GetObject(_ref: *mut PyObject) -> *mut PyObject; + #[cfg_attr( + Py_3_13, + deprecated(note = "deprecated since Python 3.13. Use `PyWeakref_GetRef` instead.") + )] + pub fn PyWeakref_GetObject(reference: *mut PyObject) -> *mut PyObject; + #[cfg(Py_3_13)] + #[cfg_attr(PyPy, link_name = "PyPyWeakref_GetRef")] + pub fn PyWeakref_GetRef(reference: *mut PyObject, pobj: *mut *mut PyObject) -> c_int; } diff --git a/src/types/weakref/anyref.rs b/src/types/weakref/anyref.rs index 182bf9f549d..06d91ea024e 100644 --- a/src/types/weakref/anyref.rs +++ b/src/types/weakref/anyref.rs @@ -1,8 +1,11 @@ -use crate::err::{DowncastError, PyResult}; +use crate::err::PyResult; use crate::ffi_ptr_ext::FfiPtrExt; use crate::type_object::{PyTypeCheck, PyTypeInfo}; -use crate::types::any::{PyAny, PyAnyMethods}; -use crate::{ffi, Borrowed, Bound}; +use crate::types::{ + any::{PyAny, PyAnyMethods}, + PyNone, +}; +use crate::{ffi, Bound}; /// Represents any Python `weakref` reference. /// @@ -34,7 +37,7 @@ pub trait PyWeakrefMethods<'py> { /// Upgrade the weakref to a direct Bound object reference. /// /// It is named `upgrade` to be inline with [rust's `Weak::upgrade`](std::rc::Weak::upgrade). - /// In Python it would be equivalent to [`PyWeakref_GetObject`]. + /// In Python it would be equivalent to [`PyWeakref_GetRef`]. /// /// # Example #[cfg_attr( @@ -94,7 +97,7 @@ pub trait PyWeakrefMethods<'py> { /// This function panics is the current object is invalid. /// If used propperly this is never the case. (NonNull and actually a weakref type) /// - /// [`PyWeakref_GetObject`]: https://docs.python.org/3/c-api/weakref.html#c.PyWeakref_GetObject + /// [`PyWeakref_GetRef`]: https://docs.python.org/3/c-api/weakref.html#c.PyWeakref_GetRef /// [`weakref.ReferenceType`]: https://docs.python.org/3/library/weakref.html#weakref.ReferenceType /// [`weakref.ref`]: https://docs.python.org/3/library/weakref.html#weakref.ref fn upgrade_as(&self) -> PyResult>> @@ -107,91 +110,10 @@ pub trait PyWeakrefMethods<'py> { .map_err(Into::into) } - /// Upgrade the weakref to a Borrowed object reference. - /// - /// It is named `upgrade_borrowed` to be inline with [rust's `Weak::upgrade`](std::rc::Weak::upgrade). - /// In Python it would be equivalent to [`PyWeakref_GetObject`]. - /// - /// # Example - #[cfg_attr( - not(all(feature = "macros", not(all(Py_LIMITED_API, not(Py_3_9))))), - doc = "```rust,ignore" - )] - #[cfg_attr( - all(feature = "macros", not(all(Py_LIMITED_API, not(Py_3_9)))), - doc = "```rust" - )] - /// use pyo3::prelude::*; - /// use pyo3::types::PyWeakrefReference; - /// - /// #[pyclass(weakref)] - /// struct Foo { /* fields omitted */ } - /// - /// #[pymethods] - /// impl Foo { - /// fn get_data(&self) -> (&str, u32) { - /// ("Dave", 10) - /// } - /// } - /// - /// fn parse_data(reference: Borrowed<'_, '_, PyWeakrefReference>) -> PyResult { - /// if let Some(data_src) = reference.upgrade_borrowed_as::()? { - /// let data = data_src.borrow(); - /// let (name, score) = data.get_data(); - /// Ok(format!("Processing '{}': score = {}", name, score)) - /// } else { - /// Ok("The supplied data reference is nolonger relavent.".to_owned()) - /// } - /// } - /// - /// # fn main() -> PyResult<()> { - /// Python::with_gil(|py| { - /// let data = Bound::new(py, Foo{})?; - /// let reference = PyWeakrefReference::new(&data)?; - /// - /// assert_eq!( - /// parse_data(reference.as_borrowed())?, - /// "Processing 'Dave': score = 10" - /// ); - /// - /// drop(data); - /// - /// assert_eq!( - /// parse_data(reference.as_borrowed())?, - /// "The supplied data reference is nolonger relavent." - /// ); - /// - /// Ok(()) - /// }) - /// # } - /// ``` - /// - /// # Panics - /// This function panics is the current object is invalid. - /// If used propperly this is never the case. (NonNull and actually a weakref type) - /// - /// [`PyWeakref_GetObject`]: https://docs.python.org/3/c-api/weakref.html#c.PyWeakref_GetObject - /// [`weakref.ReferenceType`]: https://docs.python.org/3/library/weakref.html#weakref.ReferenceType - /// [`weakref.ref`]: https://docs.python.org/3/library/weakref.html#weakref.ref? - fn upgrade_borrowed_as<'a, T>(&'a self) -> PyResult>> - where - T: PyTypeCheck, - 'py: 'a, - { - // TODO: Replace when Borrowed::downcast exists - match self.upgrade_borrowed() { - None => Ok(None), - Some(object) if T::type_check(&object) => { - Ok(Some(unsafe { object.downcast_unchecked() })) - } - Some(object) => Err(DowncastError::new(&object, T::NAME).into()), - } - } - /// Upgrade the weakref to a direct Bound object reference unchecked. The type of the recovered object is not checked before downcasting, this could lead to unexpected behavior. Use only when absolutely certain the type can be guaranteed. The `weakref` may still return `None`. /// /// It is named `upgrade` to be inline with [rust's `Weak::upgrade`](std::rc::Weak::upgrade). - /// In Python it would be equivalent to [`PyWeakref_GetObject`]. + /// In Python it would be equivalent to [`PyWeakref_GetRef`]. /// /// # Safety /// Callers must ensure that the type is valid or risk type confusion. @@ -255,94 +177,17 @@ pub trait PyWeakrefMethods<'py> { /// This function panics is the current object is invalid. /// If used propperly this is never the case. (NonNull and actually a weakref type) /// - /// [`PyWeakref_GetObject`]: https://docs.python.org/3/c-api/weakref.html#c.PyWeakref_GetObject + /// [`PyWeakref_GetRef`]: https://docs.python.org/3/c-api/weakref.html#c.PyWeakref_GetRef /// [`weakref.ReferenceType`]: https://docs.python.org/3/library/weakref.html#weakref.ReferenceType /// [`weakref.ref`]: https://docs.python.org/3/library/weakref.html#weakref.ref unsafe fn upgrade_as_unchecked(&self) -> Option> { Some(self.upgrade()?.downcast_into_unchecked()) } - /// Upgrade the weakref to a Borrowed object reference unchecked. The type of the recovered object is not checked before downcasting, this could lead to unexpected behavior. Use only when absolutely certain the type can be guaranteed. The `weakref` may still return `None`. - /// - /// It is named `upgrade_borrowed` to be inline with [rust's `Weak::upgrade`](std::rc::Weak::upgrade). - /// In Python it would be equivalent to [`PyWeakref_GetObject`]. - /// - /// # Safety - /// Callers must ensure that the type is valid or risk type confusion. - /// The `weakref` is still allowed to be `None`, if the referenced object has been cleaned up. - /// - /// # Example - #[cfg_attr( - not(all(feature = "macros", not(all(Py_LIMITED_API, not(Py_3_9))))), - doc = "```rust,ignore" - )] - #[cfg_attr( - all(feature = "macros", not(all(Py_LIMITED_API, not(Py_3_9)))), - doc = "```rust" - )] - /// use pyo3::prelude::*; - /// use pyo3::types::PyWeakrefReference; - /// - /// #[pyclass(weakref)] - /// struct Foo { /* fields omitted */ } - /// - /// #[pymethods] - /// impl Foo { - /// fn get_data(&self) -> (&str, u32) { - /// ("Dave", 10) - /// } - /// } - /// - /// fn parse_data(reference: Borrowed<'_, '_, PyWeakrefReference>) -> String { - /// if let Some(data_src) = unsafe { reference.upgrade_borrowed_as_unchecked::() } { - /// let data = data_src.borrow(); - /// let (name, score) = data.get_data(); - /// format!("Processing '{}': score = {}", name, score) - /// } else { - /// "The supplied data reference is nolonger relavent.".to_owned() - /// } - /// } - /// - /// # fn main() -> PyResult<()> { - /// Python::with_gil(|py| { - /// let data = Bound::new(py, Foo{})?; - /// let reference = PyWeakrefReference::new(&data)?; - /// - /// assert_eq!( - /// parse_data(reference.as_borrowed()), - /// "Processing 'Dave': score = 10" - /// ); - /// - /// drop(data); - /// - /// assert_eq!( - /// parse_data(reference.as_borrowed()), - /// "The supplied data reference is nolonger relavent." - /// ); - /// - /// Ok(()) - /// }) - /// # } - /// ``` - /// - /// # Panics - /// This function panics is the current object is invalid. - /// If used propperly this is never the case. (NonNull and actually a weakref type) - /// - /// [`PyWeakref_GetObject`]: https://docs.python.org/3/c-api/weakref.html#c.PyWeakref_GetObject - /// [`weakref.ReferenceType`]: https://docs.python.org/3/library/weakref.html#weakref.ReferenceType - /// [`weakref.ref`]: https://docs.python.org/3/library/weakref.html#weakref.ref? - unsafe fn upgrade_borrowed_as_unchecked<'a, T>(&'a self) -> Option> - where - 'py: 'a, - { - Some(self.upgrade_borrowed()?.downcast_unchecked()) - } - /// Upgrade the weakref to a exact direct Bound object reference. /// /// It is named `upgrade` to be inline with [rust's `Weak::upgrade`](std::rc::Weak::upgrade). - /// In Python it would be equivalent to [`PyWeakref_GetObject`]. + /// In Python it would be equivalent to [`PyWeakref_GetRef`]. /// /// # Example #[cfg_attr( @@ -402,7 +247,7 @@ pub trait PyWeakrefMethods<'py> { /// This function panics is the current object is invalid. /// If used propperly this is never the case. (NonNull and actually a weakref type) /// - /// [`PyWeakref_GetObject`]: https://docs.python.org/3/c-api/weakref.html#c.PyWeakref_GetObject + /// [`PyWeakref_GetRef`]: https://docs.python.org/3/c-api/weakref.html#c.PyWeakref_GetRef /// [`weakref.ReferenceType`]: https://docs.python.org/3/library/weakref.html#weakref.ReferenceType /// [`weakref.ref`]: https://docs.python.org/3/library/weakref.html#weakref.ref fn upgrade_as_exact(&self) -> PyResult>> @@ -415,94 +260,13 @@ pub trait PyWeakrefMethods<'py> { .map_err(Into::into) } - /// Upgrade the weakref to a exact Borrowed object reference. - /// - /// It is named `upgrade_borrowed` to be inline with [rust's `Weak::upgrade`](std::rc::Weak::upgrade). - /// In Python it would be equivalent to [`PyWeakref_GetObject`]. - /// - /// # Example - #[cfg_attr( - not(all(feature = "macros", not(all(Py_LIMITED_API, not(Py_3_9))))), - doc = "```rust,ignore" - )] - #[cfg_attr( - all(feature = "macros", not(all(Py_LIMITED_API, not(Py_3_9)))), - doc = "```rust" - )] - /// use pyo3::prelude::*; - /// use pyo3::types::PyWeakrefReference; - /// - /// #[pyclass(weakref)] - /// struct Foo { /* fields omitted */ } - /// - /// #[pymethods] - /// impl Foo { - /// fn get_data(&self) -> (&str, u32) { - /// ("Dave", 10) - /// } - /// } - /// - /// fn parse_data(reference: Borrowed<'_, '_, PyWeakrefReference>) -> PyResult { - /// if let Some(data_src) = reference.upgrade_borrowed_as_exact::()? { - /// let data = data_src.borrow(); - /// let (name, score) = data.get_data(); - /// Ok(format!("Processing '{}': score = {}", name, score)) - /// } else { - /// Ok("The supplied data reference is nolonger relavent.".to_owned()) - /// } - /// } - /// - /// # fn main() -> PyResult<()> { - /// Python::with_gil(|py| { - /// let data = Bound::new(py, Foo{})?; - /// let reference = PyWeakrefReference::new(&data)?; - /// - /// assert_eq!( - /// parse_data(reference.as_borrowed())?, - /// "Processing 'Dave': score = 10" - /// ); - /// - /// drop(data); - /// - /// assert_eq!( - /// parse_data(reference.as_borrowed())?, - /// "The supplied data reference is nolonger relavent." - /// ); - /// - /// Ok(()) - /// }) - /// # } - /// ``` - /// - /// # Panics - /// This function panics is the current object is invalid. - /// If used propperly this is never the case. (NonNull and actually a weakref type) - /// - /// [`PyWeakref_GetObject`]: https://docs.python.org/3/c-api/weakref.html#c.PyWeakref_GetObject - /// [`weakref.ReferenceType`]: https://docs.python.org/3/library/weakref.html#weakref.ReferenceType - /// [`weakref.ref`]: https://docs.python.org/3/library/weakref.html#weakref.ref? - fn upgrade_borrowed_as_exact<'a, T>(&'a self) -> PyResult>> - where - T: PyTypeInfo, - 'py: 'a, - { - // TODO: Replace when Borrowed::downcast_exact exists - match self.upgrade_borrowed() { - None => Ok(None), - Some(object) if object.is_exact_instance_of::() => { - Ok(Some(unsafe { object.downcast_unchecked() })) - } - Some(object) => Err(DowncastError::new(&object, T::NAME).into()), - } - } - /// Upgrade the weakref to a Bound [`PyAny`] reference to the target object if possible. /// /// It is named `upgrade` to be inline with [rust's `Weak::upgrade`](std::rc::Weak::upgrade). /// This function returns `Some(Bound<'py, PyAny>)` if the reference still exists, otherwise `None` will be returned. /// /// This function gets the optional target of this [`weakref.ReferenceType`] (result of calling [`weakref.ref`]). - /// It produces similair results to using [`PyWeakref_GetObject`] in the C api. + /// It produces similar results to using [`PyWeakref_GetRef`] in the C api. /// /// # Example #[cfg_attr( @@ -553,7 +317,7 @@ pub trait PyWeakrefMethods<'py> { /// This function panics is the current object is invalid. /// If used propperly this is never the case. (NonNull and actually a weakref type) /// - /// [`PyWeakref_GetObject`]: https://docs.python.org/3/c-api/weakref.html#c.PyWeakref_GetObject + /// [`PyWeakref_GetRef`]: https://docs.python.org/3/c-api/weakref.html#c.PyWeakref_GetRef /// [`weakref.ReferenceType`]: https://docs.python.org/3/library/weakref.html#weakref.ReferenceType /// [`weakref.ref`]: https://docs.python.org/3/library/weakref.html#weakref.ref fn upgrade(&self) -> Option> { @@ -566,85 +330,12 @@ pub trait PyWeakrefMethods<'py> { } } - /// Upgrade the weakref to a Borrowed [`PyAny`] reference to the target object if possible. - /// - /// It is named `upgrade_borrowed` to be inline with [rust's `Weak::upgrade`](std::rc::Weak::upgrade). - /// This function returns `Some(Borrowed<'_, 'py, PyAny>)` if the reference still exists, otherwise `None` will be returned. - /// - /// This function gets the optional target of this [`weakref.ReferenceType`] (result of calling [`weakref.ref`]). - /// It produces similair results to using [`PyWeakref_GetObject`] in the C api. - /// - /// # Example - #[cfg_attr( - not(all(feature = "macros", not(all(Py_LIMITED_API, not(Py_3_9))))), - doc = "```rust,ignore" - )] - #[cfg_attr( - all(feature = "macros", not(all(Py_LIMITED_API, not(Py_3_9)))), - doc = "```rust" - )] - /// use pyo3::prelude::*; - /// use pyo3::types::PyWeakrefReference; - /// - /// #[pyclass(weakref)] - /// struct Foo { /* fields omitted */ } - /// - /// fn parse_data(reference: Borrowed<'_, '_, PyWeakrefReference>) -> PyResult { - /// if let Some(object) = reference.upgrade_borrowed() { - /// Ok(format!("The object '{}' refered by this reference still exists.", object.getattr("__class__")?.getattr("__qualname__")?)) - /// } else { - /// Ok("The object, which this reference refered to, no longer exists".to_owned()) - /// } - /// } - /// - /// # fn main() -> PyResult<()> { - /// Python::with_gil(|py| { - /// let data = Bound::new(py, Foo{})?; - /// let reference = PyWeakrefReference::new(&data)?; - /// - /// assert_eq!( - /// parse_data(reference.as_borrowed())?, - /// "The object 'Foo' refered by this reference still exists." - /// ); - /// - /// drop(data); - /// - /// assert_eq!( - /// parse_data(reference.as_borrowed())?, - /// "The object, which this reference refered to, no longer exists" - /// ); - /// - /// Ok(()) - /// }) - /// # } - /// ``` - /// - /// # Panics - /// This function panics is the current object is invalid. - /// If used propperly this is never the case. (NonNull and actually a weakref type) - /// - /// [`PyWeakref_GetObject`]: https://docs.python.org/3/c-api/weakref.html#c.PyWeakref_GetObject - /// [`weakref.ReferenceType`]: https://docs.python.org/3/library/weakref.html#weakref.ReferenceType - /// [`weakref.ref`]: https://docs.python.org/3/library/weakref.html#weakref.ref - fn upgrade_borrowed<'a>(&'a self) -> Option> - where - 'py: 'a, - { - let object = self.get_object_borrowed(); - - if object.is_none() { - None - } else { - Some(object) - } - } - /// Retrieve to a Bound object pointed to by the weakref. /// /// This function returns `Bound<'py, PyAny>`, which is either the object if it still exists, otherwise it will refer to [`PyNone`](crate::types::PyNone). /// /// This function gets the optional target of this [`weakref.ReferenceType`] (result of calling [`weakref.ref`]). - /// It produces similair results to using [`PyWeakref_GetObject`] in the C api. + /// It produces similar results to using [`PyWeakref_GetRef`] in the C api. /// /// # Example #[cfg_attr( @@ -692,157 +383,52 @@ pub trait PyWeakrefMethods<'py> { /// This function panics is the current object is invalid. /// If used propperly this is never the case. (NonNull and actually a weakref type) /// - /// [`PyWeakref_GetObject`]: https://docs.python.org/3/c-api/weakref.html#c.PyWeakref_GetObject - /// [`weakref.ReferenceType`]: https://docs.python.org/3/library/weakref.html#weakref.ReferenceType - /// [`weakref.ref`]: https://docs.python.org/3/library/weakref.html#weakref.ref - fn get_object(&self) -> Bound<'py, PyAny> { - // PyWeakref_GetObject does some error checking, however we ensure the passed object is Non-Null and a Weakref type. - self.get_object_borrowed().to_owned() - } - - /// Retrieve to a Borrowed object pointed to by the weakref. - /// - /// This function returns `Borrowed<'py, PyAny>`, which is either the object if it still exists, otherwise it will refer to [`PyNone`](crate::types::PyNone). - /// - /// This function gets the optional target of this [`weakref.ReferenceType`] (result of calling [`weakref.ref`]). - /// It produces similair results to using [`PyWeakref_GetObject`] in the C api. - /// - /// # Example - #[cfg_attr( - not(all(feature = "macros", not(all(Py_LIMITED_API, not(Py_3_9))))), - doc = "```rust,ignore" - )] - #[cfg_attr( - all(feature = "macros", not(all(Py_LIMITED_API, not(Py_3_9)))), - doc = "```rust" - )] - /// use pyo3::prelude::*; - /// use pyo3::types::PyWeakrefReference; - /// - /// #[pyclass(weakref)] - /// struct Foo { /* fields omitted */ } - /// - /// fn get_class(reference: Borrowed<'_, '_, PyWeakrefReference>) -> PyResult { - /// reference - /// .get_object_borrowed() - /// .getattr("__class__")? - /// .repr() - /// .map(|repr| repr.to_string()) - /// } - /// - /// # fn main() -> PyResult<()> { - /// Python::with_gil(|py| { - /// let object = Bound::new(py, Foo{})?; - /// let reference = PyWeakrefReference::new(&object)?; - /// - /// assert_eq!( - /// get_class(reference.as_borrowed())?, - /// "" - /// ); - /// - /// drop(object); - /// - /// assert_eq!(get_class(reference.as_borrowed())?, ""); - /// - /// Ok(()) - /// }) - /// # } - /// ``` - /// - /// # Panics - /// This function panics is the current object is invalid. - /// If used propperly this is never the case. (NonNull and actually a weakref type) - /// - /// [`PyWeakref_GetObject`]: https://docs.python.org/3/c-api/weakref.html#c.PyWeakref_GetObject + /// [`PyWeakref_GetRef`]: https://docs.python.org/3/c-api/weakref.html#c.PyWeakref_GetRef /// [`weakref.ReferenceType`]: https://docs.python.org/3/library/weakref.html#weakref.ReferenceType /// [`weakref.ref`]: https://docs.python.org/3/library/weakref.html#weakref.ref - #[track_caller] - // TODO: This function is the reason every function tracks caller, however it only panics when the weakref object is not actually a weakreference type. So is it this neccessary? - fn get_object_borrowed(&self) -> Borrowed<'_, 'py, PyAny>; + fn get_object(&self) -> Bound<'py, PyAny>; } impl<'py> PyWeakrefMethods<'py> for Bound<'py, PyWeakref> { - fn get_object_borrowed(&self) -> Borrowed<'_, 'py, PyAny> { - // PyWeakref_GetObject does some error checking, however we ensure the passed object is Non-Null and a Weakref type. - unsafe { ffi::PyWeakref_GetObject(self.as_ptr()).assume_borrowed_or_err(self.py()) } - .expect("The 'weakref' weak reference instance should be valid (non-null and actually a weakref reference)") + fn get_object(&self) -> Bound<'py, PyAny> { + let mut obj: *mut ffi::PyObject = std::ptr::null_mut(); + match unsafe { ffi::compat::PyWeakref_GetRef(self.as_ptr(), &mut obj) } { + std::os::raw::c_int::MIN..=-1 => panic!("The 'weakref' weak reference instance should be valid (non-null and actually a weakref reference)"), + 0 => PyNone::get(self.py()).to_owned().into_any(), + 1..=std::os::raw::c_int::MAX => unsafe { obj.assume_owned(self.py()) }, + } } } -#[cfg(test)] -mod tests { - use crate::types::any::{PyAny, PyAnyMethods}; - use crate::types::weakref::{PyWeakref, PyWeakrefMethods, PyWeakrefProxy, PyWeakrefReference}; - use crate::{Bound, PyResult, Python}; - - fn new_reference<'py>(object: &Bound<'py, PyAny>) -> PyResult> { - let reference = PyWeakrefReference::new(object)?; - reference.into_any().downcast_into().map_err(Into::into) - } - - fn new_proxy<'py>(object: &Bound<'py, PyAny>) -> PyResult> { - let reference = PyWeakrefProxy::new(object)?; - reference.into_any().downcast_into().map_err(Into::into) - } - - mod python_class { - use super::*; - use crate::ffi; - use crate::{py_result_ext::PyResultExt, types::PyType}; - - fn get_type(py: Python<'_>) -> PyResult> { - py.run(ffi::c_str!("class A:\n pass\n"), None, None)?; - py.eval(ffi::c_str!("A"), None, None) - .downcast_into::() - } - - #[test] - fn test_weakref_upgrade_as() -> PyResult<()> { - fn inner( - create_reference: impl for<'py> FnOnce( - &Bound<'py, PyAny>, - ) - -> PyResult>, - ) -> PyResult<()> { - Python::with_gil(|py| { - let class = get_type(py)?; - let object = class.call0()?; - let reference = create_reference(&object)?; - - { - // This test is a bit weird but ok. - let obj = reference.upgrade_as::(); - - assert!(obj.is_ok()); - let obj = obj.unwrap(); - - assert!(obj.is_some()); - assert!(obj.map_or(false, |obj| obj.as_ptr() == object.as_ptr() - && obj.is_exact_instance(&class))); - } - - drop(object); - - { - // This test is a bit weird but ok. - let obj = reference.upgrade_as::(); +#[cfg(test)] +mod tests { + use crate::types::any::{PyAny, PyAnyMethods}; + use crate::types::weakref::{PyWeakref, PyWeakrefMethods, PyWeakrefProxy, PyWeakrefReference}; + use crate::{Bound, PyResult, Python}; - assert!(obj.is_ok()); - let obj = obj.unwrap(); + fn new_reference<'py>(object: &Bound<'py, PyAny>) -> PyResult> { + let reference = PyWeakrefReference::new(object)?; + reference.into_any().downcast_into().map_err(Into::into) + } - assert!(obj.is_none()); - } + fn new_proxy<'py>(object: &Bound<'py, PyAny>) -> PyResult> { + let reference = PyWeakrefProxy::new(object)?; + reference.into_any().downcast_into().map_err(Into::into) + } - Ok(()) - }) - } + mod python_class { + use super::*; + use crate::ffi; + use crate::{py_result_ext::PyResultExt, types::PyType}; - inner(new_reference)?; - inner(new_proxy) + fn get_type(py: Python<'_>) -> PyResult> { + py.run(ffi::c_str!("class A:\n pass\n"), None, None)?; + py.eval(ffi::c_str!("A"), None, None) + .downcast_into::() } #[test] - fn test_weakref_upgrade_borrowed_as() -> PyResult<()> { + fn test_weakref_upgrade_as() -> PyResult<()> { fn inner( create_reference: impl for<'py> FnOnce( &Bound<'py, PyAny>, @@ -856,7 +442,7 @@ mod tests { { // This test is a bit weird but ok. - let obj = reference.upgrade_borrowed_as::(); + let obj = reference.upgrade_as::(); assert!(obj.is_ok()); let obj = obj.unwrap(); @@ -870,7 +456,7 @@ mod tests { { // This test is a bit weird but ok. - let obj = reference.upgrade_borrowed_as::(); + let obj = reference.upgrade_as::(); assert!(obj.is_ok()); let obj = obj.unwrap(); @@ -925,45 +511,6 @@ mod tests { inner(new_proxy) } - #[test] - fn test_weakref_upgrade_borrowed_as_unchecked() -> PyResult<()> { - fn inner( - create_reference: impl for<'py> FnOnce( - &Bound<'py, PyAny>, - ) - -> PyResult>, - ) -> PyResult<()> { - Python::with_gil(|py| { - let class = get_type(py)?; - let object = class.call0()?; - let reference = create_reference(&object)?; - - { - // This test is a bit weird but ok. - let obj = unsafe { reference.upgrade_borrowed_as_unchecked::() }; - - assert!(obj.is_some()); - assert!(obj.map_or(false, |obj| obj.as_ptr() == object.as_ptr() - && obj.is_exact_instance(&class))); - } - - drop(object); - - { - // This test is a bit weird but ok. - let obj = unsafe { reference.upgrade_borrowed_as_unchecked::() }; - - assert!(obj.is_none()); - } - - Ok(()) - }) - } - - inner(new_reference)?; - inner(new_proxy) - } - #[test] fn test_weakref_upgrade() -> PyResult<()> { fn inner( @@ -997,41 +544,6 @@ mod tests { inner(new_proxy, false) } - #[test] - fn test_weakref_upgrade_borrowed() -> PyResult<()> { - fn inner( - create_reference: impl for<'py> FnOnce( - &Bound<'py, PyAny>, - ) - -> PyResult>, - call_retrievable: bool, - ) -> PyResult<()> { - let not_call_retrievable = !call_retrievable; - - Python::with_gil(|py| { - let class = get_type(py)?; - let object = class.call0()?; - let reference = create_reference(&object)?; - - assert!(not_call_retrievable || reference.call0()?.is(&object)); - assert!(reference.upgrade_borrowed().is_some()); - assert!(reference - .upgrade_borrowed() - .map_or(false, |obj| obj.is(&object))); - - drop(object); - - assert!(not_call_retrievable || reference.call0()?.is_none()); - assert!(reference.upgrade_borrowed().is_none()); - - Ok(()) - }) - } - - inner(new_reference, true)?; - inner(new_proxy, false) - } - #[test] fn test_weakref_get_object() -> PyResult<()> { fn inner( @@ -1064,38 +576,6 @@ mod tests { inner(new_reference, true)?; inner(new_proxy, false) } - - #[test] - fn test_weakref_get_object_borrowed() -> PyResult<()> { - fn inner( - create_reference: impl for<'py> FnOnce( - &Bound<'py, PyAny>, - ) - -> PyResult>, - call_retrievable: bool, - ) -> PyResult<()> { - let not_call_retrievable = !call_retrievable; - - Python::with_gil(|py| { - let class = get_type(py)?; - let object = class.call0()?; - let reference = create_reference(&object)?; - - assert!(not_call_retrievable || reference.call0()?.is(&object)); - assert!(reference.get_object_borrowed().is(&object)); - - drop(object); - - assert!(not_call_retrievable || reference.call0()?.is_none()); - assert!(reference.get_object_borrowed().is_none()); - - Ok(()) - }) - } - - inner(new_reference, true)?; - inner(new_proxy, false) - } } // under 'abi3-py37' and 'abi3-py38' PyClass cannot be weakreferencable. @@ -1148,47 +628,6 @@ mod tests { inner(new_proxy) } - #[test] - fn test_weakref_upgrade_borrowed_as() -> PyResult<()> { - fn inner( - create_reference: impl for<'py> FnOnce( - &Bound<'py, PyAny>, - ) - -> PyResult>, - ) -> PyResult<()> { - Python::with_gil(|py| { - let object = Py::new(py, WeakrefablePyClass {})?; - let reference = create_reference(object.bind(py))?; - - { - let obj = reference.upgrade_borrowed_as::(); - - assert!(obj.is_ok()); - let obj = obj.unwrap(); - - assert!(obj.is_some()); - assert!(obj.map_or(false, |obj| obj.as_ptr() == object.as_ptr())); - } - - drop(object); - - { - let obj = reference.upgrade_borrowed_as::(); - - assert!(obj.is_ok()); - let obj = obj.unwrap(); - - assert!(obj.is_none()); - } - - Ok(()) - }) - } - - inner(new_reference)?; - inner(new_proxy) - } - #[test] fn test_weakref_upgrade_as_unchecked() -> PyResult<()> { fn inner( @@ -1224,45 +663,6 @@ mod tests { inner(new_proxy) } - #[test] - fn test_weakref_upgrade_borrowed_as_unchecked() -> PyResult<()> { - fn inner( - create_reference: impl for<'py> FnOnce( - &Bound<'py, PyAny>, - ) - -> PyResult>, - ) -> PyResult<()> { - Python::with_gil(|py| { - let object = Py::new(py, WeakrefablePyClass {})?; - let reference = create_reference(object.bind(py))?; - - { - let obj = unsafe { - reference.upgrade_borrowed_as_unchecked::() - }; - - assert!(obj.is_some()); - assert!(obj.map_or(false, |obj| obj.as_ptr() == object.as_ptr())); - } - - drop(object); - - { - let obj = unsafe { - reference.upgrade_borrowed_as_unchecked::() - }; - - assert!(obj.is_none()); - } - - Ok(()) - }) - } - - inner(new_reference)?; - inner(new_proxy) - } - #[test] fn test_weakref_upgrade() -> PyResult<()> { fn inner( @@ -1295,40 +695,6 @@ mod tests { inner(new_proxy, false) } - #[test] - fn test_weakref_upgrade_borrowed() -> PyResult<()> { - fn inner( - create_reference: impl for<'py> FnOnce( - &Bound<'py, PyAny>, - ) - -> PyResult>, - call_retrievable: bool, - ) -> PyResult<()> { - let not_call_retrievable = !call_retrievable; - - Python::with_gil(|py| { - let object = Py::new(py, WeakrefablePyClass {})?; - let reference = create_reference(object.bind(py))?; - - assert!(not_call_retrievable || reference.call0()?.is(&object)); - assert!(reference.upgrade_borrowed().is_some()); - assert!(reference - .upgrade_borrowed() - .map_or(false, |obj| obj.is(&object))); - - drop(object); - - assert!(not_call_retrievable || reference.call0()?.is_none()); - assert!(reference.upgrade_borrowed().is_none()); - - Ok(()) - }) - } - - inner(new_reference, true)?; - inner(new_proxy, false) - } - #[test] fn test_weakref_get_object() -> PyResult<()> { fn inner( @@ -1360,36 +726,5 @@ mod tests { inner(new_reference, true)?; inner(new_proxy, false) } - - #[test] - fn test_weakref_get_object_borrowed() -> PyResult<()> { - fn inner( - create_reference: impl for<'py> FnOnce( - &Bound<'py, PyAny>, - ) - -> PyResult>, - call_retrievable: bool, - ) -> PyResult<()> { - let not_call_retrievable = !call_retrievable; - - Python::with_gil(|py| { - let object = Py::new(py, WeakrefablePyClass {})?; - let reference = create_reference(object.bind(py))?; - - assert!(not_call_retrievable || reference.call0()?.is(&object)); - assert!(reference.get_object_borrowed().is(&object)); - - drop(object); - - assert!(not_call_retrievable || reference.call0()?.is_none()); - assert!(reference.get_object_borrowed().is_none()); - - Ok(()) - }) - } - - inner(new_reference, true)?; - inner(new_proxy, false) - } } } diff --git a/src/types/weakref/proxy.rs b/src/types/weakref/proxy.rs index c902f5d94a5..798b4b435c7 100644 --- a/src/types/weakref/proxy.rs +++ b/src/types/weakref/proxy.rs @@ -2,8 +2,8 @@ use crate::err::PyResult; use crate::ffi_ptr_ext::FfiPtrExt; use crate::py_result_ext::PyResultExt; use crate::type_object::PyTypeCheck; -use crate::types::any::PyAny; -use crate::{ffi, Borrowed, Bound, ToPyObject}; +use crate::types::{any::PyAny, PyNone}; +use crate::{ffi, Bound, ToPyObject}; use super::PyWeakrefMethods; @@ -182,10 +182,13 @@ impl PyWeakrefProxy { } impl<'py> PyWeakrefMethods<'py> for Bound<'py, PyWeakrefProxy> { - fn get_object_borrowed(&self) -> Borrowed<'_, 'py, PyAny> { - // PyWeakref_GetObject does some error checking, however we ensure the passed object is Non-Null and a Weakref type. - unsafe { ffi::PyWeakref_GetObject(self.as_ptr()).assume_borrowed_or_err(self.py()) } - .expect("The 'weakref.ProxyType' (or `weakref.CallableProxyType`) instance should be valid (non-null and actually a weakref reference)") + fn get_object(&self) -> Bound<'py, PyAny> { + let mut obj: *mut ffi::PyObject = std::ptr::null_mut(); + match unsafe { ffi::compat::PyWeakref_GetRef(self.as_ptr(), &mut obj) } { + std::os::raw::c_int::MIN..=-1 => panic!("The 'weakref.ProxyType' (or `weakref.CallableProxyType`) instance should be valid (non-null and actually a weakref reference)"), + 0 => PyNone::get(self.py()).to_owned().into_any(), + 1..=std::os::raw::c_int::MAX => unsafe { obj.assume_owned(self.py()) }, + } } } @@ -359,41 +362,6 @@ mod tests { }) } - #[test] - fn test_weakref_upgrade_borrowed_as() -> PyResult<()> { - Python::with_gil(|py| { - let class = get_type(py)?; - let object = class.call0()?; - let reference = PyWeakrefProxy::new(&object)?; - - { - // This test is a bit weird but ok. - let obj = reference.upgrade_borrowed_as::(); - - assert!(obj.is_ok()); - let obj = obj.unwrap(); - - assert!(obj.is_some()); - assert!(obj.map_or(false, |obj| obj.as_ptr() == object.as_ptr() - && obj.is_exact_instance(&class))); - } - - drop(object); - - { - // This test is a bit weird but ok. - let obj = reference.upgrade_borrowed_as::(); - - assert!(obj.is_ok()); - let obj = obj.unwrap(); - - assert!(obj.is_none()); - } - - Ok(()) - }) - } - #[test] fn test_weakref_upgrade_as_unchecked() -> PyResult<()> { Python::with_gil(|py| { @@ -423,35 +391,6 @@ mod tests { }) } - #[test] - fn test_weakref_upgrade_borrowed_as_unchecked() -> PyResult<()> { - Python::with_gil(|py| { - let class = get_type(py)?; - let object = class.call0()?; - let reference = PyWeakrefProxy::new(&object)?; - - { - // This test is a bit weird but ok. - let obj = unsafe { reference.upgrade_borrowed_as_unchecked::() }; - - assert!(obj.is_some()); - assert!(obj.map_or(false, |obj| obj.as_ptr() == object.as_ptr() - && obj.is_exact_instance(&class))); - } - - drop(object); - - { - // This test is a bit weird but ok. - let obj = unsafe { reference.upgrade_borrowed_as_unchecked::() }; - - assert!(obj.is_none()); - } - - Ok(()) - }) - } - #[test] fn test_weakref_upgrade() -> PyResult<()> { Python::with_gil(|py| { @@ -470,26 +409,6 @@ mod tests { }) } - #[test] - fn test_weakref_upgrade_borrowed() -> PyResult<()> { - Python::with_gil(|py| { - let class = get_type(py)?; - let object = class.call0()?; - let reference = PyWeakrefProxy::new(&object)?; - - assert!(reference.upgrade_borrowed().is_some()); - assert!(reference - .upgrade_borrowed() - .map_or(false, |obj| obj.is(&object))); - - drop(object); - - assert!(reference.upgrade_borrowed().is_none()); - - Ok(()) - }) - } - #[test] fn test_weakref_get_object() -> PyResult<()> { Python::with_gil(|py| { @@ -506,23 +425,6 @@ mod tests { Ok(()) }) } - - #[test] - fn test_weakref_get_object_borrowed() -> PyResult<()> { - Python::with_gil(|py| { - let class = get_type(py)?; - let object = class.call0()?; - let reference = PyWeakrefProxy::new(&object)?; - - assert!(reference.get_object_borrowed().is(&object)); - - drop(object); - - assert!(reference.get_object_borrowed().is_none()); - - Ok(()) - }) - } } // under 'abi3-py37' and 'abi3-py38' PyClass cannot be weakreferencable. @@ -629,37 +531,6 @@ mod tests { }) } - #[test] - fn test_weakref_upgrade_borrowed_as() -> PyResult<()> { - Python::with_gil(|py| { - let object = Py::new(py, WeakrefablePyClass {})?; - let reference = PyWeakrefProxy::new(object.bind(py))?; - - { - let obj = reference.upgrade_borrowed_as::(); - - assert!(obj.is_ok()); - let obj = obj.unwrap(); - - assert!(obj.is_some()); - assert!(obj.map_or(false, |obj| obj.as_ptr() == object.as_ptr())); - } - - drop(object); - - { - let obj = reference.upgrade_borrowed_as::(); - - assert!(obj.is_ok()); - let obj = obj.unwrap(); - - assert!(obj.is_none()); - } - - Ok(()) - }) - } - #[test] fn test_weakref_upgrade_as_unchecked() -> PyResult<()> { Python::with_gil(|py| { @@ -685,35 +556,6 @@ mod tests { }) } - #[test] - fn test_weakref_upgrade_borrowed_as_unchecked() -> PyResult<()> { - Python::with_gil(|py| { - let object = Py::new(py, WeakrefablePyClass {})?; - let reference = PyWeakrefProxy::new(object.bind(py))?; - - { - let obj = unsafe { - reference.upgrade_borrowed_as_unchecked::() - }; - - assert!(obj.is_some()); - assert!(obj.map_or(false, |obj| obj.as_ptr() == object.as_ptr())); - } - - drop(object); - - { - let obj = unsafe { - reference.upgrade_borrowed_as_unchecked::() - }; - - assert!(obj.is_none()); - } - - Ok(()) - }) - } - #[test] fn test_weakref_upgrade() -> PyResult<()> { Python::with_gil(|py| { @@ -731,25 +573,6 @@ mod tests { }) } - #[test] - fn test_weakref_upgrade_borrowed() -> PyResult<()> { - Python::with_gil(|py| { - let object = Py::new(py, WeakrefablePyClass {})?; - let reference = PyWeakrefProxy::new(object.bind(py))?; - - assert!(reference.upgrade_borrowed().is_some()); - assert!(reference - .upgrade_borrowed() - .map_or(false, |obj| obj.is(&object))); - - drop(object); - - assert!(reference.upgrade_borrowed().is_none()); - - Ok(()) - }) - } - #[test] fn test_weakref_get_object() -> PyResult<()> { Python::with_gil(|py| { @@ -765,22 +588,6 @@ mod tests { Ok(()) }) } - - #[test] - fn test_weakref_get_object_borrowed() -> PyResult<()> { - Python::with_gil(|py| { - let object = Py::new(py, WeakrefablePyClass {})?; - let reference = PyWeakrefProxy::new(object.bind(py))?; - - assert!(reference.get_object_borrowed().is(&object)); - - drop(object); - - assert!(reference.get_object_borrowed().is_none()); - - Ok(()) - }) - } } } @@ -892,41 +699,6 @@ mod tests { }) } - #[test] - fn test_weakref_upgrade_borrowed_as() -> PyResult<()> { - Python::with_gil(|py| { - let class = get_type(py)?; - let object = class.call0()?; - let reference = PyWeakrefProxy::new(&object)?; - - { - // This test is a bit weird but ok. - let obj = reference.upgrade_borrowed_as::(); - - assert!(obj.is_ok()); - let obj = obj.unwrap(); - - assert!(obj.is_some()); - assert!(obj.map_or(false, |obj| obj.as_ptr() == object.as_ptr() - && obj.is_exact_instance(&class))); - } - - drop(object); - - { - // This test is a bit weird but ok. - let obj = reference.upgrade_borrowed_as::(); - - assert!(obj.is_ok()); - let obj = obj.unwrap(); - - assert!(obj.is_none()); - } - - Ok(()) - }) - } - #[test] fn test_weakref_upgrade_as_unchecked() -> PyResult<()> { Python::with_gil(|py| { @@ -956,35 +728,6 @@ mod tests { }) } - #[test] - fn test_weakref_upgrade_borrowed_as_unchecked() -> PyResult<()> { - Python::with_gil(|py| { - let class = get_type(py)?; - let object = class.call0()?; - let reference = PyWeakrefProxy::new(&object)?; - - { - // This test is a bit weird but ok. - let obj = unsafe { reference.upgrade_borrowed_as_unchecked::() }; - - assert!(obj.is_some()); - assert!(obj.map_or(false, |obj| obj.as_ptr() == object.as_ptr() - && obj.is_exact_instance(&class))); - } - - drop(object); - - { - // This test is a bit weird but ok. - let obj = unsafe { reference.upgrade_borrowed_as_unchecked::() }; - - assert!(obj.is_none()); - } - - Ok(()) - }) - } - #[test] fn test_weakref_upgrade() -> PyResult<()> { Python::with_gil(|py| { @@ -1003,26 +746,6 @@ mod tests { }) } - #[test] - fn test_weakref_upgrade_borrowed() -> PyResult<()> { - Python::with_gil(|py| { - let class = get_type(py)?; - let object = class.call0()?; - let reference = PyWeakrefProxy::new(&object)?; - - assert!(reference.upgrade_borrowed().is_some()); - assert!(reference - .upgrade_borrowed() - .map_or(false, |obj| obj.is(&object))); - - drop(object); - - assert!(reference.upgrade_borrowed().is_none()); - - Ok(()) - }) - } - #[test] fn test_weakref_get_object() -> PyResult<()> { Python::with_gil(|py| { @@ -1039,23 +762,6 @@ mod tests { Ok(()) }) } - - #[test] - fn test_weakref_get_object_borrowed() -> PyResult<()> { - Python::with_gil(|py| { - let class = get_type(py)?; - let object = class.call0()?; - let reference = PyWeakrefProxy::new(&object)?; - - assert!(reference.get_object_borrowed().is(&object)); - - drop(object); - - assert!(reference.get_object_borrowed().is_none()); - - Ok(()) - }) - } } // under 'abi3-py37' and 'abi3-py38' PyClass cannot be weakreferencable. @@ -1156,36 +862,6 @@ mod tests { Ok(()) }) } - #[test] - fn test_weakref_upgrade_borrowed_as() -> PyResult<()> { - Python::with_gil(|py| { - let object = Py::new(py, WeakrefablePyClass {})?; - let reference = PyWeakrefProxy::new(object.bind(py))?; - - { - let obj = reference.upgrade_borrowed_as::(); - - assert!(obj.is_ok()); - let obj = obj.unwrap(); - - assert!(obj.is_some()); - assert!(obj.map_or(false, |obj| obj.as_ptr() == object.as_ptr())); - } - - drop(object); - - { - let obj = reference.upgrade_borrowed_as::(); - - assert!(obj.is_ok()); - let obj = obj.unwrap(); - - assert!(obj.is_none()); - } - - Ok(()) - }) - } #[test] fn test_weakref_upgrade_as_unchecked() -> PyResult<()> { @@ -1211,34 +887,6 @@ mod tests { Ok(()) }) } - #[test] - fn test_weakref_upgrade_borrowed_as_unchecked() -> PyResult<()> { - Python::with_gil(|py| { - let object = Py::new(py, WeakrefablePyClass {})?; - let reference = PyWeakrefProxy::new(object.bind(py))?; - - { - let obj = unsafe { - reference.upgrade_borrowed_as_unchecked::() - }; - - assert!(obj.is_some()); - assert!(obj.map_or(false, |obj| obj.as_ptr() == object.as_ptr())); - } - - drop(object); - - { - let obj = unsafe { - reference.upgrade_borrowed_as_unchecked::() - }; - - assert!(obj.is_none()); - } - - Ok(()) - }) - } #[test] fn test_weakref_upgrade() -> PyResult<()> { @@ -1257,25 +905,6 @@ mod tests { }) } - #[test] - fn test_weakref_upgrade_borrowed() -> PyResult<()> { - Python::with_gil(|py| { - let object = Py::new(py, WeakrefablePyClass {})?; - let reference = PyWeakrefProxy::new(object.bind(py))?; - - assert!(reference.upgrade_borrowed().is_some()); - assert!(reference - .upgrade_borrowed() - .map_or(false, |obj| obj.is(&object))); - - drop(object); - - assert!(reference.upgrade_borrowed().is_none()); - - Ok(()) - }) - } - #[test] fn test_weakref_get_object() -> PyResult<()> { Python::with_gil(|py| { @@ -1291,22 +920,6 @@ mod tests { Ok(()) }) } - - #[test] - fn test_weakref_get_object_borrowed() -> PyResult<()> { - Python::with_gil(|py| { - let object = Py::new(py, WeakrefablePyClass {})?; - let reference = PyWeakrefProxy::new(object.bind(py))?; - - assert!(reference.get_object_borrowed().is(&object)); - - drop(object); - - assert!(reference.get_object_borrowed().is_none()); - - Ok(()) - }) - } } } } diff --git a/src/types/weakref/reference.rs b/src/types/weakref/reference.rs index deb62465ccc..cc8bc3d55f5 100644 --- a/src/types/weakref/reference.rs +++ b/src/types/weakref/reference.rs @@ -1,8 +1,8 @@ use crate::err::PyResult; use crate::ffi_ptr_ext::FfiPtrExt; use crate::py_result_ext::PyResultExt; -use crate::types::any::PyAny; -use crate::{ffi, Borrowed, Bound, ToPyObject}; +use crate::types::{any::PyAny, PyNone}; +use crate::{ffi, Bound, ToPyObject}; #[cfg(any(PyPy, GraalPy, Py_LIMITED_API))] use crate::type_object::PyTypeCheck; @@ -191,10 +191,13 @@ impl PyWeakrefReference { } impl<'py> PyWeakrefMethods<'py> for Bound<'py, PyWeakrefReference> { - fn get_object_borrowed(&self) -> Borrowed<'_, 'py, PyAny> { - // PyWeakref_GetObject does some error checking, however we ensure the passed object is Non-Null and a Weakref type. - unsafe { ffi::PyWeakref_GetObject(self.as_ptr()).assume_borrowed_or_err(self.py()) } - .expect("The 'weakref.ReferenceType' instance should be valid (non-null and actually a weakref reference)") + fn get_object(&self) -> Bound<'py, PyAny> { + let mut obj: *mut ffi::PyObject = std::ptr::null_mut(); + match unsafe { ffi::compat::PyWeakref_GetRef(self.as_ptr(), &mut obj) } { + std::os::raw::c_int::MIN..=-1 => panic!("The 'weakref.ReferenceType' instance should be valid (non-null and actually a weakref reference)"), + 0 => PyNone::get(self.py()).to_owned().into_any(), + 1..=std::os::raw::c_int::MAX => unsafe { obj.assume_owned(self.py()) }, + } } } @@ -334,41 +337,6 @@ mod tests { }) } - #[test] - fn test_weakref_upgrade_borrowed_as() -> PyResult<()> { - Python::with_gil(|py| { - let class = get_type(py)?; - let object = class.call0()?; - let reference = PyWeakrefReference::new(&object)?; - - { - // This test is a bit weird but ok. - let obj = reference.upgrade_borrowed_as::(); - - assert!(obj.is_ok()); - let obj = obj.unwrap(); - - assert!(obj.is_some()); - assert!(obj.map_or(false, |obj| obj.as_ptr() == object.as_ptr() - && obj.is_exact_instance(&class))); - } - - drop(object); - - { - // This test is a bit weird but ok. - let obj = reference.upgrade_borrowed_as::(); - - assert!(obj.is_ok()); - let obj = obj.unwrap(); - - assert!(obj.is_none()); - } - - Ok(()) - }) - } - #[test] fn test_weakref_upgrade_as_unchecked() -> PyResult<()> { Python::with_gil(|py| { @@ -398,35 +366,6 @@ mod tests { }) } - #[test] - fn test_weakref_upgrade_borrowed_as_unchecked() -> PyResult<()> { - Python::with_gil(|py| { - let class = get_type(py)?; - let object = class.call0()?; - let reference = PyWeakrefReference::new(&object)?; - - { - // This test is a bit weird but ok. - let obj = unsafe { reference.upgrade_borrowed_as_unchecked::() }; - - assert!(obj.is_some()); - assert!(obj.map_or(false, |obj| obj.as_ptr() == object.as_ptr() - && obj.is_exact_instance(&class))); - } - - drop(object); - - { - // This test is a bit weird but ok. - let obj = unsafe { reference.upgrade_borrowed_as_unchecked::() }; - - assert!(obj.is_none()); - } - - Ok(()) - }) - } - #[test] fn test_weakref_upgrade() -> PyResult<()> { Python::with_gil(|py| { @@ -447,28 +386,6 @@ mod tests { }) } - #[test] - fn test_weakref_upgrade_borrowed() -> PyResult<()> { - Python::with_gil(|py| { - let class = get_type(py)?; - let object = class.call0()?; - let reference = PyWeakrefReference::new(&object)?; - - assert!(reference.call0()?.is(&object)); - assert!(reference.upgrade_borrowed().is_some()); - assert!(reference - .upgrade_borrowed() - .map_or(false, |obj| obj.is(&object))); - - drop(object); - - assert!(reference.call0()?.is_none()); - assert!(reference.upgrade_borrowed().is_none()); - - Ok(()) - }) - } - #[test] fn test_weakref_get_object() -> PyResult<()> { Python::with_gil(|py| { @@ -488,25 +405,6 @@ mod tests { Ok(()) }) } - - #[test] - fn test_weakref_get_object_borrowed() -> PyResult<()> { - Python::with_gil(|py| { - let class = get_type(py)?; - let object = class.call0()?; - let reference = PyWeakrefReference::new(&object)?; - - assert!(reference.call0()?.is(&object)); - assert!(reference.get_object_borrowed().is(&object)); - - drop(object); - - assert!(reference.call0()?.is_none()); - assert!(reference.get_object_borrowed().is_none()); - - Ok(()) - }) - } } // under 'abi3-py37' and 'abi3-py38' PyClass cannot be weakreferencable. @@ -588,37 +486,6 @@ mod tests { }) } - #[test] - fn test_weakref_upgrade_borrowed_as() -> PyResult<()> { - Python::with_gil(|py| { - let object = Py::new(py, WeakrefablePyClass {})?; - let reference = PyWeakrefReference::new(object.bind(py))?; - - { - let obj = reference.upgrade_borrowed_as::(); - - assert!(obj.is_ok()); - let obj = obj.unwrap(); - - assert!(obj.is_some()); - assert!(obj.map_or(false, |obj| obj.as_ptr() == object.as_ptr())); - } - - drop(object); - - { - let obj = reference.upgrade_borrowed_as::(); - - assert!(obj.is_ok()); - let obj = obj.unwrap(); - - assert!(obj.is_none()); - } - - Ok(()) - }) - } - #[test] fn test_weakref_upgrade_as_unchecked() -> PyResult<()> { Python::with_gil(|py| { @@ -644,33 +511,6 @@ mod tests { }) } - #[test] - fn test_weakref_upgrade_borrowed_as_unchecked() -> PyResult<()> { - Python::with_gil(|py| { - let object = Py::new(py, WeakrefablePyClass {})?; - let reference = PyWeakrefReference::new(object.bind(py))?; - - { - let obj = - unsafe { reference.upgrade_borrowed_as_unchecked::() }; - - assert!(obj.is_some()); - assert!(obj.map_or(false, |obj| obj.as_ptr() == object.as_ptr())); - } - - drop(object); - - { - let obj = - unsafe { reference.upgrade_borrowed_as_unchecked::() }; - - assert!(obj.is_none()); - } - - Ok(()) - }) - } - #[test] fn test_weakref_upgrade() -> PyResult<()> { Python::with_gil(|py| { @@ -690,27 +530,6 @@ mod tests { }) } - #[test] - fn test_weakref_upgrade_borrowed() -> PyResult<()> { - Python::with_gil(|py| { - let object = Py::new(py, WeakrefablePyClass {})?; - let reference = PyWeakrefReference::new(object.bind(py))?; - - assert!(reference.call0()?.is(&object)); - assert!(reference.upgrade_borrowed().is_some()); - assert!(reference - .upgrade_borrowed() - .map_or(false, |obj| obj.is(&object))); - - drop(object); - - assert!(reference.call0()?.is_none()); - assert!(reference.upgrade_borrowed().is_none()); - - Ok(()) - }) - } - #[test] fn test_weakref_get_object() -> PyResult<()> { Python::with_gil(|py| { @@ -729,23 +548,5 @@ mod tests { Ok(()) }) } - - #[test] - fn test_weakref_get_object_borrowed() -> PyResult<()> { - Python::with_gil(|py| { - let object = Py::new(py, WeakrefablePyClass {})?; - let reference = PyWeakrefReference::new(object.bind(py))?; - - assert!(reference.call0()?.is(&object)); - assert!(reference.get_object_borrowed().is(&object)); - - drop(object); - - assert!(reference.call0()?.is_none()); - assert!(reference.get_object_borrowed().is_none()); - - Ok(()) - }) - } } } From 95949944554354cdbf3275daa03818a6f5b65c33 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Wed, 2 Oct 2024 22:45:23 +0100 Subject: [PATCH 296/495] ci: fix benchmarks build (#4591) --- pyo3-benches/Cargo.toml | 4 +++ pyo3-benches/benches/bench_any.rs | 4 +-- pyo3-benches/benches/bench_bigint.rs | 10 +++--- pyo3-benches/benches/bench_call.rs | 4 +-- pyo3-benches/benches/bench_decimal.rs | 4 +-- pyo3-benches/benches/bench_dict.rs | 40 ++++++++++++++++------ pyo3-benches/benches/bench_extract.rs | 6 ++-- pyo3-benches/benches/bench_frompyobject.rs | 8 ++--- pyo3-benches/benches/bench_intern.rs | 4 +-- pyo3-benches/benches/bench_list.rs | 4 +-- pyo3-benches/benches/bench_set.rs | 13 +++---- pyo3-benches/benches/bench_tuple.rs | 18 +++++----- pyo3-benches/build.rs | 3 ++ 13 files changed, 73 insertions(+), 49 deletions(-) create mode 100644 pyo3-benches/build.rs diff --git a/pyo3-benches/Cargo.toml b/pyo3-benches/Cargo.toml index 7a7f17d276b..4b4add8a542 100644 --- a/pyo3-benches/Cargo.toml +++ b/pyo3-benches/Cargo.toml @@ -9,11 +9,15 @@ publish = false [dependencies] pyo3 = { path = "../", features = ["auto-initialize", "full"] } +[build-dependencies] +pyo3-build-config = { path = "../pyo3-build-config" } + [dev-dependencies] codspeed-criterion-compat = "2.3" criterion = "0.5.1" num-bigint = "0.4.3" rust_decimal = { version = "1.0.0", default-features = false } +hashbrown = "0.14" [[bench]] name = "bench_any" diff --git a/pyo3-benches/benches/bench_any.rs b/pyo3-benches/benches/bench_any.rs index b77ab9567a6..af2d226b3aa 100644 --- a/pyo3-benches/benches/bench_any.rs +++ b/pyo3-benches/benches/bench_any.rs @@ -63,7 +63,7 @@ fn find_object_type(obj: &Bound<'_, PyAny>) -> ObjectType { fn bench_identify_object_type(b: &mut Bencher<'_>) { Python::with_gil(|py| { - let obj = py.eval_bound("object()", None, None).unwrap(); + let obj = py.eval(c"object()", None, None).unwrap(); b.iter(|| find_object_type(&obj)); @@ -73,7 +73,7 @@ fn bench_identify_object_type(b: &mut Bencher<'_>) { fn bench_collect_generic_iterator(b: &mut Bencher<'_>) { Python::with_gil(|py| { - let collection = py.eval_bound("list(range(1 << 20))", None, None).unwrap(); + let collection = py.eval(c"list(range(1 << 20))", None, None).unwrap(); b.iter(|| { collection diff --git a/pyo3-benches/benches/bench_bigint.rs b/pyo3-benches/benches/bench_bigint.rs index 2e585a504ff..d2c78f0ad4e 100644 --- a/pyo3-benches/benches/bench_bigint.rs +++ b/pyo3-benches/benches/bench_bigint.rs @@ -19,7 +19,7 @@ fn extract_bigint_extract_fail(bench: &mut Bencher<'_>) { fn extract_bigint_small(bench: &mut Bencher<'_>) { Python::with_gil(|py| { - let int = py.eval_bound("-42", None, None).unwrap(); + let int = py.eval(c"-42", None, None).unwrap(); bench.iter_with_large_drop(|| black_box(&int).extract::().unwrap()); }); @@ -27,7 +27,7 @@ fn extract_bigint_small(bench: &mut Bencher<'_>) { fn extract_bigint_big_negative(bench: &mut Bencher<'_>) { Python::with_gil(|py| { - let int = py.eval_bound("-10**300", None, None).unwrap(); + let int = py.eval(c"-10**300", None, None).unwrap(); bench.iter_with_large_drop(|| black_box(&int).extract::().unwrap()); }); @@ -35,7 +35,7 @@ fn extract_bigint_big_negative(bench: &mut Bencher<'_>) { fn extract_bigint_big_positive(bench: &mut Bencher<'_>) { Python::with_gil(|py| { - let int = py.eval_bound("10**300", None, None).unwrap(); + let int = py.eval(c"10**300", None, None).unwrap(); bench.iter_with_large_drop(|| black_box(&int).extract::().unwrap()); }); @@ -43,7 +43,7 @@ fn extract_bigint_big_positive(bench: &mut Bencher<'_>) { fn extract_bigint_huge_negative(bench: &mut Bencher<'_>) { Python::with_gil(|py| { - let int = py.eval_bound("-10**3000", None, None).unwrap(); + let int = py.eval(c"-10**3000", None, None).unwrap(); bench.iter_with_large_drop(|| black_box(&int).extract::().unwrap()); }); @@ -51,7 +51,7 @@ fn extract_bigint_huge_negative(bench: &mut Bencher<'_>) { fn extract_bigint_huge_positive(bench: &mut Bencher<'_>) { Python::with_gil(|py| { - let int = py.eval_bound("10**3000", None, None).unwrap(); + let int = py.eval(c"10**3000", None, None).unwrap(); bench.iter_with_large_drop(|| black_box(&int).extract::().unwrap()); }); diff --git a/pyo3-benches/benches/bench_call.rs b/pyo3-benches/benches/bench_call.rs index ca8c17093af..bbcf3ac2bdf 100644 --- a/pyo3-benches/benches/bench_call.rs +++ b/pyo3-benches/benches/bench_call.rs @@ -56,7 +56,7 @@ fn bench_call(b: &mut Bencher<'_>) { <_ as IntoPy>::into_py("s", py).into_bound(py), <_ as IntoPy>::into_py(1.23, py).into_bound(py), ); - let kwargs = [("d", 1), ("e", 42)].into_py_dict(py); + let kwargs = [("d", 1), ("e", 42)].into_py_dict(py).unwrap(); b.iter(|| { for _ in 0..1000 { @@ -149,7 +149,7 @@ class Foo: <_ as IntoPy>::into_py("s", py).into_bound(py), <_ as IntoPy>::into_py(1.23, py).into_bound(py), ); - let kwargs = [("d", 1), ("e", 42)].into_py_dict(py); + let kwargs = [("d", 1), ("e", 42)].into_py_dict(py).unwrap(); b.iter(|| { for _ in 0..1000 { diff --git a/pyo3-benches/benches/bench_decimal.rs b/pyo3-benches/benches/bench_decimal.rs index 49eee023deb..65fb47e23f7 100644 --- a/pyo3-benches/benches/bench_decimal.rs +++ b/pyo3-benches/benches/bench_decimal.rs @@ -9,8 +9,8 @@ use pyo3::types::PyDict; fn decimal_via_extract(b: &mut Bencher<'_>) { Python::with_gil(|py| { let locals = PyDict::new(py); - py.run_bound( - r#" + py.run( + cr#" import decimal py_dec = decimal.Decimal("0.0") "#, diff --git a/pyo3-benches/benches/bench_dict.rs b/pyo3-benches/benches/bench_dict.rs index 8d79de8a8e4..6a92cf21c5f 100644 --- a/pyo3-benches/benches/bench_dict.rs +++ b/pyo3-benches/benches/bench_dict.rs @@ -9,7 +9,10 @@ use pyo3::{prelude::*, types::PyMapping}; fn iter_dict(b: &mut Bencher<'_>) { Python::with_gil(|py| { const LEN: usize = 100_000; - let dict = (0..LEN as u64).map(|i| (i, i * 2)).into_py_dict(py); + let dict = (0..LEN as u64) + .map(|i| (i, i * 2)) + .into_py_dict(py) + .unwrap(); let mut sum = 0; b.iter(|| { for (k, _v) in &dict { @@ -23,14 +26,22 @@ fn iter_dict(b: &mut Bencher<'_>) { fn dict_new(b: &mut Bencher<'_>) { Python::with_gil(|py| { const LEN: usize = 50_000; - b.iter_with_large_drop(|| (0..LEN as u64).map(|i| (i, i * 2)).into_py_dict(py)); + b.iter_with_large_drop(|| { + (0..LEN as u64) + .map(|i| (i, i * 2)) + .into_py_dict(py) + .unwrap() + }); }); } fn dict_get_item(b: &mut Bencher<'_>) { Python::with_gil(|py| { const LEN: usize = 50_000; - let dict = (0..LEN as u64).map(|i| (i, i * 2)).into_py_dict(py); + let dict = (0..LEN as u64) + .map(|i| (i, i * 2)) + .into_py_dict(py) + .unwrap(); let mut sum = 0; b.iter(|| { for i in 0..LEN { @@ -48,7 +59,10 @@ fn dict_get_item(b: &mut Bencher<'_>) { fn extract_hashmap(b: &mut Bencher<'_>) { Python::with_gil(|py| { const LEN: usize = 100_000; - let dict = (0..LEN as u64).map(|i| (i, i * 2)).into_py_dict(py); + let dict = (0..LEN as u64) + .map(|i| (i, i * 2)) + .into_py_dict(py) + .unwrap(); b.iter(|| HashMap::::extract_bound(&dict)); }); } @@ -56,16 +70,21 @@ fn extract_hashmap(b: &mut Bencher<'_>) { fn extract_btreemap(b: &mut Bencher<'_>) { Python::with_gil(|py| { const LEN: usize = 100_000; - let dict = (0..LEN as u64).map(|i| (i, i * 2)).into_py_dict(py); + let dict = (0..LEN as u64) + .map(|i| (i, i * 2)) + .into_py_dict(py) + .unwrap(); b.iter(|| BTreeMap::::extract_bound(&dict)); }); } -#[cfg(feature = "hashbrown")] fn extract_hashbrown_map(b: &mut Bencher<'_>) { Python::with_gil(|py| { const LEN: usize = 100_000; - let dict = (0..LEN as u64).map(|i| (i, i * 2)).into_py_dict(py); + let dict = (0..LEN as u64) + .map(|i| (i, i * 2)) + .into_py_dict(py) + .unwrap(); b.iter(|| hashbrown::HashMap::::extract_bound(&dict)); }); } @@ -73,7 +92,10 @@ fn extract_hashbrown_map(b: &mut Bencher<'_>) { fn mapping_from_dict(b: &mut Bencher<'_>) { Python::with_gil(|py| { const LEN: usize = 100_000; - let dict = &(0..LEN as u64).map(|i| (i, i * 2)).into_py_dict(py); + let dict = &(0..LEN as u64) + .map(|i| (i, i * 2)) + .into_py_dict(py) + .unwrap(); b.iter(|| black_box(dict).downcast::().unwrap()); }); } @@ -85,8 +107,6 @@ fn criterion_benchmark(c: &mut Criterion) { c.bench_function("extract_hashmap", extract_hashmap); c.bench_function("extract_btreemap", extract_btreemap); c.bench_function("mapping_from_dict", mapping_from_dict); - - #[cfg(feature = "hashbrown")] c.bench_function("extract_hashbrown_map", extract_hashbrown_map); } diff --git a/pyo3-benches/benches/bench_extract.rs b/pyo3-benches/benches/bench_extract.rs index c69069c1b57..a261b14cfa1 100644 --- a/pyo3-benches/benches/bench_extract.rs +++ b/pyo3-benches/benches/bench_extract.rs @@ -9,7 +9,7 @@ use pyo3::{ fn extract_str_extract_success(bench: &mut Bencher<'_>) { Python::with_gil(|py| { - let s = PyString::new_bound(py, "Hello, World!").into_any(); + let s = PyString::new(py, "Hello, World!").into_any(); bench.iter(|| black_box(&s).extract::<&str>().unwrap()); }); @@ -26,10 +26,10 @@ fn extract_str_extract_fail(bench: &mut Bencher<'_>) { }); } -#[cfg(any(Py_3_10, not(Py_LIMITED_API)))] +#[cfg(any(Py_3_10))] fn extract_str_downcast_success(bench: &mut Bencher<'_>) { Python::with_gil(|py| { - let s = PyString::new_bound(py, "Hello, World!").into_any(); + let s = PyString::new(py, "Hello, World!").into_any(); bench.iter(|| { let py_str = black_box(&s).downcast::().unwrap(); diff --git a/pyo3-benches/benches/bench_frompyobject.rs b/pyo3-benches/benches/bench_frompyobject.rs index 3cb21639b68..6411a391f9a 100644 --- a/pyo3-benches/benches/bench_frompyobject.rs +++ b/pyo3-benches/benches/bench_frompyobject.rs @@ -17,7 +17,7 @@ enum ManyTypes { fn enum_from_pyobject(b: &mut Bencher<'_>) { Python::with_gil(|py| { - let any = PyString::new_bound(py, "hello world").into_any(); + let any = PyString::new(py, "hello world").into_any(); b.iter(|| black_box(&any).extract::().unwrap()); }) @@ -41,7 +41,7 @@ fn list_via_extract(b: &mut Bencher<'_>) { fn not_a_list_via_downcast(b: &mut Bencher<'_>) { Python::with_gil(|py| { - let any = PyString::new_bound(py, "foobar").into_any(); + let any = PyString::new(py, "foobar").into_any(); b.iter(|| black_box(&any).downcast::().unwrap_err()); }) @@ -49,7 +49,7 @@ fn not_a_list_via_downcast(b: &mut Bencher<'_>) { fn not_a_list_via_extract(b: &mut Bencher<'_>) { Python::with_gil(|py| { - let any = PyString::new_bound(py, "foobar").into_any(); + let any = PyString::new(py, "foobar").into_any(); b.iter(|| black_box(&any).extract::>().unwrap_err()); }) @@ -63,7 +63,7 @@ enum ListOrNotList<'a> { fn not_a_list_via_extract_enum(b: &mut Bencher<'_>) { Python::with_gil(|py| { - let any = PyString::new_bound(py, "foobar").into_any(); + let any = PyString::new(py, "foobar").into_any(); b.iter(|| match black_box(&any).extract::>() { Ok(ListOrNotList::List(_list)) => panic!(), diff --git a/pyo3-benches/benches/bench_intern.rs b/pyo3-benches/benches/bench_intern.rs index f9f9162a5ee..1b7dc07370a 100644 --- a/pyo3-benches/benches/bench_intern.rs +++ b/pyo3-benches/benches/bench_intern.rs @@ -8,7 +8,7 @@ use pyo3::intern; fn getattr_direct(b: &mut Bencher<'_>) { Python::with_gil(|py| { - let sys = &py.import_bound("sys").unwrap(); + let sys = &py.import("sys").unwrap(); b.iter(|| black_box(sys).getattr("version").unwrap()); }); @@ -16,7 +16,7 @@ fn getattr_direct(b: &mut Bencher<'_>) { fn getattr_intern(b: &mut Bencher<'_>) { Python::with_gil(|py| { - let sys = &py.import_bound("sys").unwrap(); + let sys = &py.import("sys").unwrap(); b.iter(|| black_box(sys).getattr(intern!(py, "version")).unwrap()); }); diff --git a/pyo3-benches/benches/bench_list.rs b/pyo3-benches/benches/bench_list.rs index 39829f52ce8..cc790db37bf 100644 --- a/pyo3-benches/benches/bench_list.rs +++ b/pyo3-benches/benches/bench_list.rs @@ -39,7 +39,7 @@ fn list_get_item(b: &mut Bencher<'_>) { }); } -#[cfg(not(Py_LIMITED_API))] +#[cfg(not(any(Py_LIMITED_API, Py_GIL_DISABLED)))] fn list_get_item_unchecked(b: &mut Bencher<'_>) { Python::with_gil(|py| { const LEN: usize = 50_000; @@ -67,7 +67,7 @@ fn criterion_benchmark(c: &mut Criterion) { c.bench_function("iter_list", iter_list); c.bench_function("list_new", list_new); c.bench_function("list_get_item", list_get_item); - #[cfg(not(Py_LIMITED_API))] + #[cfg(not(any(Py_LIMITED_API, Py_GIL_DISABLED)))] c.bench_function("list_get_item_unchecked", list_get_item_unchecked); c.bench_function("sequence_from_list", sequence_from_list); } diff --git a/pyo3-benches/benches/bench_set.rs b/pyo3-benches/benches/bench_set.rs index 18134a15bd5..0be06309595 100644 --- a/pyo3-benches/benches/bench_set.rs +++ b/pyo3-benches/benches/bench_set.rs @@ -13,14 +13,14 @@ fn set_new(b: &mut Bencher<'_>) { // Create Python objects up-front, so that the benchmark doesn't need to include // the cost of allocating LEN Python integers let elements: Vec = (0..LEN).map(|i| i.into_py(py)).collect(); - b.iter_with_large_drop(|| PySet::new_bound(py, &elements).unwrap()); + b.iter_with_large_drop(|| PySet::new(py, &elements).unwrap()); }); } fn iter_set(b: &mut Bencher<'_>) { Python::with_gil(|py| { const LEN: usize = 100_000; - let set = PySet::new_bound(py, &(0..LEN).collect::>()).unwrap(); + let set = PySet::new(py, &(0..LEN).collect::>()).unwrap(); let mut sum = 0; b.iter(|| { for x in &set { @@ -34,7 +34,7 @@ fn iter_set(b: &mut Bencher<'_>) { fn extract_hashset(b: &mut Bencher<'_>) { Python::with_gil(|py| { const LEN: usize = 100_000; - let any = PySet::new_bound(py, &(0..LEN).collect::>()) + let any = PySet::new(py, &(0..LEN).collect::>()) .unwrap() .into_any(); b.iter_with_large_drop(|| black_box(&any).extract::>()); @@ -44,18 +44,17 @@ fn extract_hashset(b: &mut Bencher<'_>) { fn extract_btreeset(b: &mut Bencher<'_>) { Python::with_gil(|py| { const LEN: usize = 100_000; - let any = PySet::new_bound(py, &(0..LEN).collect::>()) + let any = PySet::new(py, &(0..LEN).collect::>()) .unwrap() .into_any(); b.iter_with_large_drop(|| black_box(&any).extract::>()); }); } -#[cfg(feature = "hashbrown")] fn extract_hashbrown_set(b: &mut Bencher<'_>) { Python::with_gil(|py| { const LEN: usize = 100_000; - let any = PySet::new_bound(py, &(0..LEN).collect::>()) + let any = PySet::new(py, &(0..LEN).collect::>()) .unwrap() .into_any(); b.iter_with_large_drop(|| black_box(&any).extract::>()); @@ -67,8 +66,6 @@ fn criterion_benchmark(c: &mut Criterion) { c.bench_function("iter_set", iter_set); c.bench_function("extract_hashset", extract_hashset); c.bench_function("extract_btreeset", extract_btreeset); - - #[cfg(feature = "hashbrown")] c.bench_function("extract_hashbrown_set", extract_hashbrown_set); } diff --git a/pyo3-benches/benches/bench_tuple.rs b/pyo3-benches/benches/bench_tuple.rs index e2985cc998f..a5e43d6ef43 100644 --- a/pyo3-benches/benches/bench_tuple.rs +++ b/pyo3-benches/benches/bench_tuple.rs @@ -8,7 +8,7 @@ use pyo3::types::{PyList, PySequence, PyTuple}; fn iter_tuple(b: &mut Bencher<'_>) { Python::with_gil(|py| { const LEN: usize = 100_000; - let tuple = PyTuple::new_bound(py, 0..LEN); + let tuple = PyTuple::new(py, 0..LEN).unwrap(); let mut sum = 0; b.iter(|| { for x in tuple.iter_borrowed() { @@ -22,14 +22,14 @@ fn iter_tuple(b: &mut Bencher<'_>) { fn tuple_new(b: &mut Bencher<'_>) { Python::with_gil(|py| { const LEN: usize = 50_000; - b.iter_with_large_drop(|| PyTuple::new_bound(py, 0..LEN)); + b.iter_with_large_drop(|| PyTuple::new(py, 0..LEN).unwrap()); }); } fn tuple_get_item(b: &mut Bencher<'_>) { Python::with_gil(|py| { const LEN: usize = 50_000; - let tuple = PyTuple::new_bound(py, 0..LEN); + let tuple = PyTuple::new(py, 0..LEN).unwrap(); let mut sum = 0; b.iter(|| { for i in 0..LEN { @@ -43,7 +43,7 @@ fn tuple_get_item(b: &mut Bencher<'_>) { fn tuple_get_item_unchecked(b: &mut Bencher<'_>) { Python::with_gil(|py| { const LEN: usize = 50_000; - let tuple = PyTuple::new_bound(py, 0..LEN); + let tuple = PyTuple::new(py, 0..LEN).unwrap(); let mut sum = 0; b.iter(|| { for i in 0..LEN { @@ -58,7 +58,7 @@ fn tuple_get_item_unchecked(b: &mut Bencher<'_>) { fn tuple_get_borrowed_item(b: &mut Bencher<'_>) { Python::with_gil(|py| { const LEN: usize = 50_000; - let tuple = PyTuple::new_bound(py, 0..LEN); + let tuple = PyTuple::new(py, 0..LEN).unwrap(); let mut sum = 0; b.iter(|| { for i in 0..LEN { @@ -76,7 +76,7 @@ fn tuple_get_borrowed_item(b: &mut Bencher<'_>) { fn tuple_get_borrowed_item_unchecked(b: &mut Bencher<'_>) { Python::with_gil(|py| { const LEN: usize = 50_000; - let tuple = PyTuple::new_bound(py, 0..LEN); + let tuple = PyTuple::new(py, 0..LEN).unwrap(); let mut sum = 0; b.iter(|| { for i in 0..LEN { @@ -94,7 +94,7 @@ fn tuple_get_borrowed_item_unchecked(b: &mut Bencher<'_>) { fn sequence_from_tuple(b: &mut Bencher<'_>) { Python::with_gil(|py| { const LEN: usize = 50_000; - let tuple = PyTuple::new_bound(py, 0..LEN).into_any(); + let tuple = PyTuple::new(py, 0..LEN).unwrap().into_any(); b.iter(|| black_box(&tuple).downcast::().unwrap()); }); } @@ -102,7 +102,7 @@ fn sequence_from_tuple(b: &mut Bencher<'_>) { fn tuple_new_list(b: &mut Bencher<'_>) { Python::with_gil(|py| { const LEN: usize = 50_000; - let tuple = PyTuple::new_bound(py, 0..LEN); + let tuple = PyTuple::new(py, 0..LEN).unwrap(); b.iter_with_large_drop(|| PyList::new(py, tuple.iter_borrowed())); }); } @@ -110,7 +110,7 @@ fn tuple_new_list(b: &mut Bencher<'_>) { fn tuple_to_list(b: &mut Bencher<'_>) { Python::with_gil(|py| { const LEN: usize = 50_000; - let tuple = PyTuple::new_bound(py, 0..LEN); + let tuple = PyTuple::new(py, 0..LEN).unwrap(); b.iter_with_large_drop(|| tuple.to_list()); }); } diff --git a/pyo3-benches/build.rs b/pyo3-benches/build.rs new file mode 100644 index 00000000000..0475124bb4e --- /dev/null +++ b/pyo3-benches/build.rs @@ -0,0 +1,3 @@ +fn main() { + pyo3_build_config::use_pyo3_cfgs(); +} From e9a1b9b458477e03f79fbb1de4b60ec4e31e691b Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Fri, 4 Oct 2024 12:00:00 +0200 Subject: [PATCH 297/495] add missing `IntoPyObject` impls (#4592) --- src/conversions/std/osstr.rs | 25 ++++++-- src/conversions/std/path.rs | 25 ++++++-- src/pybacked.rs | 91 +++++++++++++++++++++++---- src/types/slice.rs | 22 +++++++ tests/ui/invalid_property_args.stderr | 4 +- tests/ui/missing_intopy.stderr | 4 +- 6 files changed, 147 insertions(+), 24 deletions(-) diff --git a/src/conversions/std/osstr.rs b/src/conversions/std/osstr.rs index c140ef703d4..4f956c64ce8 100644 --- a/src/conversions/std/osstr.rs +++ b/src/conversions/std/osstr.rs @@ -64,6 +64,17 @@ impl<'py> IntoPyObject<'py> for &OsStr { } } +impl<'py> IntoPyObject<'py> for &&OsStr { + type Target = PyString; + type Output = Bound<'py, Self::Target>; + type Error = Infallible; + + #[inline] + fn into_pyobject(self, py: Python<'py>) -> Result { + (*self).into_pyobject(py) + } +} + // There's no FromPyObject implementation for &OsStr because albeit possible on Unix, this would // be impossible to implement on Windows. Hence it's omitted entirely @@ -208,8 +219,10 @@ impl<'py> IntoPyObject<'py> for &OsString { #[cfg(test)] mod tests { + use crate::prelude::IntoPyObject; use crate::types::{PyAnyMethods, PyStringMethods}; - use crate::{types::PyString, IntoPy, PyObject, Python, ToPyObject}; + use crate::BoundObject; + use crate::{types::PyString, IntoPy, PyObject, Python}; use std::fmt::Debug; use std::{ borrow::Cow, @@ -239,9 +252,13 @@ mod tests { #[test] fn test_topyobject_roundtrip() { Python::with_gil(|py| { - fn test_roundtrip + Debug>(py: Python<'_>, obj: T) { - let pyobject = obj.to_object(py); - let pystring = pyobject.downcast_bound::(py).unwrap(); + fn test_roundtrip<'py, T>(py: Python<'py>, obj: T) + where + T: IntoPyObject<'py> + AsRef + Debug + Clone, + T::Error: Debug, + { + let pyobject = obj.clone().into_pyobject(py).unwrap().into_any(); + let pystring = pyobject.as_borrowed().downcast::().unwrap(); assert_eq!(pystring.to_string_lossy(), obj.as_ref().to_string_lossy()); let roundtripped_obj: OsString = pystring.extract().unwrap(); assert_eq!(obj.as_ref(), roundtripped_obj.as_os_str()); diff --git a/src/conversions/std/path.rs b/src/conversions/std/path.rs index 758c4bbe198..f012cc81a27 100644 --- a/src/conversions/std/path.rs +++ b/src/conversions/std/path.rs @@ -44,6 +44,17 @@ impl<'py> IntoPyObject<'py> for &Path { } } +impl<'py> IntoPyObject<'py> for &&Path { + type Target = PyString; + type Output = Bound<'py, Self::Target>; + type Error = Infallible; + + #[inline] + fn into_pyobject(self, py: Python<'py>) -> Result { + (*self).into_pyobject(py) + } +} + impl<'a> ToPyObject for Cow<'a, Path> { #[inline] fn to_object(&self, py: Python<'_>) -> PyObject { @@ -125,8 +136,10 @@ impl<'py> IntoPyObject<'py> for &PathBuf { #[cfg(test)] mod tests { + use crate::prelude::IntoPyObject; use crate::types::{PyAnyMethods, PyStringMethods}; - use crate::{types::PyString, IntoPy, PyObject, Python, ToPyObject}; + use crate::BoundObject; + use crate::{types::PyString, IntoPy, PyObject, Python}; use std::borrow::Cow; use std::fmt::Debug; use std::path::{Path, PathBuf}; @@ -155,9 +168,13 @@ mod tests { #[test] fn test_topyobject_roundtrip() { Python::with_gil(|py| { - fn test_roundtrip + Debug>(py: Python<'_>, obj: T) { - let pyobject = obj.to_object(py); - let pystring = pyobject.downcast_bound::(py).unwrap(); + fn test_roundtrip<'py, T>(py: Python<'py>, obj: T) + where + T: IntoPyObject<'py> + AsRef + Debug + Clone, + T::Error: Debug, + { + let pyobject = obj.clone().into_pyobject(py).unwrap().into_any(); + let pystring = pyobject.as_borrowed().downcast::().unwrap(); assert_eq!(pystring.to_string_lossy(), obj.as_ref().to_string_lossy()); let roundtripped_obj: PathBuf = pystring.extract().unwrap(); assert_eq!(obj.as_ref(), roundtripped_obj.as_path()); diff --git a/src/pybacked.rs b/src/pybacked.rs index b50416062b5..c27383ba9bc 100644 --- a/src/pybacked.rs +++ b/src/pybacked.rs @@ -1,13 +1,16 @@ //! Contains types for working with Python objects that own the underlying data. -use std::{ops::Deref, ptr::NonNull, sync::Arc}; +use std::{convert::Infallible, ops::Deref, ptr::NonNull, sync::Arc}; +#[allow(deprecated)] +use crate::ToPyObject; use crate::{ + prelude::IntoPyObject, types::{ any::PyAnyMethods, bytearray::PyByteArrayMethods, bytes::PyBytesMethods, string::PyStringMethods, PyByteArray, PyBytes, PyString, }, - Bound, DowncastError, FromPyObject, IntoPy, Py, PyAny, PyErr, PyResult, Python, ToPyObject, + Bound, DowncastError, FromPyObject, IntoPy, Py, PyAny, PyErr, PyResult, Python, }; /// A wrapper around `str` where the storage is owned by a Python `bytes` or `str` object. @@ -61,7 +64,7 @@ impl TryFrom> for PyBackedStr { let s = py_string.to_str()?; let data = NonNull::from(s); Ok(Self { - storage: py_string.as_any().to_owned().unbind(), + storage: py_string.into_any().unbind(), data, }) } @@ -85,10 +88,11 @@ impl FromPyObject<'_> for PyBackedStr { } } +#[allow(deprecated)] impl ToPyObject for PyBackedStr { #[cfg(any(Py_3_10, not(Py_LIMITED_API)))] fn to_object(&self, py: Python<'_>) -> Py { - self.storage.clone_ref(py) + self.storage.as_any().clone_ref(py) } #[cfg(not(any(Py_3_10, not(Py_LIMITED_API))))] fn to_object(&self, py: Python<'_>) -> Py { @@ -99,7 +103,7 @@ impl ToPyObject for PyBackedStr { impl IntoPy> for PyBackedStr { #[cfg(any(Py_3_10, not(Py_LIMITED_API)))] fn into_py(self, _py: Python<'_>) -> Py { - self.storage + self.storage.into_any() } #[cfg(not(any(Py_3_10, not(Py_LIMITED_API))))] fn into_py(self, py: Python<'_>) -> Py { @@ -107,6 +111,38 @@ impl IntoPy> for PyBackedStr { } } +impl<'py> IntoPyObject<'py> for PyBackedStr { + type Target = PyAny; + type Output = Bound<'py, Self::Target>; + type Error = Infallible; + + #[cfg(any(Py_3_10, not(Py_LIMITED_API)))] + fn into_pyobject(self, py: Python<'py>) -> Result { + Ok(self.storage.into_bound(py)) + } + + #[cfg(not(any(Py_3_10, not(Py_LIMITED_API))))] + fn into_pyobject(self, py: Python<'py>) -> Result { + Ok(PyString::new(py, &self).into_any()) + } +} + +impl<'py> IntoPyObject<'py> for &PyBackedStr { + type Target = PyAny; + type Output = Bound<'py, Self::Target>; + type Error = Infallible; + + #[cfg(any(Py_3_10, not(Py_LIMITED_API)))] + fn into_pyobject(self, py: Python<'py>) -> Result { + Ok(self.storage.bind(py).to_owned()) + } + + #[cfg(not(any(Py_3_10, not(Py_LIMITED_API))))] + fn into_pyobject(self, py: Python<'py>) -> Result { + Ok(PyString::new(py, self).into_any()) + } +} + /// A wrapper around `[u8]` where the storage is either owned by a Python `bytes` object, or a Rust `Box<[u8]>`. /// /// This type gives access to the underlying data via a `Deref` implementation. @@ -203,6 +239,7 @@ impl FromPyObject<'_> for PyBackedBytes { } } +#[allow(deprecated)] impl ToPyObject for PyBackedBytes { fn to_object(&self, py: Python<'_>) -> Py { match &self.storage { @@ -221,6 +258,32 @@ impl IntoPy> for PyBackedBytes { } } +impl<'py> IntoPyObject<'py> for PyBackedBytes { + type Target = PyBytes; + type Output = Bound<'py, Self::Target>; + type Error = Infallible; + + fn into_pyobject(self, py: Python<'py>) -> Result { + match self.storage { + PyBackedBytesStorage::Python(bytes) => Ok(bytes.into_bound(py)), + PyBackedBytesStorage::Rust(bytes) => Ok(PyBytes::new(py, &bytes)), + } + } +} + +impl<'py> IntoPyObject<'py> for &PyBackedBytes { + type Target = PyBytes; + type Output = Bound<'py, Self::Target>; + type Error = Infallible; + + fn into_pyobject(self, py: Python<'py>) -> Result { + match &self.storage { + PyBackedBytesStorage::Python(bytes) => Ok(bytes.bind(py).clone()), + PyBackedBytesStorage::Rust(bytes) => Ok(PyBytes::new(py, bytes)), + } + } +} + macro_rules! impl_traits { ($slf:ty, $equiv:ty) => { impl std::fmt::Debug for $slf { @@ -297,6 +360,7 @@ use impl_traits; #[cfg(test)] mod test { use super::*; + use crate::prelude::IntoPyObject; use crate::Python; use std::collections::hash_map::DefaultHasher; use std::hash::{Hash, Hasher}; @@ -329,12 +393,12 @@ mod test { } #[test] - fn py_backed_str_to_object() { + fn py_backed_str_into_pyobject() { Python::with_gil(|py| { let orig_str = PyString::new(py, "hello"); let py_backed_str = orig_str.extract::().unwrap(); - let new_str = py_backed_str.to_object(py); - assert_eq!(new_str.extract::(py).unwrap(), "hello"); + let new_str = py_backed_str.into_pyobject(py).unwrap(); + assert_eq!(new_str.extract::().unwrap(), "hello"); #[cfg(any(Py_3_10, not(Py_LIMITED_API)))] assert!(new_str.is(&orig_str)); }); @@ -389,17 +453,20 @@ mod test { } #[test] - fn py_backed_bytes_into_py() { + fn py_backed_bytes_into_pyobject() { Python::with_gil(|py| { let orig_bytes = PyBytes::new(py, b"abcde"); let py_backed_bytes = PyBackedBytes::from(orig_bytes.clone()); - assert!(py_backed_bytes.to_object(py).is(&orig_bytes)); + assert!((&py_backed_bytes) + .into_pyobject(py) + .unwrap() + .is(&orig_bytes)); assert!(py_backed_bytes.into_py(py).is(&orig_bytes)); }); } #[test] - fn rust_backed_bytes_into_py() { + fn rust_backed_bytes_into_pyobject() { Python::with_gil(|py| { let orig_bytes = PyByteArray::new(py, b"abcde"); let rust_backed_bytes = PyBackedBytes::from(orig_bytes); @@ -407,7 +474,7 @@ mod test { rust_backed_bytes.storage, PyBackedBytesStorage::Rust(_) )); - let to_object = rust_backed_bytes.to_object(py).into_bound(py); + let to_object = (&rust_backed_bytes).into_pyobject(py).unwrap(); assert!(&to_object.is_exact_instance_of::()); assert_eq!(&to_object.extract::().unwrap(), b"abcde"); let into_py = rust_backed_bytes.into_py(py).into_bound(py); diff --git a/src/types/slice.rs b/src/types/slice.rs index b9fad06ebdb..528e7893476 100644 --- a/src/types/slice.rs +++ b/src/types/slice.rs @@ -1,8 +1,10 @@ use crate::err::{PyErr, PyResult}; use crate::ffi; use crate::ffi_ptr_ext::FfiPtrExt; +use crate::prelude::IntoPyObject; use crate::types::any::PyAnyMethods; use crate::{Bound, PyAny, PyObject, Python, ToPyObject}; +use std::convert::Infallible; /// Represents a Python `slice`. /// @@ -139,6 +141,26 @@ impl ToPyObject for PySliceIndices { } } +impl<'py> IntoPyObject<'py> for PySliceIndices { + type Target = PySlice; + type Output = Bound<'py, Self::Target>; + type Error = Infallible; + + fn into_pyobject(self, py: Python<'py>) -> Result { + Ok(PySlice::new(py, self.start, self.stop, self.step)) + } +} + +impl<'py> IntoPyObject<'py> for &PySliceIndices { + type Target = PySlice; + type Output = Bound<'py, Self::Target>; + type Error = Infallible; + + fn into_pyobject(self, py: Python<'py>) -> Result { + Ok(PySlice::new(py, self.start, self.stop, self.step)) + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/tests/ui/invalid_property_args.stderr b/tests/ui/invalid_property_args.stderr index 786533efd53..03f3ba963d8 100644 --- a/tests/ui/invalid_property_args.stderr +++ b/tests/ui/invalid_property_args.stderr @@ -55,14 +55,14 @@ error[E0277]: `PhantomData` cannot be converted to a Python object = help: the trait `IntoPyObject<'_>` is not implemented for `PhantomData`, which is required by `for<'py> PhantomData: PyO3GetField<'py>` = note: implement `IntoPyObject` for `&PhantomData` or `IntoPyObject + Clone` for `PhantomData` to define the conversion = help: the following other types implement trait `IntoPyObject<'py>`: + &&OsStr + &&Path &&str &'a (T0, T1) &'a (T0, T1, T2) &'a (T0, T1, T2, T3) &'a (T0, T1, T2, T3, T4) &'a (T0, T1, T2, T3, T4, T5) - &'a (T0, T1, T2, T3, T4, T5, T6) - &'a (T0, T1, T2, T3, T4, T5, T6, T7) and $N others = note: required for `PhantomData` to implement `for<'py> PyO3GetField<'py>` note: required by a bound in `PyClassGetterGenerator::::generate` diff --git a/tests/ui/missing_intopy.stderr b/tests/ui/missing_intopy.stderr index 653fb785dfd..587ebd479de 100644 --- a/tests/ui/missing_intopy.stderr +++ b/tests/ui/missing_intopy.stderr @@ -8,14 +8,14 @@ error[E0277]: `Blah` cannot be converted to a Python object = note: if you do not wish to have a corresponding Python type, implement it manually = note: if you do not own `Blah` you can perform a manual conversion to one of the types in `pyo3::types::*` = help: the following other types implement trait `IntoPyObject<'py>`: + &&OsStr + &&Path &&str &'a (T0, T1) &'a (T0, T1, T2) &'a (T0, T1, T2, T3) &'a (T0, T1, T2, T3, T4) &'a (T0, T1, T2, T3, T4, T5) - &'a (T0, T1, T2, T3, T4, T5, T6) - &'a (T0, T1, T2, T3, T4, T5, T6, T7) and $N others note: required by a bound in `UnknownReturnType::::wrap` --> src/impl_/wrap.rs From d4ad8a2258e11d472203e3fd6deca1dc22adf2c4 Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Fri, 4 Oct 2024 17:38:15 +0200 Subject: [PATCH 298/495] migrate `PyModule` trait bounds (#4594) --- src/marker.rs | 5 +++-- src/types/module.rs | 37 +++++++++++++++++++++---------------- 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/src/marker.rs b/src/marker.rs index a5ba0436287..0fedf3f8c81 100644 --- a/src/marker.rs +++ b/src/marker.rs @@ -116,6 +116,7 @@ //! [`SendWrapper`]: https://docs.rs/send_wrapper/latest/send_wrapper/struct.SendWrapper.html //! [`Rc`]: std::rc::Rc //! [`Py`]: crate::Py +use crate::conversion::IntoPyObject; #[cfg(any(doc, not(Py_3_10)))] use crate::err::PyErr; use crate::err::{self, PyResult}; @@ -707,7 +708,7 @@ impl<'py> Python<'py> { /// Imports the Python module with the specified name. pub fn import(self, name: N) -> PyResult> where - N: IntoPy>, + N: IntoPyObject<'py, Target = PyString>, { PyModule::import(self, name) } @@ -720,7 +721,7 @@ impl<'py> Python<'py> { where N: IntoPy>, { - self.import(name) + self.import(name.into_py(self)) } /// Gets the Python builtin value `None`. diff --git a/src/types/module.rs b/src/types/module.rs index aec3ea0c179..4a81a4806fa 100644 --- a/src/types/module.rs +++ b/src/types/module.rs @@ -1,4 +1,5 @@ use crate::callback::IntoPyCallbackOutput; +use crate::conversion::IntoPyObject; use crate::err::{PyErr, PyResult}; use crate::ffi_ptr_ext::FfiPtrExt; use crate::py_result_ext::PyResultExt; @@ -6,7 +7,7 @@ use crate::pyclass::PyClass; use crate::types::{ any::PyAnyMethods, list::PyListMethods, PyAny, PyCFunction, PyDict, PyList, PyString, }; -use crate::{exceptions, ffi, Bound, IntoPy, Py, PyObject, Python}; +use crate::{exceptions, ffi, Borrowed, Bound, BoundObject, Py, PyObject, Python}; use std::ffi::{CStr, CString}; use std::str; @@ -82,11 +83,11 @@ impl PyModule { /// /// If you want to import a class, you can store a reference to it with /// [`GILOnceCell::import`][crate::sync::GILOnceCell#method.import]. - pub fn import(py: Python<'_>, name: N) -> PyResult> + pub fn import<'py, N>(py: Python<'py>, name: N) -> PyResult> where - N: IntoPy>, + N: IntoPyObject<'py, Target = PyString>, { - let name: Py = name.into_py(py); + let name = name.into_pyobject(py).map_err(Into::into)?; unsafe { ffi::PyImport_Import(name.as_ptr()) .assume_owned_or_err(py) @@ -99,9 +100,9 @@ impl PyModule { #[inline] pub fn import_bound(py: Python<'_>, name: N) -> PyResult> where - N: IntoPy>, + N: crate::IntoPy>, { - Self::import(py, name) + Self::import(py, name.into_py(py)) } /// Creates and loads a module named `module_name`, @@ -253,8 +254,8 @@ pub trait PyModuleMethods<'py>: crate::sealed::Sealed { /// ``` fn add(&self, name: N, value: V) -> PyResult<()> where - N: IntoPy>, - V: IntoPy; + N: IntoPyObject<'py, Target = PyString>, + V: IntoPyObject<'py>; /// Adds a new class to the module. /// @@ -452,26 +453,30 @@ impl<'py> PyModuleMethods<'py> for Bound<'py, PyModule> { fn add(&self, name: N, value: V) -> PyResult<()> where - N: IntoPy>, - V: IntoPy, + N: IntoPyObject<'py, Target = PyString>, + V: IntoPyObject<'py>, { fn inner( module: &Bound<'_, PyModule>, - name: Bound<'_, PyString>, - value: Bound<'_, PyAny>, + name: Borrowed<'_, '_, PyString>, + value: Borrowed<'_, '_, PyAny>, ) -> PyResult<()> { module .index()? - .append(&name) + .append(name) .expect("could not append __name__ to __all__"); - module.setattr(name, value.into_py(module.py())) + module.setattr(name, value) } let py = self.py(); inner( self, - name.into_py(py).into_bound(py), - value.into_py(py).into_bound(py), + name.into_pyobject(py).map_err(Into::into)?.as_borrowed(), + value + .into_pyobject(py) + .map_err(Into::into)? + .into_any() + .as_borrowed(), ) } From 75bd3f2d655ae02feee7316b59f6454978128038 Mon Sep 17 00:00:00 2001 From: Lily Foote Date: Fri, 4 Oct 2024 18:06:57 +0100 Subject: [PATCH 299/495] Add PyAnyMethods.try_iter and deprecate .iter (#4553) * Add PyAnyMethods.try_iter and deprecate .iter Fixes #4550. * Rename newsfragment to use PR number * Add example to try_iter docs --- newsfragments/4553.fixed.md | 1 + pyo3-benches/benches/bench_any.rs | 2 +- src/conversions/smallvec.rs | 2 +- src/types/any.rs | 33 ++++++++++++++++++++++++++++++- src/types/iterator.rs | 12 +++++------ src/types/mapping.rs | 6 +++--- src/types/sequence.rs | 10 +++++----- tests/test_proto_methods.rs | 4 ++-- 8 files changed, 51 insertions(+), 19 deletions(-) create mode 100644 newsfragments/4553.fixed.md diff --git a/newsfragments/4553.fixed.md b/newsfragments/4553.fixed.md new file mode 100644 index 00000000000..c6714bfce95 --- /dev/null +++ b/newsfragments/4553.fixed.md @@ -0,0 +1 @@ +Deprecate `PyAnyMethods.iter` and replace it with `PyAnyMethods.try_iter` diff --git a/pyo3-benches/benches/bench_any.rs b/pyo3-benches/benches/bench_any.rs index af2d226b3aa..4ed14493873 100644 --- a/pyo3-benches/benches/bench_any.rs +++ b/pyo3-benches/benches/bench_any.rs @@ -77,7 +77,7 @@ fn bench_collect_generic_iterator(b: &mut Bencher<'_>) { b.iter(|| { collection - .iter() + .try_iter() .unwrap() .collect::>>() .unwrap() diff --git a/src/conversions/smallvec.rs b/src/conversions/smallvec.rs index be90113344e..b9e48ace2dd 100644 --- a/src/conversions/smallvec.rs +++ b/src/conversions/smallvec.rs @@ -124,7 +124,7 @@ where }; let mut sv = SmallVec::with_capacity(seq.len().unwrap_or(0)); - for item in seq.iter()? { + for item in seq.try_iter()? { sv.push(item?.extract::()?); } Ok(sv) diff --git a/src/types/any.rs b/src/types/any.rs index 409630e12b2..76c5bcf18dd 100644 --- a/src/types/any.rs +++ b/src/types/any.rs @@ -659,10 +659,37 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed { where K: IntoPyObject<'py>; + /// Takes an object and returns an iterator for it. Returns an error if the object is not + /// iterable. + /// + /// This is typically a new iterator but if the argument is an iterator, + /// this returns itself. + /// + /// # Example: Checking a Python object for iterability + /// + /// ```rust + /// use pyo3::prelude::*; + /// use pyo3::types::{PyAny, PyNone}; + /// + /// fn is_iterable(obj: &Bound<'_, PyAny>) -> bool { + /// match obj.try_iter() { + /// Ok(_) => true, + /// Err(_) => false, + /// } + /// } + /// + /// Python::with_gil(|py| { + /// assert!(is_iterable(&vec![1, 2, 3].into_pyobject(py).unwrap())); + /// assert!(!is_iterable(&PyNone::get(py))); + /// }); + /// ``` + fn try_iter(&self) -> PyResult>; + /// Takes an object and returns an iterator for it. /// /// This is typically a new iterator but if the argument is an iterator, /// this returns itself. + #[deprecated(since = "0.23.0", note = "use `try_iter` instead")] fn iter(&self) -> PyResult>; /// Returns the Python type object for this object's type. @@ -1381,10 +1408,14 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { ) } - fn iter(&self) -> PyResult> { + fn try_iter(&self) -> PyResult> { PyIterator::from_object(self) } + fn iter(&self) -> PyResult> { + self.try_iter() + } + fn get_type(&self) -> Bound<'py, PyType> { unsafe { PyType::from_borrowed_type_ptr(self.py(), ffi::Py_TYPE(self.as_ptr())) } } diff --git a/src/types/iterator.rs b/src/types/iterator.rs index 788ed79c475..b86590e5de8 100644 --- a/src/types/iterator.rs +++ b/src/types/iterator.rs @@ -18,7 +18,7 @@ use crate::{ffi, Bound, PyAny, PyErr, PyResult, PyTypeCheck}; /// Python::with_gil(|py| -> PyResult<()> { /// let list = py.eval(c_str!("iter([1, 2, 3, 4])"), None, None)?; /// let numbers: PyResult> = list -/// .iter()? +/// .try_iter()? /// .map(|i| i.and_then(|i|i.extract::())) /// .collect(); /// let sum: usize = numbers?.iter().sum(); @@ -115,7 +115,7 @@ mod tests { Python::with_gil(|py| { let obj = vec![10, 20].to_object(py); let inst = obj.bind(py); - let mut it = inst.iter().unwrap(); + let mut it = inst.try_iter().unwrap(); assert_eq!( 10_i32, it.next().unwrap().unwrap().extract::<'_, i32>().unwrap() @@ -138,7 +138,7 @@ mod tests { Python::with_gil(|py| { let inst = obj.bind(py); - let mut it = inst.iter().unwrap(); + let mut it = inst.try_iter().unwrap(); assert_eq!( 10_i32, @@ -166,7 +166,7 @@ mod tests { { let inst = list.bind(py); - let mut it = inst.iter().unwrap(); + let mut it = inst.try_iter().unwrap(); assert_eq!( 10_i32, @@ -199,7 +199,7 @@ def fibonacci(target): let generator = py .eval(ffi::c_str!("fibonacci(5)"), None, Some(&context)) .unwrap(); - for (actual, expected) in generator.iter().unwrap().zip(&[1, 1, 2, 3, 5]) { + for (actual, expected) in generator.try_iter().unwrap().zip(&[1, 1, 2, 3, 5]) { let actual = actual.unwrap().extract::().unwrap(); assert_eq!(actual, *expected) } @@ -327,7 +327,7 @@ def fibonacci(target): fn length_hint_becomes_size_hint_lower_bound() { Python::with_gil(|py| { let list = py.eval(ffi::c_str!("[1, 2, 3]"), None, None).unwrap(); - let iter = list.iter().unwrap(); + let iter = list.try_iter().unwrap(); let hint = iter.size_hint(); assert_eq!(hint, (3, None)); }); diff --git a/src/types/mapping.rs b/src/types/mapping.rs index 5ad1aa1b3c1..6249b0eb97b 100644 --- a/src/types/mapping.rs +++ b/src/types/mapping.rs @@ -290,7 +290,7 @@ mod tests { // Can't just compare against a vector of tuples since we don't have a guaranteed ordering. let mut key_sum = 0; let mut value_sum = 0; - for el in mapping.items().unwrap().iter().unwrap() { + for el in mapping.items().unwrap().try_iter().unwrap() { let tuple = el.unwrap().downcast_into::().unwrap(); key_sum += tuple.get_item(0).unwrap().extract::().unwrap(); value_sum += tuple.get_item(1).unwrap().extract::().unwrap(); @@ -311,7 +311,7 @@ mod tests { let mapping = ob.downcast::().unwrap(); // Can't just compare against a vector of tuples since we don't have a guaranteed ordering. let mut key_sum = 0; - for el in mapping.keys().unwrap().iter().unwrap() { + for el in mapping.keys().unwrap().try_iter().unwrap() { key_sum += el.unwrap().extract::().unwrap(); } assert_eq!(7 + 8 + 9, key_sum); @@ -329,7 +329,7 @@ mod tests { let mapping = ob.downcast::().unwrap(); // Can't just compare against a vector of tuples since we don't have a guaranteed ordering. let mut values_sum = 0; - for el in mapping.values().unwrap().iter().unwrap() { + for el in mapping.values().unwrap().try_iter().unwrap() { values_sum += el.unwrap().extract::().unwrap(); } assert_eq!(32 + 42 + 123, values_sum); diff --git a/src/types/sequence.rs b/src/types/sequence.rs index 03e20644ee5..d427edbae5a 100644 --- a/src/types/sequence.rs +++ b/src/types/sequence.rs @@ -376,7 +376,7 @@ where }; let mut v = Vec::with_capacity(seq.len().unwrap_or(0)); - for item in seq.iter()? { + for item in seq.try_iter()? { v.push(item?.extract::()?); } Ok(v) @@ -656,7 +656,7 @@ mod tests { let ob = (&v).into_pyobject(py).unwrap(); let seq = ob.downcast::().unwrap(); let mut idx = 0; - for el in seq.iter().unwrap() { + for el in seq.try_iter().unwrap() { assert_eq!(v[idx], el.unwrap().extract::().unwrap()); idx += 1; } @@ -688,7 +688,7 @@ mod tests { let concat_seq = seq.concat(seq).unwrap(); assert_eq!(6, concat_seq.len().unwrap()); let concat_v: Vec = vec![1, 2, 3, 1, 2, 3]; - for (el, cc) in concat_seq.iter().unwrap().zip(concat_v) { + for (el, cc) in concat_seq.try_iter().unwrap().zip(concat_v) { assert_eq!(cc, el.unwrap().extract::().unwrap()); } }); @@ -703,7 +703,7 @@ mod tests { let concat_seq = seq.concat(seq).unwrap(); assert_eq!(12, concat_seq.len().unwrap()); let concat_v = "stringstring".to_owned(); - for (el, cc) in seq.iter().unwrap().zip(concat_v.chars()) { + for (el, cc) in seq.try_iter().unwrap().zip(concat_v.chars()) { assert_eq!(cc, el.unwrap().extract::().unwrap()); } }); @@ -718,7 +718,7 @@ mod tests { let repeat_seq = seq.repeat(3).unwrap(); assert_eq!(6, repeat_seq.len().unwrap()); let repeated = ["foo", "bar", "foo", "bar", "foo", "bar"]; - for (el, rpt) in repeat_seq.iter().unwrap().zip(repeated.iter()) { + for (el, rpt) in repeat_seq.try_iter().unwrap().zip(repeated.iter()) { assert_eq!(*rpt, el.unwrap().extract::().unwrap()); } }); diff --git a/tests/test_proto_methods.rs b/tests/test_proto_methods.rs index 9fa6a8b888e..11214ec0dc6 100644 --- a/tests/test_proto_methods.rs +++ b/tests/test_proto_methods.rs @@ -853,7 +853,7 @@ impl DefaultedContains { PyList::new(py, ["a", "b", "c"]) .unwrap() .as_ref() - .iter() + .try_iter() .unwrap() .into() } @@ -868,7 +868,7 @@ impl NoContains { PyList::new(py, ["a", "b", "c"]) .unwrap() .as_ref() - .iter() + .try_iter() .unwrap() .into() } From 822b52338adbba574549293c343c38e788970722 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Fri, 4 Oct 2024 13:30:59 -0600 Subject: [PATCH 300/495] Add critical section API wrappers (#4587) * Add critical section API wrappers * add changelog entry * fix no-default-features build * clarify docstring * disable test on WASM * no need to move the section to initialize it * fix wasm clippy * comments from review --------- Co-authored-by: David Hewitt --- newsfragments/4587.added.md | 2 + src/sync.rs | 93 ++++++++++++++++++++++++++++++++++++- 2 files changed, 93 insertions(+), 2 deletions(-) create mode 100644 newsfragments/4587.added.md diff --git a/newsfragments/4587.added.md b/newsfragments/4587.added.md new file mode 100644 index 00000000000..4ccd4cd1c5e --- /dev/null +++ b/newsfragments/4587.added.md @@ -0,0 +1,2 @@ +* Added `with_critical_section`, a safe wrapper around the Python Critical + Section API added in Python 3.13 for the free-threaded build. diff --git a/src/sync.rs b/src/sync.rs index b02b21def93..2320a5ec42a 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -5,7 +5,7 @@ //! //! [PEP 703]: https://peps.python.org/pep-703/ use crate::{ - types::{any::PyAnyMethods, PyString}, + types::{any::PyAnyMethods, PyAny, PyString}, Bound, Py, PyResult, PyTypeCheck, Python, }; use std::cell::UnsafeCell; @@ -330,11 +330,58 @@ impl Interned { } } +/// Executes a closure with a Python critical section held on an object. +/// +/// Acquires the per-object lock for the object `op` that is held +/// until the closure `f` is finished. +/// +/// This is structurally equivalent to the use of the paired +/// Py_BEGIN_CRITICAL_SECTION and Py_END_CRITICAL_SECTION C-API macros. +/// +/// A no-op on GIL-enabled builds, where the critical section API is exposed as +/// a no-op by the Python C API. +/// +/// Provides weaker locking guarantees than traditional locks, but can in some +/// cases be used to provide guarantees similar to the GIL without the risk of +/// deadlocks associated with traditional locks. +/// +/// Many CPython C API functions do not acquire the per-object lock on objects +/// passed to Python. You should not expect critical sections applied to +/// built-in types to prevent concurrent modification. This API is most useful +/// for user-defined types with full control over how the internal state for the +/// type is managed. +#[cfg_attr(not(Py_GIL_DISABLED), allow(unused_variables))] +pub fn with_critical_section(object: &Bound<'_, PyAny>, f: F) -> R +where + F: FnOnce() -> R, +{ + #[cfg(Py_GIL_DISABLED)] + { + struct Guard(crate::ffi::PyCriticalSection); + + impl Drop for Guard { + fn drop(&mut self) { + unsafe { + crate::ffi::PyCriticalSection_End(&mut self.0); + } + } + } + + let mut guard = Guard(unsafe { std::mem::zeroed() }); + unsafe { crate::ffi::PyCriticalSection_Begin(&mut guard.0, object.as_ptr()) }; + f() + } + #[cfg(not(Py_GIL_DISABLED))] + { + f() + } +} + #[cfg(test)] mod tests { use super::*; - use crate::types::{dict::PyDictMethods, PyDict}; + use crate::types::{PyDict, PyDictMethods}; #[test] fn test_intern() { @@ -381,4 +428,46 @@ mod tests { assert!(cell_py.clone_ref(py).get(py).unwrap().is_none(py)); }) } + + #[cfg(feature = "macros")] + #[cfg(not(target_arch = "wasm32"))] // We are building wasm Python with pthreads disabled + #[test] + fn test_critical_section() { + use std::sync::{ + atomic::{AtomicBool, Ordering}, + Barrier, + }; + + let barrier = Barrier::new(2); + + #[crate::pyclass(crate = "crate")] + struct BoolWrapper(AtomicBool); + + let bool_wrapper = Python::with_gil(|py| -> Py { + Py::new(py, BoolWrapper(AtomicBool::new(false))).unwrap() + }); + + std::thread::scope(|s| { + s.spawn(|| { + Python::with_gil(|py| { + let b = bool_wrapper.bind(py); + with_critical_section(b, || { + barrier.wait(); + std::thread::sleep(std::time::Duration::from_millis(10)); + b.borrow().0.store(true, Ordering::Release); + }) + }); + }); + s.spawn(|| { + barrier.wait(); + Python::with_gil(|py| { + let b = bool_wrapper.bind(py); + // this blocks until the other thread's critical section finishes + with_critical_section(b, || { + assert!(b.borrow().0.load(Ordering::Acquire)); + }); + }); + }); + }); + } } From cb884785e20faf82d94c44b054998de297763f3f Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Fri, 4 Oct 2024 22:04:04 +0100 Subject: [PATCH 301/495] seal `PyWeakrefMethods` (#4598) * seal `PyWeakrefMethods` * newsfragment --- newsfragments/4598.changed.md | 1 + src/sealed.rs | 4 ++++ src/types/weakref/anyref.rs | 2 +- 3 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 newsfragments/4598.changed.md diff --git a/newsfragments/4598.changed.md b/newsfragments/4598.changed.md new file mode 100644 index 00000000000..a8dfc040813 --- /dev/null +++ b/newsfragments/4598.changed.md @@ -0,0 +1 @@ +Seal `PyWeakrefMethods` trait. diff --git a/src/sealed.rs b/src/sealed.rs index e2d5c5ccfed..20f31f82e01 100644 --- a/src/sealed.rs +++ b/src/sealed.rs @@ -1,6 +1,7 @@ use crate::types::{ PyBool, PyByteArray, PyBytes, PyCapsule, PyComplex, PyDict, PyFloat, PyFrozenSet, PyList, PyMapping, PyModule, PySequence, PySet, PySlice, PyString, PyTraceback, PyTuple, PyType, + PyWeakref, PyWeakrefProxy, PyWeakrefReference, }; use crate::{ffi, Bound, PyAny, PyResult}; @@ -32,3 +33,6 @@ impl Sealed for Bound<'_, PyString> {} impl Sealed for Bound<'_, PyTraceback> {} impl Sealed for Bound<'_, PyTuple> {} impl Sealed for Bound<'_, PyType> {} +impl Sealed for Bound<'_, PyWeakref> {} +impl Sealed for Bound<'_, PyWeakrefProxy> {} +impl Sealed for Bound<'_, PyWeakrefReference> {} diff --git a/src/types/weakref/anyref.rs b/src/types/weakref/anyref.rs index 06d91ea024e..215b7560c00 100644 --- a/src/types/weakref/anyref.rs +++ b/src/types/weakref/anyref.rs @@ -33,7 +33,7 @@ impl PyTypeCheck for PyWeakref { /// syntax these methods are separated into a trait, because stable Rust does not yet support /// `arbitrary_self_types`. #[doc(alias = "PyWeakref")] -pub trait PyWeakrefMethods<'py> { +pub trait PyWeakrefMethods<'py>: crate::sealed::Sealed { /// Upgrade the weakref to a direct Bound object reference. /// /// It is named `upgrade` to be inline with [rust's `Weak::upgrade`](std::rc::Weak::upgrade). From 720a9be5ea8a1199dd18562592f090938855b123 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Sat, 5 Oct 2024 06:31:44 +0100 Subject: [PATCH 302/495] add `Bound::from_owned_ptr_unchecked` (crate-private for now) (#4596) * add `Bound::from_owned_ptr_unchecked` (crate-private for now) * Update src/instance.rs Co-authored-by: Icxolu <10486322+Icxolu@users.noreply.github.com> --------- Co-authored-by: Icxolu <10486322+Icxolu@users.noreply.github.com> --- src/ffi_ptr_ext.rs | 15 +++++++++++++++ src/instance.rs | 22 ++++++++++++++++++++++ src/pycell.rs | 4 ++-- src/types/dict.rs | 6 +++++- src/types/weakref/anyref.rs | 4 +++- src/types/weakref/proxy.rs | 4 +++- src/types/weakref/reference.rs | 4 +++- 7 files changed, 53 insertions(+), 6 deletions(-) diff --git a/src/ffi_ptr_ext.rs b/src/ffi_ptr_ext.rs index 183b0e3734e..956b4e8406c 100644 --- a/src/ffi_ptr_ext.rs +++ b/src/ffi_ptr_ext.rs @@ -6,10 +6,20 @@ use crate::{ }; pub(crate) trait FfiPtrExt: Sealed { + /// Assumes this pointer carries a Python reference which needs to be decref'd. + /// + /// If the pointer is NULL, this function will fetch an error. unsafe fn assume_owned_or_err(self, py: Python<'_>) -> PyResult>; + + /// Same as `assume_owned_or_err`, but doesn't fetch an error on NULL. unsafe fn assume_owned_or_opt(self, py: Python<'_>) -> Option>; + + /// Same as `assume_owned_or_err`, but panics on NULL. unsafe fn assume_owned(self, py: Python<'_>) -> Bound<'_, PyAny>; + /// Same as `assume_owned_or_err`, but does not check for NULL. + unsafe fn assume_owned_unchecked(self, py: Python<'_>) -> Bound<'_, PyAny>; + /// Assumes this pointer is borrowed from a parent object. /// /// Warning: the lifetime `'a` is not bounded by the function arguments; the caller is @@ -44,6 +54,11 @@ impl FfiPtrExt for *mut ffi::PyObject { Bound::from_owned_ptr(py, self) } + #[inline] + unsafe fn assume_owned_unchecked(self, py: Python<'_>) -> Bound<'_, PyAny> { + Bound::from_owned_ptr_unchecked(py, self) + } + #[inline] unsafe fn assume_borrowed_or_err<'a>( self, diff --git a/src/instance.rs b/src/instance.rs index 1e15624929e..4d5608a9dc0 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -134,6 +134,19 @@ impl<'py> Bound<'py, PyAny> { Py::from_owned_ptr_or_err(py, ptr).map(|obj| Self(py, ManuallyDrop::new(obj))) } + /// Constructs a new `Bound<'py, PyAny>` from a pointer without checking for null. + /// + /// # Safety + /// + /// - `ptr` must be a valid pointer to a Python object + /// - `ptr` must be a strong/owned reference + pub(crate) unsafe fn from_owned_ptr_unchecked( + py: Python<'py>, + ptr: *mut ffi::PyObject, + ) -> Self { + Self(py, ManuallyDrop::new(Py::from_owned_ptr_unchecked(ptr))) + } + /// Constructs a new `Bound<'py, PyAny>` from a pointer by creating a new Python reference. /// Panics if `ptr` is null. /// @@ -1630,6 +1643,15 @@ impl Py { NonNull::new(ptr).map(|nonnull_ptr| Py(nonnull_ptr, PhantomData)) } + /// Constructs a new `Py` instance by taking ownership of the given FFI pointer. + /// + /// # Safety + /// + /// - `ptr` must be a non-null pointer to a Python object or type `T`. + pub(crate) unsafe fn from_owned_ptr_unchecked(ptr: *mut ffi::PyObject) -> Self { + Py(NonNull::new_unchecked(ptr), PhantomData) + } + /// Create a `Py` instance by creating a new reference from the given FFI pointer. /// /// # Safety diff --git a/src/pycell.rs b/src/pycell.rs index 438f7245a22..38f82eb27fd 100644 --- a/src/pycell.rs +++ b/src/pycell.rs @@ -370,7 +370,7 @@ where inner: unsafe { ManuallyDrop::new(self) .as_ptr() - .assume_owned(py) + .assume_owned_unchecked(py) .downcast_into_unchecked() }, } @@ -587,7 +587,7 @@ where inner: unsafe { ManuallyDrop::new(self) .as_ptr() - .assume_owned(py) + .assume_owned_unchecked(py) .downcast_into_unchecked() }, } diff --git a/src/types/dict.rs b/src/types/dict.rs index c4de7bae46a..61767f245cc 100644 --- a/src/types/dict.rs +++ b/src/types/dict.rs @@ -261,7 +261,11 @@ impl<'py> PyDictMethods<'py> for Bound<'py, PyDict> { } { std::os::raw::c_int::MIN..=-1 => Err(PyErr::fetch(py)), 0 => Ok(None), - 1..=std::os::raw::c_int::MAX => Ok(Some(unsafe { result.assume_owned(py) })), + 1..=std::os::raw::c_int::MAX => { + // Safety: PyDict_GetItemRef positive return value means the result is a valid + // owned reference + Ok(Some(unsafe { result.assume_owned_unchecked(py) })) + } } } diff --git a/src/types/weakref/anyref.rs b/src/types/weakref/anyref.rs index 215b7560c00..ffbc86da98c 100644 --- a/src/types/weakref/anyref.rs +++ b/src/types/weakref/anyref.rs @@ -395,7 +395,9 @@ impl<'py> PyWeakrefMethods<'py> for Bound<'py, PyWeakref> { match unsafe { ffi::compat::PyWeakref_GetRef(self.as_ptr(), &mut obj) } { std::os::raw::c_int::MIN..=-1 => panic!("The 'weakref' weak reference instance should be valid (non-null and actually a weakref reference)"), 0 => PyNone::get(self.py()).to_owned().into_any(), - 1..=std::os::raw::c_int::MAX => unsafe { obj.assume_owned(self.py()) }, + // Safety: positive return value from `PyWeakRef_GetRef` guarantees the return value is + // a valid strong reference. + 1..=std::os::raw::c_int::MAX => unsafe { obj.assume_owned_unchecked(self.py()) }, } } } diff --git a/src/types/weakref/proxy.rs b/src/types/weakref/proxy.rs index 798b4b435c7..a16c7583882 100644 --- a/src/types/weakref/proxy.rs +++ b/src/types/weakref/proxy.rs @@ -187,7 +187,9 @@ impl<'py> PyWeakrefMethods<'py> for Bound<'py, PyWeakrefProxy> { match unsafe { ffi::compat::PyWeakref_GetRef(self.as_ptr(), &mut obj) } { std::os::raw::c_int::MIN..=-1 => panic!("The 'weakref.ProxyType' (or `weakref.CallableProxyType`) instance should be valid (non-null and actually a weakref reference)"), 0 => PyNone::get(self.py()).to_owned().into_any(), - 1..=std::os::raw::c_int::MAX => unsafe { obj.assume_owned(self.py()) }, + // Safety: positive return value from `PyWeakRef_GetRef` guarantees the return value is + // a valid strong reference. + 1..=std::os::raw::c_int::MAX => unsafe { obj.assume_owned_unchecked(self.py()) }, } } } diff --git a/src/types/weakref/reference.rs b/src/types/weakref/reference.rs index cc8bc3d55f5..40890e0f887 100644 --- a/src/types/weakref/reference.rs +++ b/src/types/weakref/reference.rs @@ -196,7 +196,9 @@ impl<'py> PyWeakrefMethods<'py> for Bound<'py, PyWeakrefReference> { match unsafe { ffi::compat::PyWeakref_GetRef(self.as_ptr(), &mut obj) } { std::os::raw::c_int::MIN..=-1 => panic!("The 'weakref.ReferenceType' instance should be valid (non-null and actually a weakref reference)"), 0 => PyNone::get(self.py()).to_owned().into_any(), - 1..=std::os::raw::c_int::MAX => unsafe { obj.assume_owned(self.py()) }, + // Safety: positive return value from `PyWeakRef_GetRef` guarantees the return value is + // a valid strong reference. + 1..=std::os::raw::c_int::MAX => unsafe { obj.assume_owned_unchecked(self.py()) }, } } } From d03d1025401e6a14da95228f18a83852e08d8b42 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Sat, 5 Oct 2024 06:44:06 +0100 Subject: [PATCH 303/495] ci: move more jobs to macOS arm (#4600) --- .github/workflows/ci.yml | 80 +++++++++++++++++++++++++++++++--------- 1 file changed, 63 insertions(+), 17 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3e085f9dbab..f53f2c31292 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -93,7 +93,7 @@ jobs: rust: [stable] platform: [ { - os: "macos-14", # first available arm macos runner + os: "macos-latest", python-architecture: "arm64", rust-target: "aarch64-apple-darwin", }, @@ -180,7 +180,7 @@ jobs: platform: [ { - os: "macos-14", # first available arm macos runner + os: "macos-latest", # first available arm macos runner python-architecture: "arm64", rust-target: "aarch64-apple-darwin", }, @@ -248,15 +248,10 @@ jobs: ] platform: [ - # for the full matrix, use x86_64 macos runners because not all Python versions - # PyO3 supports are available for arm on GitHub Actions. (Availability starts - # around Python 3.10, can switch the full matrix to arm once earlier versions - # are dropped.) - # NB: if this switches to arm, switch the arm job below in the `include` to x86_64 { - os: "macos-13", - python-architecture: "x64", - rust-target: "x86_64-apple-darwin", + os: "macos-latest", + python-architecture: "arm64", + rust-target: "aarch64-apple-darwin", }, { os: "ubuntu-latest", @@ -300,7 +295,7 @@ jobs: rust-target: "x86_64-unknown-linux-gnu", } - # Test 32-bit Windows only with the latest Python version + # Test 32-bit Windows and x64 macOS only with the latest Python version - rust: stable python-version: "3.12" platform: @@ -309,14 +304,65 @@ jobs: python-architecture: "x86", rust-target: "i686-pc-windows-msvc", } - - # test arm macos runner with the latest Python version - # NB: if the full matrix switchess to arm, switch to x86_64 here - rust: stable python-version: "3.12" platform: { - os: "macos-14", + os: "macos-13", + python-architecture: "x64", + rust-target: "x86_64-apple-darwin", + } + + # arm64 macOS Python not available on GitHub Actions until 3.10 + # so backfill 3.7-3.9 with x64 macOS runners + - rust: stable + python-version: "3.7" + platform: + { + os: "macos-13", + python-architecture: "x64", + rust-target: "x86_64-apple-darwin", + } + - rust: stable + python-version: "3.8" + platform: + { + os: "macos-13", + python-architecture: "x64", + rust-target: "x86_64-apple-darwin", + } + - rust: stable + python-version: "3.9" + platform: + { + os: "macos-13", + python-architecture: "x64", + rust-target: "x86_64-apple-darwin", + } + + exclude: + # arm64 macOS Python not available on GitHub Actions until 3.10 + - rust: stable + python-version: "3.7" + platform: + { + os: "macos-latest", + python-architecture: "arm64", + rust-target: "aarch64-apple-darwin", + } + - rust: stable + python-version: "3.8" + platform: + { + os: "macos-latest", + python-architecture: "arm64", + rust-target: "aarch64-apple-darwin", + } + - rust: stable + python-version: "3.9" + platform: + { + os: "macos-latest", python-architecture: "arm64", rust-target: "aarch64-apple-darwin", } @@ -389,7 +435,7 @@ jobs: name: coverage ${{ matrix.os }} strategy: matrix: - os: ["windows-latest", "macos-14", "ubuntu-latest"] # first available arm macos runner + os: ["windows-latest", "macos-latest", "ubuntu-latest"] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 @@ -591,7 +637,7 @@ jobs: - os: "macos-13" # last x86_64 macos runners target: "aarch64-apple-darwin" # macos aarch64 -> x86_64 - - os: "macos-14" # aarch64 macos runners + - os: "macos-latest" target: "x86_64-apple-darwin" steps: - uses: actions/checkout@v4 From 71012db0e757c4f5461f202b329cf8dc73d38202 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Sat, 5 Oct 2024 02:12:23 -0500 Subject: [PATCH 304/495] Resolve clippy warnings from nightly (#4601) --- pyo3-ffi/src/cpython/tupleobject.rs | 3 +-- pyo3-macros-backend/src/module.rs | 2 +- pyo3-macros-backend/src/pyclass.rs | 4 ++-- src/buffer.rs | 2 +- src/conversions/std/cell.rs | 2 +- src/conversions/std/num.rs | 2 +- src/conversions/std/osstr.rs | 2 +- src/conversions/std/path.rs | 8 ++++---- src/conversions/std/slice.rs | 2 +- src/conversions/std/string.rs | 6 +++--- src/err/mod.rs | 2 +- src/impl_/pymethods.rs | 2 +- src/instance.rs | 8 ++++---- src/pycell.rs | 18 +++++++++--------- src/pyclass/gc.rs | 2 +- src/types/dict.rs | 2 +- src/types/frozenset.rs | 2 +- src/types/set.rs | 2 +- src/types/tuple.rs | 8 ++++---- tests/test_gc.rs | 2 +- 20 files changed, 40 insertions(+), 41 deletions(-) diff --git a/pyo3-ffi/src/cpython/tupleobject.rs b/pyo3-ffi/src/cpython/tupleobject.rs index 4ed8520daf3..1d988d2bef0 100644 --- a/pyo3-ffi/src/cpython/tupleobject.rs +++ b/pyo3-ffi/src/cpython/tupleobject.rs @@ -12,10 +12,9 @@ pub struct PyTupleObject { // skipped _PyTuple_Resize // skipped _PyTuple_MaybeUntrack -/// Macro, trading safety for speed - // skipped _PyTuple_CAST +/// Macro, trading safety for speed #[inline] #[cfg(not(PyPy))] pub unsafe fn PyTuple_GET_SIZE(op: *mut PyObject) -> Py_ssize_t { diff --git a/pyo3-macros-backend/src/module.rs b/pyo3-macros-backend/src/module.rs index e0025fda6dd..7d2c72dbdfb 100644 --- a/pyo3-macros-backend/src/module.rs +++ b/pyo3-macros-backend/src/module.rs @@ -559,7 +559,7 @@ fn find_and_remove_attribute(attrs: &mut Vec, ident: &str) -> bo found } -impl<'a> PartialEq for IdentOrStr<'a> { +impl PartialEq for IdentOrStr<'_> { fn eq(&self, other: &syn::Ident) -> bool { match self { IdentOrStr::Str(s) => other == s, diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index c9fe1956f66..d7edeb0bf24 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -678,7 +678,7 @@ trait EnumVariant { } } -impl<'a> EnumVariant for PyClassEnumVariant<'a> { +impl EnumVariant for PyClassEnumVariant<'_> { fn get_ident(&self) -> &syn::Ident { match self { PyClassEnumVariant::Struct(struct_variant) => struct_variant.ident, @@ -701,7 +701,7 @@ struct PyClassEnumUnitVariant<'a> { cfg_attrs: Vec<&'a syn::Attribute>, } -impl<'a> EnumVariant for PyClassEnumUnitVariant<'a> { +impl EnumVariant for PyClassEnumUnitVariant<'_> { fn get_ident(&self) -> &syn::Ident { self.ident } diff --git a/src/buffer.rs b/src/buffer.rs index ad070e13e05..85144f6ff99 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -182,7 +182,7 @@ pub unsafe trait Element: Copy { fn is_compatible_format(format: &CStr) -> bool; } -impl<'py, T: Element> FromPyObject<'py> for PyBuffer { +impl FromPyObject<'_> for PyBuffer { fn extract_bound(obj: &Bound<'_, PyAny>) -> PyResult> { Self::get(obj) } diff --git a/src/conversions/std/cell.rs b/src/conversions/std/cell.rs index 75a8a13a787..3c1ab55c4a7 100644 --- a/src/conversions/std/cell.rs +++ b/src/conversions/std/cell.rs @@ -28,7 +28,7 @@ impl<'py, T: Copy + IntoPyObject<'py>> IntoPyObject<'py> for Cell { } } -impl<'a, 'py, T: Copy + IntoPyObject<'py>> IntoPyObject<'py> for &'a Cell { +impl<'py, T: Copy + IntoPyObject<'py>> IntoPyObject<'py> for &Cell { type Target = T::Target; type Output = T::Output; type Error = T::Error; diff --git a/src/conversions/std/num.rs b/src/conversions/std/num.rs index 80bc678fddf..d1faa905abd 100644 --- a/src/conversions/std/num.rs +++ b/src/conversions/std/num.rs @@ -276,7 +276,7 @@ impl<'py> IntoPyObject<'py> for &'_ u8 { } } -impl<'py> FromPyObject<'py> for u8 { +impl FromPyObject<'_> for u8 { fn extract_bound(obj: &Bound<'_, PyAny>) -> PyResult { let val: c_long = extract_int!(obj, -1, ffi::PyLong_AsLong)?; u8::try_from(val).map_err(|e| exceptions::PyOverflowError::new_err(e.to_string())) diff --git a/src/conversions/std/osstr.rs b/src/conversions/std/osstr.rs index 4f956c64ce8..57387b1e600 100644 --- a/src/conversions/std/osstr.rs +++ b/src/conversions/std/osstr.rs @@ -199,7 +199,7 @@ impl<'py> IntoPyObject<'py> for OsString { } } -impl<'a> IntoPy for &'a OsString { +impl IntoPy for &OsString { #[inline] fn into_py(self, py: Python<'_>) -> PyObject { self.into_pyobject(py).unwrap().into_any().unbind() diff --git a/src/conversions/std/path.rs b/src/conversions/std/path.rs index f012cc81a27..615d98e3cd8 100644 --- a/src/conversions/std/path.rs +++ b/src/conversions/std/path.rs @@ -26,7 +26,7 @@ impl FromPyObject<'_> for PathBuf { } } -impl<'a> IntoPy for &'a Path { +impl IntoPy for &Path { #[inline] fn into_py(self, py: Python<'_>) -> PyObject { self.into_pyobject(py).unwrap().into_any().unbind() @@ -55,14 +55,14 @@ impl<'py> IntoPyObject<'py> for &&Path { } } -impl<'a> ToPyObject for Cow<'a, Path> { +impl ToPyObject for Cow<'_, Path> { #[inline] fn to_object(&self, py: Python<'_>) -> PyObject { self.into_pyobject(py).unwrap().into_any().unbind() } } -impl<'a> IntoPy for Cow<'a, Path> { +impl IntoPy for Cow<'_, Path> { #[inline] fn into_py(self, py: Python<'_>) -> PyObject { self.into_pyobject(py).unwrap().into_any().unbind() @@ -116,7 +116,7 @@ impl<'py> IntoPyObject<'py> for PathBuf { } } -impl<'a> IntoPy for &'a PathBuf { +impl IntoPy for &PathBuf { #[inline] fn into_py(self, py: Python<'_>) -> PyObject { self.into_pyobject(py).unwrap().into_any().unbind() diff --git a/src/conversions/std/slice.rs b/src/conversions/std/slice.rs index a90d70a49f7..241c520e847 100644 --- a/src/conversions/std/slice.rs +++ b/src/conversions/std/slice.rs @@ -8,7 +8,7 @@ use crate::{ Bound, IntoPy, Py, PyAny, PyErr, PyObject, PyResult, Python, ToPyObject, }; -impl<'a> IntoPy for &'a [u8] { +impl IntoPy for &[u8] { fn into_py(self, py: Python<'_>) -> PyObject { PyBytes::new(py, self).unbind().into() } diff --git a/src/conversions/std/string.rs b/src/conversions/std/string.rs index 02688641e78..5c634a621bd 100644 --- a/src/conversions/std/string.rs +++ b/src/conversions/std/string.rs @@ -18,7 +18,7 @@ impl ToPyObject for str { } } -impl<'a> IntoPy for &'a str { +impl IntoPy for &str { #[inline] fn into_py(self, py: Python<'_>) -> PyObject { self.into_pyobject(py).unwrap().into_any().unbind() @@ -30,7 +30,7 @@ impl<'a> IntoPy for &'a str { } } -impl<'a> IntoPy> for &'a str { +impl IntoPy> for &str { #[inline] fn into_py(self, py: Python<'_>) -> Py { self.into_pyobject(py).unwrap().unbind() @@ -179,7 +179,7 @@ impl<'py> IntoPyObject<'py> for String { } } -impl<'a> IntoPy for &'a String { +impl IntoPy for &String { #[inline] fn into_py(self, py: Python<'_>) -> PyObject { self.into_pyobject(py).unwrap().into_any().unbind() diff --git a/src/err/mod.rs b/src/err/mod.rs index 2ef9e531768..ac03c2e573e 100644 --- a/src/err/mod.rs +++ b/src/err/mod.rs @@ -891,7 +891,7 @@ impl ToPyObject for PyErr { } } -impl<'a> IntoPy for &'a PyErr { +impl IntoPy for &PyErr { #[inline] fn into_py(self, py: Python<'_>) -> PyObject { self.into_pyobject(py).unwrap().into_any().unbind() diff --git a/src/impl_/pymethods.rs b/src/impl_/pymethods.rs index 300af22db3a..617bad52ea4 100644 --- a/src/impl_/pymethods.rs +++ b/src/impl_/pymethods.rs @@ -300,7 +300,7 @@ where // ... and we cannot traverse a type which might be being mutated by a Rust thread && class_object.borrow_checker().try_borrow().is_ok() { struct TraverseGuard<'a, T: PyClass>(&'a PyClassObject); - impl<'a, T: PyClass> Drop for TraverseGuard<'a, T> { + impl Drop for TraverseGuard<'_, T> { fn drop(&mut self) { self.0.borrow_checker().release_borrow() } diff --git a/src/instance.rs b/src/instance.rs index 4d5608a9dc0..d476c7cef31 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -43,9 +43,9 @@ mod bound_object_sealed { pub unsafe trait Sealed {} // SAFETY: `Bound` is layout-compatible with `*mut ffi::PyObject`. - unsafe impl<'py, T> Sealed for super::Bound<'py, T> {} + unsafe impl Sealed for super::Bound<'_, T> {} // SAFETY: `Borrowed` is layout-compatible with `*mut ffi::PyObject`. - unsafe impl<'a, 'py, T> Sealed for super::Borrowed<'a, 'py, T> {} + unsafe impl Sealed for super::Borrowed<'_, '_, T> {} } /// A GIL-attached equivalent to [`Py`]. @@ -465,14 +465,14 @@ where } } -impl<'py, T> std::fmt::Debug for Bound<'py, T> { +impl std::fmt::Debug for Bound<'_, T> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { let any = self.as_any(); python_format(any, any.repr(), f) } } -impl<'py, T> std::fmt::Display for Bound<'py, T> { +impl std::fmt::Display for Bound<'_, T> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { let any = self.as_any(); python_format(any, any.str(), f) diff --git a/src/pycell.rs b/src/pycell.rs index 38f82eb27fd..a330e4962dc 100644 --- a/src/pycell.rs +++ b/src/pycell.rs @@ -265,7 +265,7 @@ impl<'p, T: PyClass> PyRef<'p, T> { } } -impl<'p, T, U> AsRef for PyRef<'p, T> +impl AsRef for PyRef<'_, T> where T: PyClass, U: PyClass, @@ -429,7 +429,7 @@ where } } -impl<'p, T: PyClass> Deref for PyRef<'p, T> { +impl Deref for PyRef<'_, T> { type Target = T; #[inline] @@ -438,7 +438,7 @@ impl<'p, T: PyClass> Deref for PyRef<'p, T> { } } -impl<'p, T: PyClass> Drop for PyRef<'p, T> { +impl Drop for PyRef<'_, T> { fn drop(&mut self) { self.inner .get_class_object() @@ -479,7 +479,7 @@ impl<'a, 'py, T: PyClass> IntoPyObject<'py> for &'a PyRef<'py, T> { } } -unsafe impl<'a, T: PyClass> AsPyPointer for PyRef<'a, T> { +unsafe impl AsPyPointer for PyRef<'_, T> { fn as_ptr(&self) -> *mut ffi::PyObject { self.inner.as_ptr() } @@ -508,7 +508,7 @@ impl<'p, T: PyClass> PyRefMut<'p, T> { } } -impl<'p, T, U> AsRef for PyRefMut<'p, T> +impl AsRef for PyRefMut<'_, T> where T: PyClass, U: PyClass, @@ -518,7 +518,7 @@ where } } -impl<'p, T, U> AsMut for PyRefMut<'p, T> +impl AsMut for PyRefMut<'_, T> where T: PyClass, U: PyClass, @@ -611,7 +611,7 @@ where } } -impl<'p, T: PyClass> Deref for PyRefMut<'p, T> { +impl> Deref for PyRefMut<'_, T> { type Target = T; #[inline] @@ -620,14 +620,14 @@ impl<'p, T: PyClass> Deref for PyRefMut<'p, T> { } } -impl<'p, T: PyClass> DerefMut for PyRefMut<'p, T> { +impl> DerefMut for PyRefMut<'_, T> { #[inline] fn deref_mut(&mut self) -> &mut T { unsafe { &mut *self.inner.get_class_object().get_ptr() } } } -impl<'p, T: PyClass> Drop for PyRefMut<'p, T> { +impl> Drop for PyRefMut<'_, T> { fn drop(&mut self) { self.inner .get_class_object() diff --git a/src/pyclass/gc.rs b/src/pyclass/gc.rs index b6747a63f89..b95bfa3804f 100644 --- a/src/pyclass/gc.rs +++ b/src/pyclass/gc.rs @@ -25,7 +25,7 @@ pub struct PyVisit<'a> { pub(crate) _guard: PhantomData<&'a ()>, } -impl<'a> PyVisit<'a> { +impl PyVisit<'_> { /// Visit `obj`. pub fn call(&self, obj: &T) -> Result<(), PyTraverseError> where diff --git a/src/types/dict.rs b/src/types/dict.rs index 61767f245cc..ff7595afceb 100644 --- a/src/types/dict.rs +++ b/src/types/dict.rs @@ -466,7 +466,7 @@ impl<'py> Iterator for BoundDictIterator<'py> { } } -impl<'py> ExactSizeIterator for BoundDictIterator<'py> { +impl ExactSizeIterator for BoundDictIterator<'_> { fn len(&self) -> usize { self.len as usize } diff --git a/src/types/frozenset.rs b/src/types/frozenset.rs index 88d726b136d..524baaa2f08 100644 --- a/src/types/frozenset.rs +++ b/src/types/frozenset.rs @@ -242,7 +242,7 @@ impl<'py> Iterator for BoundFrozenSetIterator<'py> { } } -impl<'py> ExactSizeIterator for BoundFrozenSetIterator<'py> { +impl ExactSizeIterator for BoundFrozenSetIterator<'_> { fn len(&self) -> usize { self.remaining } diff --git a/src/types/set.rs b/src/types/set.rs index eddc2eb8885..3e28b2c20e0 100644 --- a/src/types/set.rs +++ b/src/types/set.rs @@ -279,7 +279,7 @@ impl<'py> Iterator for BoundSetIterator<'py> { } } -impl<'py> ExactSizeIterator for BoundSetIterator<'py> { +impl ExactSizeIterator for BoundSetIterator<'_> { fn len(&self) -> usize { self.remaining } diff --git a/src/types/tuple.rs b/src/types/tuple.rs index 8f26e4d89e6..7c8abfcae91 100644 --- a/src/types/tuple.rs +++ b/src/types/tuple.rs @@ -383,7 +383,7 @@ impl<'py> Iterator for BoundTupleIterator<'py> { } } -impl<'py> DoubleEndedIterator for BoundTupleIterator<'py> { +impl DoubleEndedIterator for BoundTupleIterator<'_> { #[inline] fn next_back(&mut self) -> Option { if self.index < self.length { @@ -399,7 +399,7 @@ impl<'py> DoubleEndedIterator for BoundTupleIterator<'py> { } } -impl<'py> ExactSizeIterator for BoundTupleIterator<'py> { +impl ExactSizeIterator for BoundTupleIterator<'_> { fn len(&self) -> usize { self.length.saturating_sub(self.index) } @@ -475,7 +475,7 @@ impl<'a, 'py> Iterator for BorrowedTupleIterator<'a, 'py> { } } -impl<'a, 'py> DoubleEndedIterator for BorrowedTupleIterator<'a, 'py> { +impl DoubleEndedIterator for BorrowedTupleIterator<'_, '_> { #[inline] fn next_back(&mut self) -> Option { if self.index < self.length { @@ -488,7 +488,7 @@ impl<'a, 'py> DoubleEndedIterator for BorrowedTupleIterator<'a, 'py> { } } -impl<'a, 'py> ExactSizeIterator for BorrowedTupleIterator<'a, 'py> { +impl ExactSizeIterator for BorrowedTupleIterator<'_, '_> { fn len(&self) -> usize { self.length.saturating_sub(self.index) } diff --git a/tests/test_gc.rs b/tests/test_gc.rs index 1f55d91d496..ab1302a9d88 100644 --- a/tests/test_gc.rs +++ b/tests/test_gc.rs @@ -416,7 +416,7 @@ trait Traversable { fn __traverse__(&self, visit: PyVisit<'_>) -> Result<(), PyTraverseError>; } -impl<'a> Traversable for PyRef<'a, HijackedTraverse> { +impl Traversable for PyRef<'_, HijackedTraverse> { fn __traverse__(&self, _visit: PyVisit<'_>) -> Result<(), PyTraverseError> { self.hijacked.set(true); Ok(()) From 8288fb922fdd487862a4b7e288fa6a62b6e80185 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Sat, 5 Oct 2024 02:37:05 -0600 Subject: [PATCH 305/495] Make PyClassBorrowChecker thread safe (#4544) * use a mutex in PyClassBorrowChecker * add a test that triggers a reference race * make BorrowFlag wrap an AtomicUsize * fix errors seen on CI * add changelog entry * use a compare-exchange loop in try_borrow * move atomic increment implementation into increment method * fix bug pointed out in code review * make test use an atomic * make test runnable on GIL-enabled build * use less restrictive ordering, add comments * fix ruff error * relax ordering in mutable borrows as well * Update impl_.rs Co-authored-by: David Hewitt * fix path * use AcqRel for mutable borrow compare_exchange loop * add test from david * one more test * disable thread safety tests on WASM * skip python test on WASM as well * fix format * fixup skipif reason --------- Co-authored-by: David Hewitt --- newsfragments/4544.changed.md | 2 + pytests/src/pyclasses.rs | 26 +++++ pytests/tests/test_pyclasses.py | 22 +++++ src/pycell/impl_.rs | 169 +++++++++++++++++++++++++++----- 4 files changed, 192 insertions(+), 27 deletions(-) create mode 100644 newsfragments/4544.changed.md diff --git a/newsfragments/4544.changed.md b/newsfragments/4544.changed.md new file mode 100644 index 00000000000..c94758a770d --- /dev/null +++ b/newsfragments/4544.changed.md @@ -0,0 +1,2 @@ +* Refactored runtime borrow checking for mutable pyclass instances + to be thread-safe when the GIL is disabled. diff --git a/pytests/src/pyclasses.rs b/pytests/src/pyclasses.rs index e499b436395..4e52dbc8712 100644 --- a/pytests/src/pyclasses.rs +++ b/pytests/src/pyclasses.rs @@ -1,3 +1,5 @@ +use std::{thread, time}; + use pyo3::exceptions::{PyStopIteration, PyValueError}; use pyo3::prelude::*; use pyo3::types::PyType; @@ -43,6 +45,29 @@ impl PyClassIter { } } +#[pyclass] +#[derive(Default)] +struct PyClassThreadIter { + count: usize, +} + +#[pymethods] +impl PyClassThreadIter { + #[new] + pub fn new() -> Self { + Default::default() + } + + fn __next__(&mut self, py: Python<'_>) -> usize { + let current_count = self.count; + self.count += 1; + if current_count == 0 { + py.allow_threads(|| thread::sleep(time::Duration::from_millis(100))); + } + self.count + } +} + /// Demonstrates a base class which can operate on the relevant subclass in its constructor. #[pyclass(subclass)] #[derive(Clone, Debug)] @@ -83,6 +108,7 @@ impl ClassWithDict { pub fn pyclasses(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_class::()?; + m.add_class::()?; m.add_class::()?; m.add_class::()?; #[cfg(any(Py_3_10, not(Py_LIMITED_API)))] diff --git a/pytests/tests/test_pyclasses.py b/pytests/tests/test_pyclasses.py index a1424fc75aa..9f611b634b6 100644 --- a/pytests/tests/test_pyclasses.py +++ b/pytests/tests/test_pyclasses.py @@ -1,3 +1,4 @@ +import platform from typing import Type import pytest @@ -53,6 +54,27 @@ def test_iter(): assert excinfo.value.value == "Ended" +@pytest.mark.skipif( + platform.machine() in ["wasm32", "wasm64"], + reason="not supporting threads in CI for WASM yet", +) +def test_parallel_iter(): + import concurrent.futures + + i = pyclasses.PyClassThreadIter() + + def func(): + next(i) + + # the second thread attempts to borrow a reference to the instance's + # state while the first thread is still sleeping, so we trigger a + # runtime borrow-check error + with pytest.raises(RuntimeError, match="Already borrowed"): + with concurrent.futures.ThreadPoolExecutor(max_workers=2) as tpe: + futures = [tpe.submit(func), tpe.submit(func)] + [f.result() for f in futures] + + class AssertingSubClass(pyclasses.AssertingBaseClass): pass diff --git a/src/pycell/impl_.rs b/src/pycell/impl_.rs index 1b5ce774379..1bd225de830 100644 --- a/src/pycell/impl_.rs +++ b/src/pycell/impl_.rs @@ -1,9 +1,10 @@ #![allow(missing_docs)] //! Crate-private implementation of PyClassObject -use std::cell::{Cell, UnsafeCell}; +use std::cell::UnsafeCell; use std::marker::PhantomData; use std::mem::ManuallyDrop; +use std::sync::atomic::{AtomicUsize, Ordering}; use crate::impl_::pyclass::{ PyClassBaseType, PyClassDict, PyClassImpl, PyClassThreadChecker, PyClassWeakRef, @@ -50,22 +51,49 @@ impl PyClassMutability for ExtendsMutableAncestor { type MutableChild = ExtendsMutableAncestor; } -#[derive(Debug, Copy, Clone, Eq, PartialEq)] -struct BorrowFlag(usize); +#[derive(Debug)] +struct BorrowFlag(AtomicUsize); impl BorrowFlag { - pub(crate) const UNUSED: BorrowFlag = BorrowFlag(0); - const HAS_MUTABLE_BORROW: BorrowFlag = BorrowFlag(usize::MAX); - const fn increment(self) -> Self { - Self(self.0 + 1) + pub(crate) const UNUSED: usize = 0; + const HAS_MUTABLE_BORROW: usize = usize::MAX; + fn increment(&self) -> Result<(), PyBorrowError> { + let mut value = self.0.load(Ordering::Relaxed); + loop { + if value == BorrowFlag::HAS_MUTABLE_BORROW { + return Err(PyBorrowError { _private: () }); + } + match self.0.compare_exchange( + // only increment if the value hasn't changed since the + // last atomic load + value, + value + 1, + Ordering::Relaxed, + Ordering::Relaxed, + ) { + Ok(..) => { + // value has been successfully incremented, we need an acquire fence + // so that data this borrow flag protects can be read safely in this thread + std::sync::atomic::fence(Ordering::Acquire); + break Ok(()); + } + Err(changed_value) => { + // value changed under us, need to try again + value = changed_value; + } + } + } } - const fn decrement(self) -> Self { - Self(self.0 - 1) + fn decrement(&self) { + // impossible to get into a bad state from here so relaxed + // ordering is fine, the decrement only needs to eventually + // be visible + self.0.fetch_sub(1, Ordering::Relaxed); } } pub struct EmptySlot(()); -pub struct BorrowChecker(Cell); +pub struct BorrowChecker(BorrowFlag); pub trait PyClassBorrowChecker { /// Initial value for self @@ -110,36 +138,38 @@ impl PyClassBorrowChecker for EmptySlot { impl PyClassBorrowChecker for BorrowChecker { #[inline] fn new() -> Self { - Self(Cell::new(BorrowFlag::UNUSED)) + Self(BorrowFlag(AtomicUsize::new(BorrowFlag::UNUSED))) } fn try_borrow(&self) -> Result<(), PyBorrowError> { - let flag = self.0.get(); - if flag != BorrowFlag::HAS_MUTABLE_BORROW { - self.0.set(flag.increment()); - Ok(()) - } else { - Err(PyBorrowError { _private: () }) - } + self.0.increment() } fn release_borrow(&self) { - let flag = self.0.get(); - self.0.set(flag.decrement()) + self.0.decrement(); } fn try_borrow_mut(&self) -> Result<(), PyBorrowMutError> { - let flag = self.0.get(); - if flag == BorrowFlag::UNUSED { - self.0.set(BorrowFlag::HAS_MUTABLE_BORROW); - Ok(()) - } else { - Err(PyBorrowMutError { _private: () }) + let flag = &self.0; + match flag.0.compare_exchange( + // only allowed to transition to mutable borrow if the reference is + // currently unused + BorrowFlag::UNUSED, + BorrowFlag::HAS_MUTABLE_BORROW, + // On success, reading the flag and updating its state are an atomic + // operation + Ordering::AcqRel, + // It doesn't matter precisely when the failure gets turned + // into an error + Ordering::Relaxed, + ) { + Ok(..) => Ok(()), + Err(..) => Err(PyBorrowMutError { _private: () }), } } fn release_borrow_mut(&self) { - self.0.set(BorrowFlag::UNUSED) + self.0 .0.store(BorrowFlag::UNUSED, Ordering::Release) } } @@ -497,4 +527,89 @@ mod tests { assert!(mmm_bound.extract::>().is_ok()); }) } + + #[test] + #[cfg(not(target_arch = "wasm32"))] + fn test_thread_safety() { + #[crate::pyclass(crate = "crate")] + struct MyClass { + x: u64, + } + + Python::with_gil(|py| { + let inst = Py::new(py, MyClass { x: 0 }).unwrap(); + + let total_modifications = py.allow_threads(|| { + std::thread::scope(|s| { + // Spawn a bunch of threads all racing to write to + // the same instance of `MyClass`. + let threads = (0..10) + .map(|_| { + s.spawn(|| { + Python::with_gil(|py| { + // Each thread records its own view of how many writes it made + let mut local_modifications = 0; + for _ in 0..100 { + if let Ok(mut i) = inst.try_borrow_mut(py) { + i.x += 1; + local_modifications += 1; + } + } + local_modifications + }) + }) + }) + .collect::>(); + + // Sum up the total number of writes made by all threads + threads.into_iter().map(|t| t.join().unwrap()).sum::() + }) + }); + + // If the implementation is free of data races, the total number of writes + // should match the final value of `x`. + assert_eq!(total_modifications, inst.borrow(py).x); + }); + } + + #[test] + #[cfg(not(target_arch = "wasm32"))] + fn test_thread_safety_2() { + struct SyncUnsafeCell(UnsafeCell); + unsafe impl Sync for SyncUnsafeCell {} + + impl SyncUnsafeCell { + fn get(&self) -> *mut T { + self.0.get() + } + } + + let data = SyncUnsafeCell(UnsafeCell::new(0)); + let data2 = SyncUnsafeCell(UnsafeCell::new(0)); + let borrow_checker = BorrowChecker(BorrowFlag(AtomicUsize::new(BorrowFlag::UNUSED))); + + std::thread::scope(|s| { + s.spawn(|| { + for _ in 0..1_000_000 { + if borrow_checker.try_borrow_mut().is_ok() { + // thread 1 writes to both values during the mutable borrow + unsafe { *data.get() += 1 }; + unsafe { *data2.get() += 1 }; + borrow_checker.release_borrow_mut(); + } + } + }); + + s.spawn(|| { + for _ in 0..1_000_000 { + if borrow_checker.try_borrow().is_ok() { + // if the borrow checker is working correctly, it should be impossible + // for thread 2 to observe a difference in the two values + assert_eq!(unsafe { *data.get() }, unsafe { *data2.get() }); + borrow_checker.release_borrow(); + } + } + }); + }); + } } From 4f779c819de4927fddfc6c2f9be1f274749b618d Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Mon, 7 Oct 2024 22:25:58 +0200 Subject: [PATCH 306/495] deprecate `ToPyObject` in favor or `IntoPyObject` (#4595) * deprecate `ToPyObject` in favor of `IntoPyObject` * review comments --- guide/src/class.md | 4 +- guide/src/conversions/traits.md | 24 ++--- guide/src/migration.md | 5 +- newsfragments/4595.changed.md | 2 + pytests/src/objstore.rs | 4 +- src/conversion.rs | 5 + src/conversions/chrono.rs | 106 ++++++++++++--------- src/conversions/chrono_tz.rs | 30 +++--- src/conversions/either.rs | 51 +++++----- src/conversions/hashbrown.rs | 9 +- src/conversions/indexmap.rs | 10 +- src/conversions/num_bigint.rs | 29 +++--- src/conversions/num_complex.rs | 16 ++-- src/conversions/num_rational.rs | 7 +- src/conversions/rust_decimal.rs | 7 +- src/conversions/smallvec.rs | 8 +- src/conversions/std/array.rs | 16 ++-- src/conversions/std/cell.rs | 5 +- src/conversions/std/ipaddr.rs | 13 ++- src/conversions/std/map.rs | 12 ++- src/conversions/std/num.rs | 164 +++++++++++++++++--------------- src/conversions/std/option.rs | 7 +- src/conversions/std/osstr.rs | 13 ++- src/conversions/std/path.rs | 13 ++- src/conversions/std/set.rs | 18 ++-- src/conversions/std/slice.rs | 19 ++-- src/conversions/std/string.rs | 37 +++---- src/conversions/std/time.rs | 44 ++++++--- src/conversions/std/vec.rs | 6 +- src/err/mod.rs | 33 ++++--- src/impl_/pyclass.rs | 6 +- src/instance.rs | 36 +++---- src/lib.rs | 4 +- src/macros.rs | 5 +- src/prelude.rs | 4 +- src/pybacked.rs | 6 +- src/tests/common.rs | 5 +- src/types/any.rs | 61 ++++++------ src/types/boolobject.rs | 14 ++- src/types/dict.rs | 62 ++++-------- src/types/float.rs | 15 ++- src/types/frozenset.rs | 5 +- src/types/iterator.rs | 29 +++--- src/types/list.rs | 9 +- src/types/none.rs | 8 +- src/types/sequence.rs | 6 +- src/types/set.rs | 6 +- src/types/slice.rs | 6 +- src/types/string.rs | 8 +- src/types/tuple.rs | 19 ++-- src/types/weakref/proxy.rs | 18 +++- src/types/weakref/reference.rs | 18 +++- tests/test_class_conversion.rs | 11 +-- tests/test_gc.rs | 11 +-- tests/test_getter_setter.rs | 9 +- tests/test_inheritance.rs | 2 +- tests/test_methods.rs | 73 ++++++++++---- tests/test_module.rs | 19 ++-- tests/test_sequence.rs | 5 +- tests/test_various.rs | 6 +- 60 files changed, 687 insertions(+), 516 deletions(-) create mode 100644 newsfragments/4595.changed.md diff --git a/guide/src/class.md b/guide/src/class.md index 45718f5b667..c20cacd3cc7 100644 --- a/guide/src/class.md +++ b/guide/src/class.md @@ -412,10 +412,10 @@ impl SubSubClass { 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)) + Ok(Py::new(py, sub)?.into_any()) } else { let sub_sub = sub.add_subclass(SubSubClass { val3: val }); - Ok(Py::new(py, sub_sub)?.to_object(py)) + Ok(Py::new(py, sub_sub)?.into_any()) } } } diff --git a/guide/src/conversions/traits.md b/guide/src/conversions/traits.md index fd9e90097f2..9f163df9ed2 100644 --- a/guide/src/conversions/traits.md +++ b/guide/src/conversions/traits.md @@ -294,8 +294,8 @@ enum RustyEnum<'py> { # fn main() -> PyResult<()> { # Python::with_gil(|py| -> PyResult<()> { # { -# let thing = 42_u8.to_object(py); -# let rust_thing: RustyEnum<'_> = thing.extract(py)?; +# let thing = 42_u8.into_pyobject(py)?; +# let rust_thing: RustyEnum<'_> = thing.extract()?; # # assert_eq!( # 42, @@ -318,8 +318,8 @@ enum RustyEnum<'py> { # ); # } # { -# let thing = (32_u8, 73_u8).to_object(py); -# let rust_thing: RustyEnum<'_> = thing.extract(py)?; +# let thing = (32_u8, 73_u8).into_pyobject(py)?; +# let rust_thing: RustyEnum<'_> = thing.extract()?; # # assert_eq!( # (32, 73), @@ -330,8 +330,8 @@ enum RustyEnum<'py> { # ); # } # { -# let thing = ("foo", 73_u8).to_object(py); -# let rust_thing: RustyEnum<'_> = thing.extract(py)?; +# let thing = ("foo", 73_u8).into_pyobject(py)?; +# let rust_thing: RustyEnum<'_> = thing.extract()?; # # assert_eq!( # (String::from("foo"), 73), @@ -427,8 +427,8 @@ enum RustyEnum { # fn main() -> PyResult<()> { # Python::with_gil(|py| -> PyResult<()> { # { -# let thing = 42_u8.to_object(py); -# let rust_thing: RustyEnum = thing.extract(py)?; +# let thing = 42_u8.into_pyobject(py)?; +# let rust_thing: RustyEnum = thing.extract()?; # # assert_eq!( # 42, @@ -440,8 +440,8 @@ enum RustyEnum { # } # # { -# let thing = "foo".to_object(py); -# let rust_thing: RustyEnum = thing.extract(py)?; +# let thing = "foo".into_pyobject(py)?; +# let rust_thing: RustyEnum = thing.extract()?; # # assert_eq!( # "foo", @@ -453,8 +453,8 @@ enum RustyEnum { # } # # { -# let thing = b"foo".to_object(py); -# let error = thing.extract::(py).unwrap_err(); +# let thing = b"foo".into_pyobject(py)?; +# let error = thing.extract::().unwrap_err(); # assert!(error.is_instance_of::(py)); # } # diff --git a/guide/src/migration.md b/guide/src/migration.md index 75b102d1c40..ba20f39c3db 100644 --- a/guide/src/migration.md +++ b/guide/src/migration.md @@ -155,9 +155,12 @@ Notable features of this new trait: All PyO3 provided types as well as `#[pyclass]`es already implement `IntoPyObject`. Other types will need to adapt an implementation of `IntoPyObject` to stay compatible with the Python APIs. +Together with the introduction of `IntoPyObject` the old conversion traits `ToPyObject` and `IntoPy` +are deprecated and will be removed in a future PyO3 version. + Before: -```rust +```rust,ignore # use pyo3::prelude::*; # #[allow(dead_code)] struct MyPyObjectWrapper(PyObject); diff --git a/newsfragments/4595.changed.md b/newsfragments/4595.changed.md new file mode 100644 index 00000000000..e8b7eba5cbf --- /dev/null +++ b/newsfragments/4595.changed.md @@ -0,0 +1,2 @@ +- `PyErr::matches` is now fallible due to `IntoPyObject` migration. +- deprecate `ToPyObject` in favor of `IntoPyObject` \ No newline at end of file diff --git a/pytests/src/objstore.rs b/pytests/src/objstore.rs index 9a005c0ec97..844cee946ad 100644 --- a/pytests/src/objstore.rs +++ b/pytests/src/objstore.rs @@ -13,8 +13,8 @@ impl ObjStore { ObjStore::default() } - fn push(&mut self, py: Python<'_>, obj: &Bound<'_, PyAny>) { - self.obj.push(obj.to_object(py)); + fn push(&mut self, obj: &Bound<'_, PyAny>) { + self.obj.push(obj.clone().unbind()); } } diff --git a/src/conversion.rs b/src/conversion.rs index 3cc73072ed9..4e0de44b4a1 100644 --- a/src/conversion.rs +++ b/src/conversion.rs @@ -64,6 +64,10 @@ pub unsafe trait AsPyPointer { } /// Conversion trait that allows various objects to be converted into `PyObject`. +#[deprecated( + since = "0.23.0", + note = "`ToPyObject` is going to be replaced by `IntoPyObject`. See the migration guide (https://pyo3.rs/v0.23/migration) for more information." +)] pub trait ToPyObject { /// Converts self into a Python object. fn to_object(&self, py: Python<'_>) -> PyObject; @@ -548,6 +552,7 @@ where /// Identity conversion: allows using existing `PyObject` instances where /// `T: ToPyObject` is expected. +#[allow(deprecated)] impl ToPyObject for &'_ T { #[inline] fn to_object(&self, py: Python<'_>) -> PyObject { diff --git a/src/conversions/chrono.rs b/src/conversions/chrono.rs index 7f36961b9ea..ce94b98c1a3 100644 --- a/src/conversions/chrono.rs +++ b/src/conversions/chrono.rs @@ -20,23 +20,24 @@ //! //! ```rust //! use chrono::{DateTime, Duration, TimeZone, Utc}; -//! use pyo3::{Python, ToPyObject}; +//! use pyo3::{Python, PyResult, IntoPyObject, types::PyAnyMethods}; //! -//! fn main() { +//! fn main() -> PyResult<()> { //! pyo3::prepare_freethreaded_python(); //! Python::with_gil(|py| { //! // Build some chrono values //! let chrono_datetime = Utc.with_ymd_and_hms(2022, 1, 1, 12, 0, 0).unwrap(); //! let chrono_duration = Duration::seconds(1); //! // Convert them to Python -//! let py_datetime = chrono_datetime.to_object(py); -//! let py_timedelta = chrono_duration.to_object(py); +//! let py_datetime = chrono_datetime.into_pyobject(py)?; +//! let py_timedelta = chrono_duration.into_pyobject(py)?; //! // Do an operation in Python -//! let py_sum = py_datetime.call_method1(py, "__add__", (py_timedelta,)).unwrap(); +//! let py_sum = py_datetime.call_method1("__add__", (py_timedelta,))?; //! // Convert back to Rust -//! let chrono_sum: DateTime = py_sum.extract(py).unwrap(); +//! let chrono_sum: DateTime = py_sum.extract()?; //! println!("DateTime: {}", chrono_datetime); -//! }); +//! Ok(()) +//! }) //! } //! ``` @@ -53,9 +54,9 @@ use crate::types::{ timezone_utc, PyDate, PyDateAccess, PyDateTime, PyDelta, PyDeltaAccess, PyTime, PyTimeAccess, PyTzInfo, PyTzInfoAccess, }; -use crate::{ - ffi, Bound, FromPyObject, IntoPy, PyAny, PyErr, PyObject, PyResult, Python, ToPyObject, -}; +#[allow(deprecated)] +use crate::ToPyObject; +use crate::{ffi, Bound, FromPyObject, IntoPy, PyAny, PyErr, PyObject, PyResult, Python}; #[cfg(Py_LIMITED_API)] use crate::{intern, DowncastError}; use chrono::offset::{FixedOffset, Utc}; @@ -63,6 +64,7 @@ use chrono::{ DateTime, Datelike, Duration, NaiveDate, NaiveDateTime, NaiveTime, Offset, TimeZone, Timelike, }; +#[allow(deprecated)] impl ToPyObject for Duration { #[inline] fn to_object(&self, py: Python<'_>) -> PyObject { @@ -168,6 +170,7 @@ impl FromPyObject<'_> for Duration { } } +#[allow(deprecated)] impl ToPyObject for NaiveDate { #[inline] fn to_object(&self, py: Python<'_>) -> PyObject { @@ -233,6 +236,7 @@ impl FromPyObject<'_> for NaiveDate { } } +#[allow(deprecated)] impl ToPyObject for NaiveTime { #[inline] fn to_object(&self, py: Python<'_>) -> PyObject { @@ -308,6 +312,7 @@ impl FromPyObject<'_> for NaiveTime { } } +#[allow(deprecated)] impl ToPyObject for NaiveDateTime { #[inline] fn to_object(&self, py: Python<'_>) -> PyObject { @@ -395,6 +400,7 @@ impl FromPyObject<'_> for NaiveDateTime { } } +#[allow(deprecated)] impl ToPyObject for DateTime { fn to_object(&self, py: Python<'_>) -> PyObject { // FIXME: convert to better timezone representation here than just convert to fixed offset @@ -407,7 +413,7 @@ impl ToPyObject for DateTime { impl IntoPy for DateTime { fn into_py(self, py: Python<'_>) -> PyObject { - self.to_object(py) + self.into_pyobject(py).unwrap().into_any().unbind() } } @@ -491,6 +497,7 @@ impl FromPyObject<'py>> FromPyObject<'_> for DateTime) -> PyObject { @@ -574,6 +581,7 @@ impl FromPyObject<'_> for FixedOffset { } } +#[allow(deprecated)] impl ToPyObject for Utc { #[inline] fn to_object(&self, py: Python<'_>) -> PyObject { @@ -925,15 +933,15 @@ mod tests { } #[test] - fn test_pyo3_timedelta_topyobject() { + fn test_pyo3_timedelta_into_pyobject() { // Utility function used to check different durations. // The `name` parameter is used to identify the check in case of a failure. let check = |name: &'static str, delta: Duration, py_days, py_seconds, py_ms| { Python::with_gil(|py| { - let delta = delta.to_object(py); + let delta = delta.into_pyobject(py).unwrap(); let py_delta = new_py_datetime_ob(py, "timedelta", (py_days, py_seconds, py_ms)); assert!( - delta.bind(py).eq(&py_delta).unwrap(), + delta.eq(&py_delta).unwrap(), "{}: {} != {}", name, delta, @@ -954,10 +962,10 @@ mod tests { let delta = Duration::seconds(86399999999999) + Duration::nanoseconds(999999000); // max check("delta max value", delta, 999999999, 86399, 999999); - // Also check that trying to convert an out of bound value panics. + // Also check that trying to convert an out of bound value errors. Python::with_gil(|py| { - assert!(panic::catch_unwind(|| Duration::min_value().to_object(py)).is_err()); - assert!(panic::catch_unwind(|| Duration::max_value().to_object(py)).is_err()); + assert!(Duration::min_value().into_pyobject(py).is_err()); + assert!(Duration::max_value().into_pyobject(py).is_err()); }); } @@ -1021,15 +1029,16 @@ mod tests { } #[test] - fn test_pyo3_date_topyobject() { + fn test_pyo3_date_into_pyobject() { let eq_ymd = |name: &'static str, year, month, day| { Python::with_gil(|py| { let date = NaiveDate::from_ymd_opt(year, month, day) .unwrap() - .to_object(py); + .into_pyobject(py) + .unwrap(); let py_date = new_py_datetime_ob(py, "date", (year, month, day)); assert_eq!( - date.bind(py).compare(&py_date).unwrap(), + date.compare(&py_date).unwrap(), Ordering::Equal, "{}: {} != {}", name, @@ -1063,7 +1072,7 @@ mod tests { } #[test] - fn test_pyo3_datetime_topyobject_utc() { + fn test_pyo3_datetime_into_pyobject_utc() { Python::with_gil(|py| { let check_utc = |name: &'static str, year, month, day, hour, minute, second, ms, py_ms| { @@ -1072,7 +1081,7 @@ mod tests { .and_hms_micro_opt(hour, minute, second, ms) .unwrap() .and_utc(); - let datetime = datetime.to_object(py); + let datetime = datetime.into_pyobject(py).unwrap(); let py_datetime = new_py_datetime_ob( py, "datetime", @@ -1088,7 +1097,7 @@ mod tests { ), ); assert_eq!( - datetime.bind(py).compare(&py_datetime).unwrap(), + datetime.compare(&py_datetime).unwrap(), Ordering::Equal, "{}: {} != {}", name, @@ -1112,7 +1121,7 @@ mod tests { } #[test] - fn test_pyo3_datetime_topyobject_fixed_offset() { + fn test_pyo3_datetime_into_pyobject_fixed_offset() { Python::with_gil(|py| { let check_fixed_offset = |name: &'static str, year, month, day, hour, minute, second, ms, py_ms| { @@ -1123,15 +1132,15 @@ mod tests { .unwrap() .and_local_timezone(offset) .unwrap(); - let datetime = datetime.to_object(py); - let py_tz = offset.to_object(py); + let datetime = datetime.into_pyobject(py).unwrap(); + let py_tz = offset.into_pyobject(py).unwrap(); let py_datetime = new_py_datetime_ob( py, "datetime", (year, month, day, hour, minute, second, py_ms, py_tz), ); assert_eq!( - datetime.bind(py).compare(&py_datetime).unwrap(), + datetime.compare(&py_datetime).unwrap(), Ordering::Equal, "{}: {} != {}", name, @@ -1191,7 +1200,7 @@ mod tests { let second = 9; let micro = 999_999; let offset = FixedOffset::east_opt(3600).unwrap(); - let py_tz = offset.to_object(py); + let py_tz = offset.into_pyobject(py).unwrap(); let py_datetime = new_py_datetime_ob( py, "datetime", @@ -1224,21 +1233,27 @@ mod tests { } #[test] - fn test_pyo3_offset_fixed_topyobject() { + fn test_pyo3_offset_fixed_into_pyobject() { Python::with_gil(|py| { // Chrono offset - let offset = FixedOffset::east_opt(3600).unwrap().to_object(py); + let offset = FixedOffset::east_opt(3600) + .unwrap() + .into_pyobject(py) + .unwrap(); // Python timezone from timedelta let td = new_py_datetime_ob(py, "timedelta", (0, 3600, 0)); let py_timedelta = new_py_datetime_ob(py, "timezone", (td,)); // Should be equal - assert!(offset.bind(py).eq(py_timedelta).unwrap()); + assert!(offset.eq(py_timedelta).unwrap()); // Same but with negative values - let offset = FixedOffset::east_opt(-3600).unwrap().to_object(py); + let offset = FixedOffset::east_opt(-3600) + .unwrap() + .into_pyobject(py) + .unwrap(); let td = new_py_datetime_ob(py, "timedelta", (0, -3600, 0)); let py_timedelta = new_py_datetime_ob(py, "timezone", (td,)); - assert!(offset.bind(py).eq(py_timedelta).unwrap()); + assert!(offset.eq(py_timedelta).unwrap()); }) } @@ -1253,11 +1268,11 @@ mod tests { } #[test] - fn test_pyo3_offset_utc_topyobject() { + fn test_pyo3_offset_utc_into_pyobject() { Python::with_gil(|py| { - let utc = Utc.to_object(py); + let utc = Utc.into_pyobject(py).unwrap(); let py_utc = python_utc(py); - assert!(utc.bind(py).is(&py_utc)); + assert!(utc.is(&py_utc)); }) } @@ -1280,15 +1295,16 @@ mod tests { } #[test] - fn test_pyo3_time_topyobject() { + fn test_pyo3_time_into_pyobject() { Python::with_gil(|py| { let check_time = |name: &'static str, hour, minute, second, ms, py_ms| { let time = NaiveTime::from_hms_micro_opt(hour, minute, second, ms) .unwrap() - .to_object(py); + .into_pyobject(py) + .unwrap(); let py_time = new_py_datetime_ob(py, "time", (hour, minute, second, py_ms)); assert!( - time.bind(py).eq(&py_time).unwrap(), + time.eq(&py_time).unwrap(), "{}: {} != {}", name, time, @@ -1420,8 +1436,8 @@ mod tests { // We use to `from_ymd_opt` constructor so that we only test valid `NaiveDate`s. // This is to skip the test if we are creating an invalid date, like February 31. if let Some(date) = NaiveDate::from_ymd_opt(year, month, day) { - let py_date = date.to_object(py); - let roundtripped: NaiveDate = py_date.extract(py).expect("Round trip"); + let py_date = date.into_pyobject(py).unwrap(); + let roundtripped: NaiveDate = py_date.extract().expect("Round trip"); assert_eq!(date, roundtripped); } }) @@ -1441,8 +1457,8 @@ mod tests { Python::with_gil(|py| { if let Some(time) = NaiveTime::from_hms_micro_opt(hour, min, sec, micro) { // Wrap in CatchWarnings to avoid to_object firing warning for truncated leap second - let py_time = CatchWarnings::enter(py, |_| Ok(time.to_object(py))).unwrap(); - let roundtripped: NaiveTime = py_time.extract(py).expect("Round trip"); + let py_time = CatchWarnings::enter(py, |_| time.into_pyobject(py)).unwrap(); + let roundtripped: NaiveTime = py_time.extract().expect("Round trip"); // Leap seconds are not roundtripped let expected_roundtrip_time = micro.checked_sub(1_000_000).map(|micro| NaiveTime::from_hms_micro_opt(hour, min, sec, micro).unwrap()).unwrap_or(time); assert_eq!(expected_roundtrip_time, roundtripped); @@ -1465,8 +1481,8 @@ mod tests { let time_opt = NaiveTime::from_hms_micro_opt(hour, min, sec, micro); if let (Some(date), Some(time)) = (date_opt, time_opt) { let dt = NaiveDateTime::new(date, time); - let pydt = dt.to_object(py); - let roundtripped: NaiveDateTime = pydt.extract(py).expect("Round trip"); + let pydt = dt.into_pyobject(py).unwrap(); + let roundtripped: NaiveDateTime = pydt.extract().expect("Round trip"); assert_eq!(dt, roundtripped); } }) diff --git a/src/conversions/chrono_tz.rs b/src/conversions/chrono_tz.rs index 91428638f1e..ff014eb99d9 100644 --- a/src/conversions/chrono_tz.rs +++ b/src/conversions/chrono_tz.rs @@ -21,16 +21,17 @@ //! //! ```rust,no_run //! use chrono_tz::Tz; -//! use pyo3::{Python, ToPyObject}; +//! use pyo3::{Python, PyResult, IntoPyObject, types::PyAnyMethods}; //! -//! fn main() { +//! fn main() -> PyResult<()> { //! pyo3::prepare_freethreaded_python(); //! Python::with_gil(|py| { //! // Convert to Python -//! let py_tzinfo = Tz::Europe__Paris.to_object(py); +//! let py_tzinfo = Tz::Europe__Paris.into_pyobject(py)?; //! // Convert back to Rust -//! assert_eq!(py_tzinfo.extract::(py).unwrap(), Tz::Europe__Paris); -//! }); +//! assert_eq!(py_tzinfo.extract::()?, Tz::Europe__Paris); +//! Ok(()) +//! }) //! } //! ``` use crate::conversion::IntoPyObject; @@ -38,12 +39,13 @@ use crate::exceptions::PyValueError; use crate::pybacked::PyBackedStr; use crate::sync::GILOnceCell; use crate::types::{any::PyAnyMethods, PyType}; -use crate::{ - intern, Bound, FromPyObject, IntoPy, Py, PyAny, PyErr, PyObject, PyResult, Python, ToPyObject, -}; +#[allow(deprecated)] +use crate::ToPyObject; +use crate::{intern, Bound, FromPyObject, IntoPy, Py, PyAny, PyErr, PyObject, PyResult, Python}; use chrono_tz::Tz; use std::str::FromStr; +#[allow(deprecated)] impl ToPyObject for Tz { #[inline] fn to_object(&self, py: Python<'_>) -> PyObject { @@ -112,19 +114,19 @@ mod tests { } #[test] - fn test_topyobject() { + fn test_into_pyobject() { Python::with_gil(|py| { - let assert_eq = |l: PyObject, r: Bound<'_, PyAny>| { - assert!(l.bind(py).eq(r).unwrap()); + let assert_eq = |l: Bound<'_, PyAny>, r: Bound<'_, PyAny>| { + assert!(l.eq(r).unwrap()); }; assert_eq( - Tz::Europe__Paris.to_object(py), + Tz::Europe__Paris.into_pyobject(py).unwrap(), new_zoneinfo(py, "Europe/Paris"), ); - assert_eq(Tz::UTC.to_object(py), new_zoneinfo(py, "UTC")); + assert_eq(Tz::UTC.into_pyobject(py).unwrap(), new_zoneinfo(py, "UTC")); assert_eq( - Tz::Etc__GMTMinus5.to_object(py), + Tz::Etc__GMTMinus5.into_pyobject(py).unwrap(), new_zoneinfo(py, "Etc/GMT-5"), ); }); diff --git a/src/conversions/either.rs b/src/conversions/either.rs index 43822347c81..2cf1029ffb6 100644 --- a/src/conversions/either.rs +++ b/src/conversions/either.rs @@ -26,18 +26,19 @@ //! //! ```rust //! use either::Either; -//! use pyo3::{Python, ToPyObject}; +//! use pyo3::{Python, PyResult, IntoPyObject, types::PyAnyMethods}; //! -//! fn main() { +//! fn main() -> PyResult<()> { //! 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); +//! let py_str = "crab".into_pyobject(py)?; +//! let py_int = 42i32.into_pyobject(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(); -//! }); +//! let either_str: Either = py_str.extract()?; +//! let either_int: Either = py_int.extract()?; +//! Ok(()) +//! }) //! } //! ``` //! @@ -45,9 +46,11 @@ #[cfg(feature = "experimental-inspect")] use crate::inspect::types::TypeInfo; +#[allow(deprecated)] +use crate::ToPyObject; use crate::{ conversion::IntoPyObject, exceptions::PyTypeError, types::any::PyAnyMethods, Bound, - BoundObject, FromPyObject, IntoPy, PyAny, PyErr, PyObject, PyResult, Python, ToPyObject, + BoundObject, FromPyObject, IntoPy, PyAny, PyErr, PyObject, PyResult, Python, }; use either::Either; @@ -119,6 +122,7 @@ where } #[cfg_attr(docsrs, doc(cfg(feature = "either")))] +#[allow(deprecated)] impl ToPyObject for Either where L: ToPyObject, @@ -168,8 +172,9 @@ mod tests { use std::borrow::Cow; use crate::exceptions::PyTypeError; - use crate::{Python, ToPyObject}; + use crate::{IntoPyObject, Python}; + use crate::types::PyAnyMethods; use either::Either; #[test] @@ -180,30 +185,30 @@ mod tests { 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 obj_l = (&l).into_pyobject(py).unwrap(); + assert_eq!(obj_l.extract::().unwrap(), 42); + assert_eq!(obj_l.extract::().unwrap(), l); let r = E::Right("foo".to_owned()); - let obj_r = r.to_object(py); - assert_eq!(obj_r.extract::>(py).unwrap(), "foo"); - assert_eq!(obj_r.extract::(py).unwrap(), r); + let obj_r = (&r).into_pyobject(py).unwrap(); + assert_eq!(obj_r.extract::>().unwrap(), "foo"); + assert_eq!(obj_r.extract::().unwrap(), r); - let obj_s = "foo".to_object(py); - let err = obj_s.extract::(py).unwrap_err(); + let obj_s = "foo".into_pyobject(py).unwrap(); + let err = obj_s.extract::().unwrap_err(); assert!(err.is_instance_of::(py)); assert_eq!( err.to_string(), "TypeError: failed to convert the value to 'Union[i32, f32]'" ); - 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_i = 42i32.into_pyobject(py).unwrap(); + assert_eq!(obj_i.extract::().unwrap(), E1::Left(42)); + assert_eq!(obj_i.extract::().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)); + let obj_f = 42.0f64.into_pyobject(py).unwrap(); + assert_eq!(obj_f.extract::().unwrap(), E1::Right(42.0)); + assert_eq!(obj_f.extract::().unwrap(), E2::Left(42.0)); }); } } diff --git a/src/conversions/hashbrown.rs b/src/conversions/hashbrown.rs index db42075adeb..25ec014341e 100644 --- a/src/conversions/hashbrown.rs +++ b/src/conversions/hashbrown.rs @@ -16,6 +16,8 @@ //! //! Note that you must use compatible versions of hashbrown and PyO3. //! The required hashbrown version may vary based on the version of PyO3. +#[allow(deprecated)] +use crate::ToPyObject; use crate::{ conversion::IntoPyObject, types::{ @@ -25,10 +27,11 @@ use crate::{ set::{new_from_iter, try_new_from_iter, PySetMethods}, PyDict, PyFrozenSet, PySet, }, - Bound, FromPyObject, IntoPy, PyAny, PyErr, PyObject, PyResult, Python, ToPyObject, + Bound, FromPyObject, IntoPy, PyAny, PyErr, PyObject, PyResult, Python, }; use std::{cmp, hash}; +#[allow(deprecated)] impl ToPyObject for hashbrown::HashMap where K: hash::Hash + cmp::Eq + ToPyObject, @@ -113,6 +116,7 @@ where } } +#[allow(deprecated)] impl ToPyObject for hashbrown::HashSet where T: hash::Hash + Eq + ToPyObject, @@ -194,8 +198,7 @@ mod tests { let mut map = hashbrown::HashMap::::new(); map.insert(1, 1); - let m = map.to_object(py); - let py_map = m.downcast_bound::(py).unwrap(); + let py_map = (&map).into_pyobject(py).unwrap(); assert!(py_map.len() == 1); assert!( diff --git a/src/conversions/indexmap.rs b/src/conversions/indexmap.rs index acb466c3d0a..8d74b126f29 100644 --- a/src/conversions/indexmap.rs +++ b/src/conversions/indexmap.rs @@ -89,9 +89,12 @@ use crate::conversion::IntoPyObject; use crate::types::*; -use crate::{Bound, FromPyObject, IntoPy, PyErr, PyObject, Python, ToPyObject}; +#[allow(deprecated)] +use crate::ToPyObject; +use crate::{Bound, FromPyObject, IntoPy, PyErr, PyObject, Python}; use std::{cmp, hash}; +#[allow(deprecated)] impl ToPyObject for indexmap::IndexMap where K: hash::Hash + cmp::Eq + ToPyObject, @@ -180,7 +183,7 @@ where mod test_indexmap { use crate::types::*; - use crate::{IntoPy, PyObject, Python, ToPyObject}; + use crate::{IntoPy, IntoPyObject, PyObject, Python}; #[test] fn test_indexmap_indexmap_to_python() { @@ -188,8 +191,7 @@ mod test_indexmap { let mut map = indexmap::IndexMap::::new(); map.insert(1, 1); - let m = map.to_object(py); - let py_map = m.downcast_bound::(py).unwrap(); + let py_map = (&map).into_pyobject(py).unwrap(); assert!(py_map.len() == 1); assert!( diff --git a/src/conversions/num_bigint.rs b/src/conversions/num_bigint.rs index e05a807bb2c..df8e108776f 100644 --- a/src/conversions/num_bigint.rs +++ b/src/conversions/num_bigint.rs @@ -49,12 +49,14 @@ #[cfg(Py_LIMITED_API)] use crate::types::{bytes::PyBytesMethods, PyBytes}; +#[allow(deprecated)] +use crate::ToPyObject; use crate::{ conversion::IntoPyObject, ffi, instance::Bound, types::{any::PyAnyMethods, PyInt}, - FromPyObject, IntoPy, Py, PyAny, PyErr, PyObject, PyResult, Python, ToPyObject, + FromPyObject, IntoPy, Py, PyAny, PyErr, PyObject, PyResult, Python, }; use num_bigint::{BigInt, BigUint}; @@ -66,6 +68,7 @@ use num_bigint::Sign; macro_rules! bigint_conversion { ($rust_ty: ty, $is_signed: literal, $to_bytes: path) => { #[cfg_attr(docsrs, doc(cfg(feature = "num-bigint")))] + #[allow(deprecated)] impl ToPyObject for $rust_ty { #[inline] fn to_object(&self, py: Python<'_>) -> PyObject { @@ -355,11 +358,11 @@ mod tests { }) } - fn python_fib(py: Python<'_>) -> impl Iterator + '_ { - let mut f0 = 1.to_object(py); - let mut f1 = 1.to_object(py); + fn python_fib(py: Python<'_>) -> impl Iterator> + '_ { + let mut f0 = 1i32.into_pyobject(py).unwrap().into_any(); + let mut f1 = 1i32.into_pyobject(py).unwrap().into_any(); std::iter::from_fn(move || { - let f2 = f0.call_method1(py, "__add__", (f1.bind(py),)).unwrap(); + let f2 = f0.call_method1("__add__", (&f1,)).unwrap(); Some(std::mem::replace(&mut f0, std::mem::replace(&mut f1, f2))) }) } @@ -370,9 +373,9 @@ mod tests { // check the first 2000 numbers in the fibonacci sequence for (py_result, rs_result) in python_fib(py).zip(rust_fib::()).take(2000) { // Python -> Rust - assert_eq!(py_result.extract::(py).unwrap(), rs_result); + assert_eq!(py_result.extract::().unwrap(), rs_result); // Rust -> Python - assert!(py_result.bind(py).eq(rs_result).unwrap()); + assert!(py_result.eq(rs_result).unwrap()); } }); } @@ -383,19 +386,19 @@ mod tests { // check the first 2000 numbers in the fibonacci sequence for (py_result, rs_result) in python_fib(py).zip(rust_fib::()).take(2000) { // Python -> Rust - assert_eq!(py_result.extract::(py).unwrap(), rs_result); + assert_eq!(py_result.extract::().unwrap(), rs_result); // Rust -> Python - assert!(py_result.bind(py).eq(&rs_result).unwrap()); + assert!(py_result.eq(&rs_result).unwrap()); // negate let rs_result = rs_result * -1; - let py_result = py_result.call_method0(py, "__neg__").unwrap(); + let py_result = py_result.call_method0("__neg__").unwrap(); // Python -> Rust - assert_eq!(py_result.extract::(py).unwrap(), rs_result); + assert_eq!(py_result.extract::().unwrap(), rs_result); // Rust -> Python - assert!(py_result.bind(py).eq(rs_result).unwrap()); + assert!(py_result.eq(rs_result).unwrap()); } }); } @@ -435,7 +438,7 @@ mod tests { #[test] fn handle_zero() { Python::with_gil(|py| { - let zero: BigInt = 0.to_object(py).extract(py).unwrap(); + let zero: BigInt = 0i32.into_pyobject(py).unwrap().extract().unwrap(); assert_eq!(zero, BigInt::from(0)); }) } diff --git a/src/conversions/num_complex.rs b/src/conversions/num_complex.rs index bf28aa73932..4b3be8e15b0 100644 --- a/src/conversions/num_complex.rs +++ b/src/conversions/num_complex.rs @@ -93,11 +93,13 @@ //! result = get_eigenvalues(m11,m12,m21,m22) //! assert result == [complex(1,-1), complex(-2,0)] //! ``` +#[allow(deprecated)] +use crate::ToPyObject; use crate::{ ffi, ffi_ptr_ext::FfiPtrExt, types::{any::PyAnyMethods, PyComplex}, - Bound, FromPyObject, PyAny, PyErr, PyObject, PyResult, Python, ToPyObject, + Bound, FromPyObject, PyAny, PyErr, PyObject, PyResult, Python, }; use num_complex::Complex; use std::os::raw::c_double; @@ -119,6 +121,7 @@ impl PyComplex { macro_rules! complex_conversion { ($float: ty) => { #[cfg_attr(docsrs, doc(cfg(feature = "num-complex")))] + #[allow(deprecated)] impl ToPyObject for Complex<$float> { #[inline] fn to_object(&self, py: Python<'_>) -> PyObject { @@ -218,6 +221,7 @@ mod tests { use super::*; use crate::tests::common::generate_unique_module_name; use crate::types::{complex::PyComplexMethods, PyModule}; + use crate::IntoPyObject; use pyo3_ffi::c_str; #[test] @@ -232,16 +236,16 @@ mod tests { #[test] fn to_from_complex() { Python::with_gil(|py| { - let val = Complex::new(3.0, 1.2); - let obj = val.to_object(py); - assert_eq!(obj.extract::>(py).unwrap(), val); + let val = Complex::new(3.0f64, 1.2); + let obj = val.into_pyobject(py).unwrap(); + assert_eq!(obj.extract::>().unwrap(), val); }); } #[test] fn from_complex_err() { Python::with_gil(|py| { - let obj = vec![1].to_object(py); - assert!(obj.extract::>(py).is_err()); + let obj = vec![1i32].into_pyobject(py).unwrap(); + assert!(obj.extract::>().is_err()); }); } #[test] diff --git a/src/conversions/num_rational.rs b/src/conversions/num_rational.rs index 2448ad3701e..cda877502b3 100644 --- a/src/conversions/num_rational.rs +++ b/src/conversions/num_rational.rs @@ -48,9 +48,9 @@ use crate::ffi; use crate::sync::GILOnceCell; use crate::types::any::PyAnyMethods; use crate::types::PyType; -use crate::{ - Bound, FromPyObject, IntoPy, Py, PyAny, PyErr, PyObject, PyResult, Python, ToPyObject, -}; +#[allow(deprecated)] +use crate::ToPyObject; +use crate::{Bound, FromPyObject, IntoPy, Py, PyAny, PyErr, PyObject, PyResult, Python}; #[cfg(feature = "num-bigint")] use num_bigint::BigInt; @@ -84,6 +84,7 @@ macro_rules! rational_conversion { } } + #[allow(deprecated)] impl ToPyObject for Ratio<$int> { #[inline] fn to_object(&self, py: Python<'_>) -> PyObject { diff --git a/src/conversions/rust_decimal.rs b/src/conversions/rust_decimal.rs index c353eb91d8a..7926592119a 100644 --- a/src/conversions/rust_decimal.rs +++ b/src/conversions/rust_decimal.rs @@ -55,9 +55,9 @@ use crate::sync::GILOnceCell; use crate::types::any::PyAnyMethods; use crate::types::string::PyStringMethods; use crate::types::PyType; -use crate::{ - Bound, FromPyObject, IntoPy, Py, PyAny, PyErr, PyObject, PyResult, Python, ToPyObject, -}; +#[allow(deprecated)] +use crate::ToPyObject; +use crate::{Bound, FromPyObject, IntoPy, Py, PyAny, PyErr, PyObject, PyResult, Python}; use rust_decimal::Decimal; use std::str::FromStr; @@ -82,6 +82,7 @@ fn get_decimal_cls(py: Python<'_>) -> PyResult<&Bound<'_, PyType>> { DECIMAL_CLS.import(py, "decimal", "Decimal") } +#[allow(deprecated)] impl ToPyObject for Decimal { #[inline] fn to_object(&self, py: Python<'_>) -> PyObject { diff --git a/src/conversions/smallvec.rs b/src/conversions/smallvec.rs index b9e48ace2dd..dc8b18437a5 100644 --- a/src/conversions/smallvec.rs +++ b/src/conversions/smallvec.rs @@ -23,12 +23,14 @@ use crate::types::any::PyAnyMethods; use crate::types::list::new_from_iter; use crate::types::{PySequence, PyString}; use crate::PyErr; +#[allow(deprecated)] +use crate::ToPyObject; use crate::{ err::DowncastError, ffi, Bound, FromPyObject, IntoPy, PyAny, PyObject, PyResult, Python, - ToPyObject, }; use smallvec::{Array, SmallVec}; +#[allow(deprecated)] impl ToPyObject for SmallVec where A: Array, @@ -167,10 +169,10 @@ mod tests { } #[test] - fn test_smallvec_to_object() { + fn test_smallvec_into_pyobject() { 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 hso = sv.into_pyobject(py).unwrap(); let l = PyList::new(py, [1, 2, 3, 4, 5]).unwrap(); assert!(l.eq(hso).unwrap()); }); diff --git a/src/conversions/std/array.rs b/src/conversions/std/array.rs index 5a07b224c5b..c184aa7f732 100644 --- a/src/conversions/std/array.rs +++ b/src/conversions/std/array.rs @@ -2,10 +2,9 @@ use crate::conversion::IntoPyObject; use crate::instance::Bound; use crate::types::any::PyAnyMethods; use crate::types::PySequence; -use crate::{ - err::DowncastError, ffi, FromPyObject, IntoPy, Py, PyAny, PyObject, PyResult, Python, - ToPyObject, -}; +#[allow(deprecated)] +use crate::ToPyObject; +use crate::{err::DowncastError, ffi, FromPyObject, IntoPy, Py, PyAny, PyObject, PyResult, Python}; use crate::{exceptions, PyErr}; impl IntoPy for [T; N] @@ -69,6 +68,7 @@ where } } +#[allow(deprecated)] impl ToPyObject for [T; N] where T: ToPyObject, @@ -168,7 +168,7 @@ mod tests { ffi, types::{any::PyAnyMethods, PyBytes, PyBytesMethods}, }; - use crate::{types::PyList, IntoPy, PyResult, Python, ToPyObject}; + use crate::{types::PyList, IntoPy, PyResult, Python}; #[test] fn array_try_from_fn() { @@ -219,11 +219,11 @@ mod tests { }); } #[test] - fn test_topyobject_array_conversion() { + fn test_into_pyobject_array_conversion() { Python::with_gil(|py| { let array: [f32; 4] = [0.0, -16.0, 16.0, 42.0]; - let pyobject = array.to_object(py); - let pylist = pyobject.downcast_bound::(py).unwrap(); + let pyobject = array.into_pyobject(py).unwrap(); + let pylist = pyobject.downcast::().unwrap(); assert_eq!(pylist.get_item(0).unwrap().extract::().unwrap(), 0.0); assert_eq!(pylist.get_item(1).unwrap().extract::().unwrap(), -16.0); assert_eq!(pylist.get_item(2).unwrap().extract::().unwrap(), 16.0); diff --git a/src/conversions/std/cell.rs b/src/conversions/std/cell.rs index 3c1ab55c4a7..369d91bf69e 100644 --- a/src/conversions/std/cell.rs +++ b/src/conversions/std/cell.rs @@ -2,10 +2,11 @@ use std::cell::Cell; use crate::{ conversion::IntoPyObject, types::any::PyAnyMethods, Bound, FromPyObject, IntoPy, PyAny, - PyObject, PyResult, Python, ToPyObject, + PyObject, PyResult, Python, }; -impl ToPyObject for Cell { +#[allow(deprecated)] +impl crate::ToPyObject for Cell { fn to_object(&self, py: Python<'_>) -> PyObject { self.get().to_object(py) } diff --git a/src/conversions/std/ipaddr.rs b/src/conversions/std/ipaddr.rs index bff81ad1a1f..6c5e74d4881 100755 --- a/src/conversions/std/ipaddr.rs +++ b/src/conversions/std/ipaddr.rs @@ -7,9 +7,9 @@ use crate::sync::GILOnceCell; use crate::types::any::PyAnyMethods; use crate::types::string::PyStringMethods; use crate::types::PyType; -use crate::{ - intern, FromPyObject, IntoPy, Py, PyAny, PyErr, PyObject, PyResult, Python, ToPyObject, -}; +#[allow(deprecated)] +use crate::ToPyObject; +use crate::{intern, FromPyObject, IntoPy, Py, PyAny, PyErr, PyObject, PyResult, Python}; impl FromPyObject<'_> for IpAddr { fn extract_bound(obj: &Bound<'_, PyAny>) -> PyResult { @@ -31,6 +31,7 @@ impl FromPyObject<'_> for IpAddr { } } +#[allow(deprecated)] impl ToPyObject for Ipv4Addr { #[inline] fn to_object(&self, py: Python<'_>) -> PyObject { @@ -62,6 +63,7 @@ impl<'py> IntoPyObject<'py> for &Ipv4Addr { } } +#[allow(deprecated)] impl ToPyObject for Ipv6Addr { #[inline] fn to_object(&self, py: Python<'_>) -> PyObject { @@ -93,6 +95,7 @@ impl<'py> IntoPyObject<'py> for &Ipv6Addr { } } +#[allow(deprecated)] impl ToPyObject for IpAddr { #[inline] fn to_object(&self, py: Python<'_>) -> PyObject { @@ -168,11 +171,11 @@ mod test_ipaddr { fn test_from_pystring() { Python::with_gil(|py| { let py_str = PyString::new(py, "0:0:0:0:0:0:0:1"); - let ip: IpAddr = py_str.to_object(py).extract(py).unwrap(); + let ip: IpAddr = py_str.extract().unwrap(); assert_eq!(ip, IpAddr::from_str("::1").unwrap()); let py_str = PyString::new(py, "invalid"); - assert!(py_str.to_object(py).extract::(py).is_err()); + assert!(py_str.extract::().is_err()); }); } } diff --git a/src/conversions/std/map.rs b/src/conversions/std/map.rs index 582c56b613f..888bd13c180 100644 --- a/src/conversions/std/map.rs +++ b/src/conversions/std/map.rs @@ -2,13 +2,16 @@ use std::{cmp, collections, hash}; #[cfg(feature = "experimental-inspect")] use crate::inspect::types::TypeInfo; +#[allow(deprecated)] +use crate::ToPyObject; use crate::{ conversion::IntoPyObject, instance::Bound, types::{any::PyAnyMethods, dict::PyDictMethods, PyDict}, - FromPyObject, IntoPy, PyAny, PyErr, PyObject, Python, ToPyObject, + FromPyObject, IntoPy, PyAny, PyErr, PyObject, Python, }; +#[allow(deprecated)] impl ToPyObject for collections::HashMap where K: hash::Hash + cmp::Eq + ToPyObject, @@ -24,6 +27,7 @@ where } } +#[allow(deprecated)] impl ToPyObject for collections::BTreeMap where K: cmp::Eq + ToPyObject, @@ -203,8 +207,7 @@ mod tests { let mut map = HashMap::::new(); map.insert(1, 1); - let m = map.to_object(py); - let py_map = m.downcast_bound::(py).unwrap(); + let py_map = (&map).into_pyobject(py).unwrap(); assert!(py_map.len() == 1); assert!( @@ -226,8 +229,7 @@ mod tests { let mut map = BTreeMap::::new(); map.insert(1, 1); - let m = map.to_object(py); - let py_map = m.downcast_bound::(py).unwrap(); + let py_map = (&map).into_pyobject(py).unwrap(); assert!(py_map.len() == 1); assert!( diff --git a/src/conversions/std/num.rs b/src/conversions/std/num.rs index d1faa905abd..e7b8addac82 100644 --- a/src/conversions/std/num.rs +++ b/src/conversions/std/num.rs @@ -5,9 +5,10 @@ use crate::ffi_ptr_ext::FfiPtrExt; use crate::inspect::types::TypeInfo; use crate::types::any::PyAnyMethods; use crate::types::{PyBytes, PyInt}; +#[allow(deprecated)] +use crate::ToPyObject; use crate::{ exceptions, ffi, Bound, FromPyObject, IntoPy, PyAny, PyErr, PyObject, PyResult, Python, - ToPyObject, }; use std::convert::Infallible; use std::num::{ @@ -18,6 +19,7 @@ use std::os::raw::c_long; macro_rules! int_fits_larger_int { ($rust_type:ty, $larger_type:ty) => { + #[allow(deprecated)] impl ToPyObject for $rust_type { #[inline] fn to_object(&self, py: Python<'_>) -> PyObject { @@ -98,6 +100,7 @@ macro_rules! extract_int { macro_rules! int_convert_u64_or_i64 { ($rust_type:ty, $pylong_from_ll_or_ull:expr, $pylong_as_ll_or_ull:expr, $force_index_call:literal) => { + #[allow(deprecated)] impl ToPyObject for $rust_type { #[inline] fn to_object(&self, py: Python<'_>) -> PyObject { @@ -153,6 +156,7 @@ macro_rules! int_convert_u64_or_i64 { macro_rules! int_fits_c_long { ($rust_type:ty) => { + #[allow(deprecated)] impl ToPyObject for $rust_type { #[inline] fn to_object(&self, py: Python<'_>) -> PyObject { @@ -211,6 +215,7 @@ macro_rules! int_fits_c_long { }; } +#[allow(deprecated)] impl ToPyObject for u8 { #[inline] fn to_object(&self, py: Python<'_>) -> PyObject { @@ -328,6 +333,7 @@ mod fast_128bit_int_conversion { // for 128bit Integers macro_rules! int_convert_128 { ($rust_type: ty, $is_signed: literal) => { + #[allow(deprecated)] impl ToPyObject for $rust_type { #[inline] fn to_object(&self, py: Python<'_>) -> PyObject { @@ -474,6 +480,7 @@ mod slow_128bit_int_conversion { // for 128bit Integers macro_rules! int_convert_128 { ($rust_type: ty, $half_type: ty) => { + #[allow(deprecated)] impl ToPyObject for $rust_type { #[inline] fn to_object(&self, py: Python<'_>) -> PyObject { @@ -571,6 +578,7 @@ fn err_if_invalid_value( macro_rules! nonzero_int_impl { ($nonzero_type:ty, $primitive_type:ty) => { + #[allow(deprecated)] impl ToPyObject for $nonzero_type { #[inline] fn to_object(&self, py: Python<'_>) -> PyObject { @@ -717,10 +725,10 @@ mod test_128bit_integers { fn test_i128_max() { Python::with_gil(|py| { let v = i128::MAX; - let obj = v.to_object(py); - assert_eq!(v, obj.extract::(py).unwrap()); - assert_eq!(v as u128, obj.extract::(py).unwrap()); - assert!(obj.extract::(py).is_err()); + let obj = v.into_pyobject(py).unwrap(); + assert_eq!(v, obj.extract::().unwrap()); + assert_eq!(v as u128, obj.extract::().unwrap()); + assert!(obj.extract::().is_err()); }) } @@ -728,10 +736,10 @@ mod test_128bit_integers { fn test_i128_min() { Python::with_gil(|py| { let v = i128::MIN; - let obj = v.to_object(py); - assert_eq!(v, obj.extract::(py).unwrap()); - assert!(obj.extract::(py).is_err()); - assert!(obj.extract::(py).is_err()); + let obj = v.into_pyobject(py).unwrap(); + assert_eq!(v, obj.extract::().unwrap()); + assert!(obj.extract::().is_err()); + assert!(obj.extract::().is_err()); }) } @@ -739,9 +747,9 @@ mod test_128bit_integers { fn test_u128_max() { Python::with_gil(|py| { let v = u128::MAX; - let obj = v.to_object(py); - assert_eq!(v, obj.extract::(py).unwrap()); - assert!(obj.extract::(py).is_err()); + let obj = v.into_pyobject(py).unwrap(); + assert_eq!(v, obj.extract::().unwrap()); + assert!(obj.extract::().is_err()); }) } @@ -767,13 +775,13 @@ mod test_128bit_integers { fn test_nonzero_i128_max() { Python::with_gil(|py| { let v = NonZeroI128::new(i128::MAX).unwrap(); - let obj = v.to_object(py); - assert_eq!(v, obj.extract::(py).unwrap()); + let obj = v.into_pyobject(py).unwrap(); + assert_eq!(v, obj.extract::().unwrap()); assert_eq!( NonZeroU128::new(v.get() as u128).unwrap(), - obj.extract::(py).unwrap() + obj.extract::().unwrap() ); - assert!(obj.extract::(py).is_err()); + assert!(obj.extract::().is_err()); }) } @@ -781,10 +789,10 @@ mod test_128bit_integers { fn test_nonzero_i128_min() { Python::with_gil(|py| { let v = NonZeroI128::new(i128::MIN).unwrap(); - let obj = v.to_object(py); - assert_eq!(v, obj.extract::(py).unwrap()); - assert!(obj.extract::(py).is_err()); - assert!(obj.extract::(py).is_err()); + let obj = v.into_pyobject(py).unwrap(); + assert_eq!(v, obj.extract::().unwrap()); + assert!(obj.extract::().is_err()); + assert!(obj.extract::().is_err()); }) } @@ -792,9 +800,9 @@ mod test_128bit_integers { fn test_nonzero_u128_max() { Python::with_gil(|py| { let v = NonZeroU128::new(u128::MAX).unwrap(); - let obj = v.to_object(py); - assert_eq!(v, obj.extract::(py).unwrap()); - assert!(obj.extract::(py).is_err()); + let obj = v.into_pyobject(py).unwrap(); + assert_eq!(v, obj.extract::().unwrap()); + assert!(obj.extract::().is_err()); }) } @@ -837,18 +845,18 @@ mod test_128bit_integers { #[cfg(test)] mod tests { - use crate::Python; - use crate::ToPyObject; + use crate::types::PyAnyMethods; + use crate::{IntoPyObject, Python}; use std::num::*; #[test] fn test_u32_max() { Python::with_gil(|py| { let v = u32::MAX; - let obj = v.to_object(py); - assert_eq!(v, obj.extract::(py).unwrap()); - assert_eq!(u64::from(v), obj.extract::(py).unwrap()); - assert!(obj.extract::(py).is_err()); + let obj = v.into_pyobject(py).unwrap(); + assert_eq!(v, obj.extract::().unwrap()); + assert_eq!(u64::from(v), obj.extract::().unwrap()); + assert!(obj.extract::().is_err()); }); } @@ -856,10 +864,10 @@ mod tests { fn test_i64_max() { Python::with_gil(|py| { let v = i64::MAX; - let obj = v.to_object(py); - assert_eq!(v, obj.extract::(py).unwrap()); - assert_eq!(v as u64, obj.extract::(py).unwrap()); - assert!(obj.extract::(py).is_err()); + let obj = v.into_pyobject(py).unwrap(); + assert_eq!(v, obj.extract::().unwrap()); + assert_eq!(v as u64, obj.extract::().unwrap()); + assert!(obj.extract::().is_err()); }); } @@ -867,10 +875,10 @@ mod tests { fn test_i64_min() { Python::with_gil(|py| { let v = i64::MIN; - let obj = v.to_object(py); - assert_eq!(v, obj.extract::(py).unwrap()); - assert!(obj.extract::(py).is_err()); - assert!(obj.extract::(py).is_err()); + let obj = v.into_pyobject(py).unwrap(); + assert_eq!(v, obj.extract::().unwrap()); + assert!(obj.extract::().is_err()); + assert!(obj.extract::().is_err()); }); } @@ -878,9 +886,9 @@ mod tests { fn test_u64_max() { Python::with_gil(|py| { let v = u64::MAX; - let obj = v.to_object(py); - assert_eq!(v, obj.extract::(py).unwrap()); - assert!(obj.extract::(py).is_err()); + let obj = v.into_pyobject(py).unwrap(); + assert_eq!(v, obj.extract::().unwrap()); + assert!(obj.extract::().is_err()); }); } @@ -888,14 +896,15 @@ mod tests { ($test_mod_name:ident, $t:ty) => ( mod $test_mod_name { use crate::exceptions; - use crate::ToPyObject; + use crate::conversion::IntoPyObject; + use crate::types::PyAnyMethods; use crate::Python; #[test] fn from_py_string_type_error() { Python::with_gil(|py| { - let obj = ("123").to_object(py); - let err = obj.extract::<$t>(py).unwrap_err(); + let obj = ("123").into_pyobject(py).unwrap(); + let err = obj.extract::<$t>().unwrap_err(); assert!(err.is_instance_of::(py)); }); } @@ -903,8 +912,8 @@ mod tests { #[test] fn from_py_float_type_error() { Python::with_gil(|py| { - let obj = (12.3).to_object(py); - let err = obj.extract::<$t>(py).unwrap_err(); + let obj = (12.3f64).into_pyobject(py).unwrap(); + let err = obj.extract::<$t>().unwrap_err(); assert!(err.is_instance_of::(py));}); } @@ -912,8 +921,8 @@ mod tests { fn to_py_object_and_back() { Python::with_gil(|py| { let val = 123 as $t; - let obj = val.to_object(py); - assert_eq!(obj.extract::<$t>(py).unwrap(), val as $t);}); + let obj = val.into_pyobject(py).unwrap(); + assert_eq!(obj.extract::<$t>().unwrap(), val as $t);}); } } ) @@ -936,10 +945,10 @@ mod tests { fn test_nonzero_u32_max() { Python::with_gil(|py| { let v = NonZeroU32::new(u32::MAX).unwrap(); - let obj = v.to_object(py); - assert_eq!(v, obj.extract::(py).unwrap()); - assert_eq!(NonZeroU64::from(v), obj.extract::(py).unwrap()); - assert!(obj.extract::(py).is_err()); + let obj = v.into_pyobject(py).unwrap(); + assert_eq!(v, obj.extract::().unwrap()); + assert_eq!(NonZeroU64::from(v), obj.extract::().unwrap()); + assert!(obj.extract::().is_err()); }); } @@ -947,13 +956,13 @@ mod tests { fn test_nonzero_i64_max() { Python::with_gil(|py| { let v = NonZeroI64::new(i64::MAX).unwrap(); - let obj = v.to_object(py); - assert_eq!(v, obj.extract::(py).unwrap()); + let obj = v.into_pyobject(py).unwrap(); + assert_eq!(v, obj.extract::().unwrap()); assert_eq!( NonZeroU64::new(v.get() as u64).unwrap(), - obj.extract::(py).unwrap() + obj.extract::().unwrap() ); - assert!(obj.extract::(py).is_err()); + assert!(obj.extract::().is_err()); }); } @@ -961,10 +970,10 @@ mod tests { fn test_nonzero_i64_min() { Python::with_gil(|py| { let v = NonZeroI64::new(i64::MIN).unwrap(); - let obj = v.to_object(py); - assert_eq!(v, obj.extract::(py).unwrap()); - assert!(obj.extract::(py).is_err()); - assert!(obj.extract::(py).is_err()); + let obj = v.into_pyobject(py).unwrap(); + assert_eq!(v, obj.extract::().unwrap()); + assert!(obj.extract::().is_err()); + assert!(obj.extract::().is_err()); }); } @@ -972,9 +981,9 @@ mod tests { fn test_nonzero_u64_max() { Python::with_gil(|py| { let v = NonZeroU64::new(u64::MAX).unwrap(); - let obj = v.to_object(py); - assert_eq!(v, obj.extract::(py).unwrap()); - assert!(obj.extract::(py).is_err()); + let obj = v.into_pyobject(py).unwrap(); + assert_eq!(v, obj.extract::().unwrap()); + assert!(obj.extract::().is_err()); }); } @@ -982,15 +991,16 @@ mod tests { ($test_mod_name:ident, $t:ty) => ( mod $test_mod_name { use crate::exceptions; - use crate::ToPyObject; + use crate::conversion::IntoPyObject; + use crate::types::PyAnyMethods; use crate::Python; use std::num::*; #[test] fn from_py_string_type_error() { Python::with_gil(|py| { - let obj = ("123").to_object(py); - let err = obj.extract::<$t>(py).unwrap_err(); + let obj = ("123").into_pyobject(py).unwrap(); + let err = obj.extract::<$t>().unwrap_err(); assert!(err.is_instance_of::(py)); }); } @@ -998,8 +1008,8 @@ mod tests { #[test] fn from_py_float_type_error() { Python::with_gil(|py| { - let obj = (12.3).to_object(py); - let err = obj.extract::<$t>(py).unwrap_err(); + let obj = (12.3f64).into_pyobject(py).unwrap(); + let err = obj.extract::<$t>().unwrap_err(); assert!(err.is_instance_of::(py));}); } @@ -1007,8 +1017,8 @@ mod tests { fn to_py_object_and_back() { Python::with_gil(|py| { let val = <$t>::new(123).unwrap(); - let obj = val.to_object(py); - assert_eq!(obj.extract::<$t>(py).unwrap(), val);}); + let obj = val.into_pyobject(py).unwrap(); + assert_eq!(obj.extract::<$t>().unwrap(), val);}); } } ) @@ -1030,22 +1040,22 @@ mod tests { #[test] fn test_i64_bool() { Python::with_gil(|py| { - let obj = true.to_object(py); - assert_eq!(1, obj.extract::(py).unwrap()); - let obj = false.to_object(py); - assert_eq!(0, obj.extract::(py).unwrap()); + let obj = true.into_pyobject(py).unwrap(); + assert_eq!(1, obj.extract::().unwrap()); + let obj = false.into_pyobject(py).unwrap(); + assert_eq!(0, obj.extract::().unwrap()); }) } #[test] fn test_i64_f64() { Python::with_gil(|py| { - let obj = 12.34f64.to_object(py); - let err = obj.extract::(py).unwrap_err(); + let obj = 12.34f64.into_pyobject(py).unwrap(); + let err = obj.extract::().unwrap_err(); assert!(err.is_instance_of::(py)); // with no remainder - let obj = 12f64.to_object(py); - let err = obj.extract::(py).unwrap_err(); + let obj = 12f64.into_pyobject(py).unwrap(); + let err = obj.extract::().unwrap_err(); assert!(err.is_instance_of::(py)); }) } diff --git a/src/conversions/std/option.rs b/src/conversions/std/option.rs index 38eaebd499b..a9c8908faa7 100644 --- a/src/conversions/std/option.rs +++ b/src/conversions/std/option.rs @@ -1,13 +1,14 @@ use crate::{ conversion::IntoPyObject, ffi, types::any::PyAnyMethods, AsPyPointer, Bound, BoundObject, - FromPyObject, IntoPy, PyAny, PyObject, PyResult, Python, ToPyObject, + FromPyObject, IntoPy, PyAny, PyObject, PyResult, Python, }; /// `Option::Some` is converted like `T`. /// `Option::None` is converted to Python `None`. -impl ToPyObject for Option +#[allow(deprecated)] +impl crate::ToPyObject for Option where - T: ToPyObject, + T: crate::ToPyObject, { fn to_object(&self, py: Python<'_>) -> PyObject { self.as_ref() diff --git a/src/conversions/std/osstr.rs b/src/conversions/std/osstr.rs index 57387b1e600..14c5902d8d9 100644 --- a/src/conversions/std/osstr.rs +++ b/src/conversions/std/osstr.rs @@ -3,11 +3,14 @@ use crate::ffi_ptr_ext::FfiPtrExt; use crate::instance::Bound; use crate::types::any::PyAnyMethods; use crate::types::PyString; -use crate::{ffi, FromPyObject, IntoPy, PyAny, PyObject, PyResult, Python, ToPyObject}; +#[allow(deprecated)] +use crate::ToPyObject; +use crate::{ffi, FromPyObject, IntoPy, PyAny, PyObject, PyResult, Python}; use std::borrow::Cow; use std::convert::Infallible; use std::ffi::{OsStr, OsString}; +#[allow(deprecated)] impl ToPyObject for OsStr { #[inline] fn to_object(&self, py: Python<'_>) -> PyObject { @@ -138,6 +141,7 @@ impl IntoPy for &'_ OsStr { } } +#[allow(deprecated)] impl ToPyObject for Cow<'_, OsStr> { #[inline] fn to_object(&self, py: Python<'_>) -> PyObject { @@ -174,6 +178,7 @@ impl<'py> IntoPyObject<'py> for &Cow<'_, OsStr> { } } +#[allow(deprecated)] impl ToPyObject for OsString { #[inline] fn to_object(&self, py: Python<'_>) -> PyObject { @@ -219,10 +224,8 @@ impl<'py> IntoPyObject<'py> for &OsString { #[cfg(test)] mod tests { - use crate::prelude::IntoPyObject; - use crate::types::{PyAnyMethods, PyStringMethods}; - use crate::BoundObject; - use crate::{types::PyString, IntoPy, PyObject, Python}; + use crate::types::{PyAnyMethods, PyString, PyStringMethods}; + use crate::{BoundObject, IntoPy, IntoPyObject, PyObject, Python}; use std::fmt::Debug; use std::{ borrow::Cow, diff --git a/src/conversions/std/path.rs b/src/conversions/std/path.rs index 615d98e3cd8..d1b628b065e 100644 --- a/src/conversions/std/path.rs +++ b/src/conversions/std/path.rs @@ -3,12 +3,15 @@ use crate::ffi_ptr_ext::FfiPtrExt; use crate::instance::Bound; use crate::types::any::PyAnyMethods; use crate::types::PyString; -use crate::{ffi, FromPyObject, IntoPy, PyAny, PyObject, PyResult, Python, ToPyObject}; +#[allow(deprecated)] +use crate::ToPyObject; +use crate::{ffi, FromPyObject, IntoPy, PyAny, PyObject, PyResult, Python}; use std::borrow::Cow; use std::convert::Infallible; use std::ffi::OsString; use std::path::{Path, PathBuf}; +#[allow(deprecated)] impl ToPyObject for Path { #[inline] fn to_object(&self, py: Python<'_>) -> PyObject { @@ -55,6 +58,7 @@ impl<'py> IntoPyObject<'py> for &&Path { } } +#[allow(deprecated)] impl ToPyObject for Cow<'_, Path> { #[inline] fn to_object(&self, py: Python<'_>) -> PyObject { @@ -91,6 +95,7 @@ impl<'py> IntoPyObject<'py> for &Cow<'_, Path> { } } +#[allow(deprecated)] impl ToPyObject for PathBuf { #[inline] fn to_object(&self, py: Python<'_>) -> PyObject { @@ -136,10 +141,8 @@ impl<'py> IntoPyObject<'py> for &PathBuf { #[cfg(test)] mod tests { - use crate::prelude::IntoPyObject; - use crate::types::{PyAnyMethods, PyStringMethods}; - use crate::BoundObject; - use crate::{types::PyString, IntoPy, PyObject, Python}; + use crate::types::{PyAnyMethods, PyString, PyStringMethods}; + use crate::{BoundObject, IntoPy, IntoPyObject, PyObject, Python}; use std::borrow::Cow; use std::fmt::Debug; use std::path::{Path, PathBuf}; diff --git a/src/conversions/std/set.rs b/src/conversions/std/set.rs index cc706c43f01..ff5ab572bb5 100644 --- a/src/conversions/std/set.rs +++ b/src/conversions/std/set.rs @@ -2,6 +2,8 @@ use std::{cmp, collections, hash}; #[cfg(feature = "experimental-inspect")] use crate::inspect::types::TypeInfo; +#[allow(deprecated)] +use crate::ToPyObject; use crate::{ conversion::IntoPyObject, instance::Bound, @@ -11,9 +13,10 @@ use crate::{ set::{new_from_iter, try_new_from_iter, PySetMethods}, PyFrozenSet, PySet, }, - FromPyObject, IntoPy, PyAny, PyErr, PyObject, PyResult, Python, ToPyObject, + FromPyObject, IntoPy, PyAny, PyErr, PyObject, PyResult, Python, }; +#[allow(deprecated)] impl ToPyObject for collections::HashSet where T: hash::Hash + Eq + ToPyObject, @@ -26,6 +29,7 @@ where } } +#[allow(deprecated)] impl ToPyObject for collections::BTreeSet where T: hash::Hash + Eq + ToPyObject, @@ -174,7 +178,7 @@ where #[cfg(test)] mod tests { use crate::types::{any::PyAnyMethods, PyFrozenSet, PySet}; - use crate::{IntoPy, PyObject, Python, ToPyObject}; + use crate::{IntoPy, IntoPyObject, PyObject, Python}; use std::collections::{BTreeSet, HashSet}; #[test] @@ -218,16 +222,16 @@ mod tests { } #[test] - fn test_set_to_object() { + fn test_set_into_pyobject() { Python::with_gil(|py| { let bt: BTreeSet = [1, 2, 3, 4, 5].iter().cloned().collect(); let hs: HashSet = [1, 2, 3, 4, 5].iter().cloned().collect(); - let bto: PyObject = bt.to_object(py); - let hso: PyObject = hs.to_object(py); + let bto = (&bt).into_pyobject(py).unwrap(); + let hso = (&hs).into_pyobject(py).unwrap(); - assert_eq!(bt, bto.extract(py).unwrap()); - assert_eq!(hs, hso.extract(py).unwrap()); + assert_eq!(bt, bto.extract().unwrap()); + assert_eq!(hs, hso.extract().unwrap()); }); } } diff --git a/src/conversions/std/slice.rs b/src/conversions/std/slice.rs index 241c520e847..eb758271fcf 100644 --- a/src/conversions/std/slice.rs +++ b/src/conversions/std/slice.rs @@ -2,10 +2,12 @@ use std::borrow::Cow; #[cfg(feature = "experimental-inspect")] use crate::inspect::types::TypeInfo; +#[allow(deprecated)] +use crate::ToPyObject; use crate::{ conversion::IntoPyObject, types::{PyByteArray, PyByteArrayMethods, PyBytes}, - Bound, IntoPy, Py, PyAny, PyErr, PyObject, PyResult, Python, ToPyObject, + Bound, IntoPy, Py, PyAny, PyErr, PyObject, PyResult, Python, }; impl IntoPy for &[u8] { @@ -69,6 +71,7 @@ impl<'a> crate::conversion::FromPyObjectBound<'a, '_> for Cow<'a, [u8]> { } } +#[allow(deprecated)] impl ToPyObject for Cow<'_, [u8]> { fn to_object(&self, py: Python<'_>) -> Py { PyBytes::new(py, self.as_ref()).into() @@ -77,7 +80,7 @@ impl ToPyObject for Cow<'_, [u8]> { impl IntoPy> for Cow<'_, [u8]> { fn into_py(self, py: Python<'_>) -> Py { - self.to_object(py) + self.into_pyobject(py).unwrap().into_any().unbind() } } @@ -108,7 +111,7 @@ mod tests { conversion::IntoPyObject, ffi, types::{any::PyAnyMethods, PyBytes, PyBytesMethods, PyList}, - Python, ToPyObject, + Python, }; #[test] @@ -138,11 +141,13 @@ mod tests { .extract::>() .unwrap_err(); - let cow = Cow::<[u8]>::Borrowed(b"foobar").to_object(py); - assert!(cow.bind(py).is_instance_of::()); + let cow = Cow::<[u8]>::Borrowed(b"foobar").into_pyobject(py).unwrap(); + assert!(cow.is_instance_of::()); - let cow = Cow::<[u8]>::Owned(b"foobar".to_vec()).to_object(py); - assert!(cow.bind(py).is_instance_of::()); + let cow = Cow::<[u8]>::Owned(b"foobar".to_vec()) + .into_pyobject(py) + .unwrap(); + assert!(cow.is_instance_of::()); }); } diff --git a/src/conversions/std/string.rs b/src/conversions/std/string.rs index 5c634a621bd..667edaaeb5f 100644 --- a/src/conversions/std/string.rs +++ b/src/conversions/std/string.rs @@ -2,15 +2,18 @@ use std::{borrow::Cow, convert::Infallible}; #[cfg(feature = "experimental-inspect")] use crate::inspect::types::TypeInfo; +#[allow(deprecated)] +use crate::ToPyObject; use crate::{ conversion::IntoPyObject, instance::Bound, types::{any::PyAnyMethods, string::PyStringMethods, PyString}, - FromPyObject, IntoPy, Py, PyAny, PyObject, PyResult, Python, ToPyObject, + FromPyObject, IntoPy, Py, PyAny, PyObject, PyResult, Python, }; /// Converts a Rust `str` to a Python object. /// See `PyString::new` for details on the conversion. +#[allow(deprecated)] impl ToPyObject for str { #[inline] fn to_object(&self, py: Python<'_>) -> PyObject { @@ -66,6 +69,7 @@ impl<'py> IntoPyObject<'py> for &&str { /// Converts a Rust `Cow<'_, str>` to a Python object. /// See `PyString::new` for details on the conversion. +#[allow(deprecated)] impl ToPyObject for Cow<'_, str> { #[inline] fn to_object(&self, py: Python<'_>) -> PyObject { @@ -109,6 +113,7 @@ impl<'py> IntoPyObject<'py> for &Cow<'_, str> { /// Converts a Rust `String` to a Python object. /// See `PyString::new` for details on the conversion. +#[allow(deprecated)] impl ToPyObject for String { #[inline] fn to_object(&self, py: Python<'_>) -> PyObject { @@ -116,6 +121,7 @@ impl ToPyObject for String { } } +#[allow(deprecated)] impl ToPyObject for char { #[inline] fn to_object(&self, py: Python<'_>) -> PyObject { @@ -260,8 +266,7 @@ impl FromPyObject<'_> for char { #[cfg(test)] mod tests { use crate::types::any::PyAnyMethods; - use crate::Python; - use crate::{IntoPy, PyObject, ToPyObject}; + use crate::{IntoPy, IntoPyObject, PyObject, Python}; use std::borrow::Cow; #[test] @@ -276,13 +281,13 @@ mod tests { } #[test] - fn test_cow_to_object() { + fn test_cow_into_pyobject() { Python::with_gil(|py| { let s = "Hello Python"; - let py_string = Cow::Borrowed(s).to_object(py); - assert_eq!(s, py_string.extract::>(py).unwrap()); - let py_string = Cow::::Owned(s.into()).to_object(py); - assert_eq!(s, py_string.extract::>(py).unwrap()); + let py_string = Cow::Borrowed(s).into_pyobject(py).unwrap(); + assert_eq!(s, py_string.extract::>().unwrap()); + let py_string = Cow::::Owned(s.into()).into_pyobject(py).unwrap(); + assert_eq!(s, py_string.extract::>().unwrap()); }) } @@ -290,8 +295,8 @@ mod tests { fn test_non_bmp() { Python::with_gil(|py| { let s = "\u{1F30F}"; - let py_string = s.to_object(py); - assert_eq!(s, py_string.extract::(py).unwrap()); + let py_string = s.into_pyobject(py).unwrap(); + assert_eq!(s, py_string.extract::().unwrap()); }) } @@ -299,9 +304,9 @@ mod tests { fn test_extract_str() { Python::with_gil(|py| { let s = "Hello Python"; - let py_string = s.to_object(py); + let py_string = s.into_pyobject(py).unwrap(); - let s2: Cow<'_, str> = py_string.bind(py).extract().unwrap(); + let s2: Cow<'_, str> = py_string.extract().unwrap(); assert_eq!(s, s2); }) } @@ -310,8 +315,8 @@ mod tests { fn test_extract_char() { Python::with_gil(|py| { let ch = '😃'; - let py_string = ch.to_object(py); - let ch2: char = py_string.bind(py).extract().unwrap(); + let py_string = ch.into_pyobject(py).unwrap(); + let ch2: char = py_string.extract().unwrap(); assert_eq!(ch, ch2); }) } @@ -320,8 +325,8 @@ mod tests { fn test_extract_char_err() { Python::with_gil(|py| { let s = "Hello Python"; - let py_string = s.to_object(py); - let err: crate::PyResult = py_string.bind(py).extract(); + let py_string = s.into_pyobject(py).unwrap(); + let err: crate::PyResult = py_string.extract(); assert!(err .unwrap_err() .to_string() diff --git a/src/conversions/std/time.rs b/src/conversions/std/time.rs index f8345b12e61..c55a22666de 100755 --- a/src/conversions/std/time.rs +++ b/src/conversions/std/time.rs @@ -8,9 +8,9 @@ use crate::types::PyType; use crate::types::{timezone_utc, PyDateTime, PyDelta, PyDeltaAccess}; #[cfg(Py_LIMITED_API)] use crate::Py; -use crate::{ - intern, Bound, FromPyObject, IntoPy, PyAny, PyErr, PyObject, PyResult, Python, ToPyObject, -}; +#[allow(deprecated)] +use crate::ToPyObject; +use crate::{intern, Bound, FromPyObject, IntoPy, PyAny, PyErr, PyObject, PyResult, Python}; use std::time::{Duration, SystemTime, UNIX_EPOCH}; const SECONDS_PER_DAY: u64 = 24 * 60 * 60; @@ -52,6 +52,7 @@ impl FromPyObject<'_> for Duration { } } +#[allow(deprecated)] impl ToPyObject for Duration { #[inline] fn to_object(&self, py: Python<'_>) -> PyObject { @@ -132,6 +133,7 @@ impl FromPyObject<'_> for SystemTime { } } +#[allow(deprecated)] impl ToPyObject for SystemTime { #[inline] fn to_object(&self, py: Python<'_>) -> PyObject { @@ -249,47 +251,59 @@ mod tests { } #[test] - fn test_duration_topyobject() { + fn test_duration_into_pyobject() { Python::with_gil(|py| { - let assert_eq = |l: PyObject, r: Bound<'_, PyAny>| { - assert!(l.bind(py).eq(r).unwrap()); + let assert_eq = |l: Bound<'_, PyAny>, r: Bound<'_, PyAny>| { + assert!(l.eq(r).unwrap()); }; assert_eq( - Duration::new(0, 0).to_object(py), + Duration::new(0, 0).into_pyobject(py).unwrap().into_any(), new_timedelta(py, 0, 0, 0), ); assert_eq( - Duration::new(86400, 0).to_object(py), + Duration::new(86400, 0) + .into_pyobject(py) + .unwrap() + .into_any(), new_timedelta(py, 1, 0, 0), ); assert_eq( - Duration::new(1, 0).to_object(py), + Duration::new(1, 0).into_pyobject(py).unwrap().into_any(), new_timedelta(py, 0, 1, 0), ); assert_eq( - Duration::new(0, 1_000).to_object(py), + Duration::new(0, 1_000) + .into_pyobject(py) + .unwrap() + .into_any(), new_timedelta(py, 0, 0, 1), ); assert_eq( - Duration::new(0, 1).to_object(py), + Duration::new(0, 1).into_pyobject(py).unwrap().into_any(), new_timedelta(py, 0, 0, 0), ); assert_eq( - Duration::new(86401, 1_000).to_object(py), + Duration::new(86401, 1_000) + .into_pyobject(py) + .unwrap() + .into_any(), new_timedelta(py, 1, 1, 1), ); assert_eq( - Duration::new(86399999999999, 999999000).to_object(py), + Duration::new(86399999999999, 999999000) + .into_pyobject(py) + .unwrap() + .into_any(), timedelta_class(py).getattr("max").unwrap(), ); }); } #[test] - fn test_duration_topyobject_overflow() { + fn test_duration_into_pyobject_overflow() { Python::with_gil(|py| { - assert!(panic::catch_unwind(|| Duration::MAX.to_object(py)).is_err()); + assert!(Duration::MAX.into_pyobject(py).is_err()); }) } diff --git a/src/conversions/std/vec.rs b/src/conversions/std/vec.rs index 1548f604e42..ea3fff117c0 100644 --- a/src/conversions/std/vec.rs +++ b/src/conversions/std/vec.rs @@ -2,8 +2,11 @@ use crate::conversion::IntoPyObject; #[cfg(feature = "experimental-inspect")] use crate::inspect::types::TypeInfo; use crate::types::list::new_from_iter; -use crate::{Bound, IntoPy, PyAny, PyErr, PyObject, Python, ToPyObject}; +#[allow(deprecated)] +use crate::ToPyObject; +use crate::{Bound, IntoPy, PyAny, PyErr, PyObject, Python}; +#[allow(deprecated)] impl ToPyObject for [T] where T: ToPyObject, @@ -15,6 +18,7 @@ where } } +#[allow(deprecated)] impl ToPyObject for Vec where T: ToPyObject, diff --git a/src/err/mod.rs b/src/err/mod.rs index ac03c2e573e..59d99f72c06 100644 --- a/src/err/mod.rs +++ b/src/err/mod.rs @@ -3,11 +3,13 @@ use crate::panic::PanicException; use crate::type_object::PyTypeInfo; use crate::types::any::PyAnyMethods; use crate::types::{string::PyStringMethods, typeobject::PyTypeMethods, PyTraceback, PyType}; +#[allow(deprecated)] +use crate::ToPyObject; use crate::{ exceptions::{self, PyBaseException}, ffi, }; -use crate::{Borrowed, IntoPy, Py, PyAny, PyObject, Python, ToPyObject}; +use crate::{Borrowed, BoundObject, IntoPy, Py, PyAny, PyObject, Python}; use std::borrow::Cow; use std::cell::UnsafeCell; use std::ffi::{CStr, CString}; @@ -560,11 +562,11 @@ impl PyErr { /// /// If `exc` is a class object, this also returns `true` when `self` is an instance of a subclass. /// If `exc` is a tuple, all exceptions in the tuple (and recursively in subtuples) are searched for a match. - pub fn matches(&self, py: Python<'_>, exc: T) -> bool + pub fn matches<'py, T>(&self, py: Python<'py>, exc: T) -> Result where - T: ToPyObject, + T: IntoPyObject<'py>, { - self.is_instance(py, exc.to_object(py).bind(py)) + Ok(self.is_instance(py, &exc.into_pyobject(py)?.into_any().as_borrowed())) } /// Returns true if the current exception is instance of `T`. @@ -884,6 +886,7 @@ impl IntoPy for PyErr { } } +#[allow(deprecated)] impl ToPyObject for PyErr { #[inline] fn to_object(&self, py: Python<'_>) -> PyObject { @@ -933,7 +936,11 @@ impl PyErrArguments for PyDowncastErrorArguments { Ok(qn) => qn.to_cow().unwrap_or(FAILED_TO_EXTRACT), Err(_) => FAILED_TO_EXTRACT, }; - format!("'{}' object cannot be converted to '{}'", from, self.to).to_object(py) + format!("'{}' object cannot be converted to '{}'", from, self.to) + .into_pyobject(py) + .unwrap() + .into_any() + .unbind() } } @@ -1187,18 +1194,20 @@ mod tests { fn test_pyerr_matches() { Python::with_gil(|py| { let err = PyErr::new::("foo"); - assert!(err.matches(py, PyValueError::type_object(py))); + assert!(err.matches(py, PyValueError::type_object(py)).unwrap()); - assert!(err.matches( - py, - (PyValueError::type_object(py), PyTypeError::type_object(py)) - )); + assert!(err + .matches( + py, + (PyValueError::type_object(py), PyTypeError::type_object(py)) + ) + .unwrap()); - assert!(!err.matches(py, PyTypeError::type_object(py))); + assert!(!err.matches(py, PyTypeError::type_object(py)).unwrap()); // String is not a valid exception class, so we should get a TypeError let err: PyErr = PyErr::from_type(crate::types::PyString::type_object(py), "foo"); - assert!(err.matches(py, PyTypeError::type_object(py))); + assert!(err.matches(py, PyTypeError::type_object(py)).unwrap()); }) } diff --git a/src/impl_/pyclass.rs b/src/impl_/pyclass.rs index bf94503f1c0..4e3ed140a76 100644 --- a/src/impl_/pyclass.rs +++ b/src/impl_/pyclass.rs @@ -1,3 +1,5 @@ +#[allow(deprecated)] +use crate::ToPyObject; use crate::{ conversion::IntoPyObject, exceptions::{PyAttributeError, PyNotImplementedError, PyRuntimeError, PyValueError}, @@ -11,7 +13,6 @@ use crate::{ pyclass_init::PyObjectInit, types::{any::PyAnyMethods, PyBool}, Borrowed, BoundObject, IntoPy, Py, PyAny, PyClass, PyErr, PyRef, PyResult, PyTypeInfo, Python, - ToPyObject, }; use std::{ borrow::Cow, @@ -1292,6 +1293,7 @@ impl< /// Fallback case; Field is not `Py`; try to use `ToPyObject` to avoid potentially expensive /// clones of containers like `Vec` +#[allow(deprecated)] impl< ClassT: PyClass, FieldT: ToPyObject, @@ -1443,6 +1445,7 @@ impl IsPyT> { probe!(IsToPyObject); +#[allow(deprecated)] impl IsToPyObject { pub const VALUE: bool = true; } @@ -1492,6 +1495,7 @@ where unsafe { obj.cast::().add(Offset::offset()).cast::() } } +#[allow(deprecated)] fn pyo3_get_value_topyobject< ClassT: PyClass, FieldT: ToPyObject, diff --git a/src/instance.rs b/src/instance.rs index d476c7cef31..a86bed94513 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -6,9 +6,11 @@ use crate::pycell::{PyBorrowError, PyBorrowMutError}; use crate::pyclass::boolean_struct::{False, True}; use crate::types::{any::PyAnyMethods, string::PyStringMethods, typeobject::PyTypeMethods}; use crate::types::{DerefToPyAny, PyDict, PyString, PyTuple}; +#[allow(deprecated)] +use crate::ToPyObject; use crate::{ ffi, AsPyPointer, DowncastError, FromPyObject, IntoPy, PyAny, PyClass, PyClassInitializer, - PyRef, PyRefMut, PyTypeInfo, Python, ToPyObject, + PyRef, PyRefMut, PyTypeInfo, Python, }; use crate::{gil, PyTypeCheck}; use std::marker::PhantomData; @@ -819,6 +821,7 @@ impl Clone for Borrowed<'_, '_, T> { impl Copy for Borrowed<'_, '_, T> {} +#[allow(deprecated)] impl ToPyObject for Borrowed<'_, '_, T> { /// Converts `Py` instance -> PyObject. #[inline] @@ -1708,6 +1711,7 @@ impl Py { } } +#[allow(deprecated)] impl ToPyObject for Py { /// Converts `Py` instance -> PyObject. #[inline] @@ -1728,10 +1732,11 @@ impl IntoPy for Py { impl IntoPy for &'_ Py { #[inline] fn into_py(self, py: Python<'_>) -> PyObject { - self.to_object(py) + self.into_pyobject(py).unwrap().into_any().unbind() } } +#[allow(deprecated)] impl ToPyObject for Bound<'_, T> { /// Converts `&Bound` instance -> PyObject, increasing the reference count. #[inline] @@ -1752,7 +1757,7 @@ impl IntoPy for &Bound<'_, T> { /// Converts `&Bound` instance -> PyObject, increasing the reference count. #[inline] fn into_py(self, py: Python<'_>) -> PyObject { - self.to_object(py) + self.into_pyobject(py).unwrap().into_any().unbind() } } @@ -1960,31 +1965,30 @@ impl PyObject { #[cfg(test)] mod tests { - use super::{Bound, Py, PyObject}; + use super::{Bound, IntoPyObject, Py, PyObject}; use crate::tests::common::generate_unique_module_name; use crate::types::{dict::IntoPyDict, PyAnyMethods, PyCapsule, PyDict, PyString}; - use crate::{ffi, Borrowed, PyAny, PyResult, Python, ToPyObject}; + use crate::{ffi, Borrowed, PyAny, PyResult, Python}; use pyo3_ffi::c_str; use std::ffi::CStr; #[test] fn test_call() { Python::with_gil(|py| { - let obj = py.get_type::().to_object(py); + let obj = py.get_type::().into_pyobject(py).unwrap(); - let assert_repr = |obj: &Bound<'_, PyAny>, expected: &str| { + let assert_repr = |obj: Bound<'_, PyAny>, expected: &str| { assert_eq!(obj.repr().unwrap(), expected); }; - assert_repr(obj.call0(py).unwrap().bind(py), "{}"); - assert_repr(obj.call1(py, ()).unwrap().bind(py), "{}"); - assert_repr(obj.call(py, (), None).unwrap().bind(py), "{}"); + assert_repr(obj.call0().unwrap(), "{}"); + assert_repr(obj.call1(()).unwrap(), "{}"); + assert_repr(obj.call((), None).unwrap(), "{}"); - assert_repr(obj.call1(py, ((('x', 1),),)).unwrap().bind(py), "{'x': 1}"); + assert_repr(obj.call1(((('x', 1),),)).unwrap(), "{'x': 1}"); assert_repr( - obj.call(py, (), Some(&[('x', 1)].into_py_dict(py).unwrap())) - .unwrap() - .bind(py), + obj.call((), Some(&[('x', 1)].into_py_dict(py).unwrap())) + .unwrap(), "{'x': 1}", ); }) @@ -2117,7 +2121,7 @@ a = A() #[test] fn test_debug_fmt() { Python::with_gil(|py| { - let obj = "hello world".to_object(py).into_bound(py); + let obj = "hello world".into_pyobject(py).unwrap(); assert_eq!(format!("{:?}", obj), "'hello world'"); }); } @@ -2125,7 +2129,7 @@ a = A() #[test] fn test_display_fmt() { Python::with_gil(|py| { - let obj = "hello world".to_object(py).into_bound(py); + let obj = "hello world".into_pyobject(py).unwrap(); assert_eq!(format!("{}", obj), "hello world"); }); } diff --git a/src/lib.rs b/src/lib.rs index 8144ef740e0..2cb189fb489 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -320,7 +320,9 @@ #![doc = concat!("[Features chapter of the guide]: https://pyo3.rs/v", env!("CARGO_PKG_VERSION"), "/features.html#features-reference \"Features Reference - PyO3 user guide\"")] //! [`Ungil`]: crate::marker::Ungil pub use crate::class::*; -pub use crate::conversion::{AsPyPointer, FromPyObject, IntoPy, ToPyObject}; +#[allow(deprecated)] +pub use crate::conversion::ToPyObject; +pub use crate::conversion::{AsPyPointer, FromPyObject, IntoPy, IntoPyObject}; pub use crate::err::{DowncastError, DowncastIntoError, PyErr, PyErrArguments, PyResult, ToPyErr}; #[cfg(not(any(PyPy, GraalPy)))] pub use crate::gil::{prepare_freethreaded_python, with_embedded_python_interpreter}; diff --git a/src/macros.rs b/src/macros.rs index 936dbd43af0..d2fa6f31ada 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -104,8 +104,9 @@ macro_rules! py_run { macro_rules! py_run_impl { ($py:expr, $($val:ident)+, $code:expr) => {{ use $crate::types::IntoPyDict; - use $crate::ToPyObject; - let d = [$((stringify!($val), $val.to_object($py)),)+].into_py_dict($py).unwrap(); + use $crate::conversion::IntoPyObject; + use $crate::BoundObject; + let d = [$((stringify!($val), (&$val).into_pyobject($py).unwrap().into_any().into_bound()),)+].into_py_dict($py).unwrap(); $crate::py_run_impl!($py, *d, $code) }}; ($py:expr, *$dict:expr, $code:expr) => {{ diff --git a/src/prelude.rs b/src/prelude.rs index b2b86c8449d..97f3e35afa1 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -8,7 +8,9 @@ //! use pyo3::prelude::*; //! ``` -pub use crate::conversion::{FromPyObject, IntoPy, IntoPyObject, ToPyObject}; +#[allow(deprecated)] +pub use crate::conversion::ToPyObject; +pub use crate::conversion::{FromPyObject, IntoPy, IntoPyObject}; pub use crate::err::{PyErr, PyResult}; pub use crate::instance::{Borrowed, Bound, Py, PyObject}; pub use crate::marker::Python; diff --git a/src/pybacked.rs b/src/pybacked.rs index c27383ba9bc..5a45c8f1036 100644 --- a/src/pybacked.rs +++ b/src/pybacked.rs @@ -5,12 +5,11 @@ use std::{convert::Infallible, ops::Deref, ptr::NonNull, sync::Arc}; #[allow(deprecated)] use crate::ToPyObject; use crate::{ - prelude::IntoPyObject, types::{ any::PyAnyMethods, bytearray::PyByteArrayMethods, bytes::PyBytesMethods, string::PyStringMethods, PyByteArray, PyBytes, PyString, }, - Bound, DowncastError, FromPyObject, IntoPy, Py, PyAny, PyErr, PyResult, Python, + Bound, DowncastError, FromPyObject, IntoPy, IntoPyObject, Py, PyAny, PyErr, PyResult, Python, }; /// A wrapper around `str` where the storage is owned by a Python `bytes` or `str` object. @@ -360,8 +359,7 @@ use impl_traits; #[cfg(test)] mod test { use super::*; - use crate::prelude::IntoPyObject; - use crate::Python; + use crate::{IntoPyObject, Python}; use std::collections::hash_map::DefaultHasher; use std::hash::{Hash, Hasher}; diff --git a/src/tests/common.rs b/src/tests/common.rs index caa4120b720..cd0374e9019 100644 --- a/src/tests/common.rs +++ b/src/tests/common.rs @@ -40,14 +40,15 @@ mod inner { // Case1: idents & no err_msg ($py:expr, $($val:ident)+, $code:expr, $err:ident) => {{ use pyo3::types::IntoPyDict; - let d = [$((stringify!($val), $val.to_object($py)),)+].into_py_dict($py).unwrap(); + use pyo3::BoundObject; + let d = [$((stringify!($val), (&$val).into_pyobject($py).unwrap().into_any().into_bound()),)+].into_py_dict($py).unwrap(); py_expect_exception!($py, *d, $code, $err) }}; // Case2: dict & no err_msg ($py:expr, *$dict:expr, $code:expr, $err:ident) => {{ let res = $py.run(&std::ffi::CString::new($code).unwrap(), None, Some(&$dict.as_borrowed())); let err = res.expect_err(&format!("Did not raise {}", stringify!($err))); - if !err.matches($py, $py.get_type::()) { + if !err.matches($py, $py.get_type::()).unwrap() { panic!("Expected {} but got {:?}", stringify!($err), err) } err diff --git a/src/types/any.rs b/src/types/any.rs index 76c5bcf18dd..47cebb0363b 100644 --- a/src/types/any.rs +++ b/src/types/any.rs @@ -1639,9 +1639,10 @@ mod tests { ffi, tests::common::generate_unique_module_name, types::{IntoPyDict, PyAny, PyAnyMethods, PyBool, PyInt, PyList, PyModule, PyTypeMethods}, - Bound, PyTypeInfo, Python, ToPyObject, + Bound, BoundObject, IntoPyObject, PyTypeInfo, Python, }; use pyo3_ffi::c_str; + use std::fmt::Debug; #[test] fn test_lookup_special() { @@ -1731,10 +1732,10 @@ class NonHeapNonDescriptorInt: #[test] fn test_call_with_kwargs() { Python::with_gil(|py| { - let list = vec![3, 6, 5, 4, 7].to_object(py); + let list = vec![3, 6, 5, 4, 7].into_pyobject(py).unwrap(); let dict = vec![("reverse", true)].into_py_dict(py).unwrap(); - list.call_method(py, "sort", (), Some(&dict)).unwrap(); - assert_eq!(list.extract::>(py).unwrap(), vec![7, 6, 5, 4, 3]); + list.call_method("sort", (), Some(&dict)).unwrap(); + assert_eq!(list.extract::>().unwrap(), vec![7, 6, 5, 4, 3]); }); } @@ -1797,7 +1798,7 @@ class SimpleClass: #[test] fn test_hasattr() { Python::with_gil(|py| { - let x = 5.to_object(py).into_bound(py); + let x = 5i32.into_pyobject(py).unwrap(); assert!(x.is_instance_of::()); assert!(x.hasattr("to_bytes").unwrap()); @@ -1844,10 +1845,10 @@ class SimpleClass: #[test] fn test_any_is_instance_of() { Python::with_gil(|py| { - let x = 5.to_object(py).into_bound(py); + let x = 5i32.into_pyobject(py).unwrap(); assert!(x.is_instance_of::()); - let l = vec![&x, &x].to_object(py).into_bound(py); + let l = vec![&x, &x].into_pyobject(py).unwrap(); assert!(l.is_instance_of::()); }); } @@ -1855,7 +1856,7 @@ class SimpleClass: #[test] fn test_any_is_instance() { Python::with_gil(|py| { - let l = vec![1u8, 2].to_object(py).into_bound(py); + let l = vec![1i8, 2].into_pyobject(py).unwrap(); assert!(l.is_instance(&py.get_type::()).unwrap()); }); } @@ -1863,7 +1864,7 @@ class SimpleClass: #[test] fn test_any_is_exact_instance_of() { Python::with_gil(|py| { - let x = 5.to_object(py).into_bound(py); + let x = 5i32.into_pyobject(py).unwrap(); assert!(x.is_exact_instance_of::()); let t = PyBool::new(py, true); @@ -1871,7 +1872,7 @@ class SimpleClass: assert!(!t.is_exact_instance_of::()); assert!(t.is_exact_instance_of::()); - let l = vec![&x, &x].to_object(py).into_bound(py); + let l = vec![&x, &x].into_pyobject(py).unwrap(); assert!(l.is_exact_instance_of::()); }); } @@ -1890,34 +1891,36 @@ class SimpleClass: fn test_any_contains() { Python::with_gil(|py| { let v: Vec = vec![1, 1, 2, 3, 5, 8]; - let ob = v.to_object(py).into_bound(py); + let ob = v.into_pyobject(py).unwrap(); - let bad_needle = 7i32.to_object(py); + let bad_needle = 7i32.into_pyobject(py).unwrap(); assert!(!ob.contains(&bad_needle).unwrap()); - let good_needle = 8i32.to_object(py); + let good_needle = 8i32.into_pyobject(py).unwrap(); assert!(ob.contains(&good_needle).unwrap()); - let type_coerced_needle = 8f32.to_object(py); + let type_coerced_needle = 8f32.into_pyobject(py).unwrap(); assert!(ob.contains(&type_coerced_needle).unwrap()); let n: u32 = 42; - let bad_haystack = n.to_object(py).into_bound(py); - let irrelevant_needle = 0i32.to_object(py); + let bad_haystack = n.into_pyobject(py).unwrap(); + let irrelevant_needle = 0i32.into_pyobject(py).unwrap(); assert!(bad_haystack.contains(&irrelevant_needle).is_err()); }); } // This is intentionally not a test, it's a generic function used by the tests below. - fn test_eq_methods_generic(list: &[T]) + fn test_eq_methods_generic<'a, T>(list: &'a [T]) where - T: PartialEq + PartialOrd + ToPyObject, + T: PartialEq + PartialOrd, + for<'py> &'a T: IntoPyObject<'py>, + for<'py> <&'a T as IntoPyObject<'py>>::Error: Debug, { Python::with_gil(|py| { for a in list { for b in list { - let a_py = a.to_object(py).into_bound(py); - let b_py = b.to_object(py).into_bound(py); + let a_py = a.into_pyobject(py).unwrap().into_any().into_bound(); + let b_py = b.into_pyobject(py).unwrap().into_any().into_bound(); assert_eq!( a.lt(b), @@ -1975,13 +1978,13 @@ class SimpleClass: #[test] fn test_eq_methods_integers() { let ints = [-4, -4, 1, 2, 0, -100, 1_000_000]; - test_eq_methods_generic(&ints); + test_eq_methods_generic::(&ints); } #[test] fn test_eq_methods_strings() { let strings = ["Let's", "test", "some", "eq", "methods"]; - test_eq_methods_generic(&strings); + test_eq_methods_generic::<&str>(&strings); } #[test] @@ -1996,20 +1999,20 @@ class SimpleClass: 10.0 / 3.0, -1_000_000.0, ]; - test_eq_methods_generic(&floats); + test_eq_methods_generic::(&floats); } #[test] fn test_eq_methods_bools() { let bools = [true, false]; - test_eq_methods_generic(&bools); + test_eq_methods_generic::(&bools); } #[test] fn test_rich_compare_type_error() { Python::with_gil(|py| { - let py_int = 1.to_object(py).into_bound(py); - let py_str = "1".to_object(py).into_bound(py); + let py_int = 1i32.into_pyobject(py).unwrap(); + let py_str = "1".into_pyobject(py).unwrap(); assert!(py_int.rich_compare(&py_str, CompareOp::Lt).is_err()); assert!(!py_int @@ -2031,7 +2034,7 @@ class SimpleClass: assert!(v.is_ellipsis()); - let not_ellipsis = 5.to_object(py).into_bound(py); + let not_ellipsis = 5i32.into_pyobject(py).unwrap(); assert!(!not_ellipsis.is_ellipsis()); }); } @@ -2041,7 +2044,7 @@ class SimpleClass: Python::with_gil(|py| { assert!(PyList::type_object(py).is_callable()); - let not_callable = 5.to_object(py).into_bound(py); + let not_callable = 5i32.into_pyobject(py).unwrap(); assert!(!not_callable.is_callable()); }); } @@ -2055,7 +2058,7 @@ class SimpleClass: let list = PyList::new(py, vec![1, 2, 3]).unwrap().into_any(); assert!(!list.is_empty().unwrap()); - let not_container = 5.to_object(py).into_bound(py); + let not_container = 5i32.into_pyobject(py).unwrap(); assert!(not_container.is_empty().is_err()); }); } diff --git a/src/types/boolobject.rs b/src/types/boolobject.rs index 8f211a27a54..6f90329900d 100644 --- a/src/types/boolobject.rs +++ b/src/types/boolobject.rs @@ -1,9 +1,11 @@ #[cfg(feature = "experimental-inspect")] use crate::inspect::types::TypeInfo; +#[allow(deprecated)] +use crate::ToPyObject; use crate::{ exceptions::PyTypeError, ffi, ffi_ptr_ext::FfiPtrExt, instance::Bound, types::typeobject::PyTypeMethods, Borrowed, FromPyObject, IntoPy, PyAny, PyObject, PyResult, - Python, ToPyObject, + Python, }; use super::any::PyAnyMethods; @@ -145,6 +147,7 @@ impl PartialEq> for &'_ bool { } /// Converts a Rust `bool` to a Python `bool`. +#[allow(deprecated)] impl ToPyObject for bool { #[inline] fn to_object(&self, py: Python<'_>) -> PyObject { @@ -254,8 +257,8 @@ mod tests { use crate::types::any::PyAnyMethods; use crate::types::boolobject::PyBoolMethods; use crate::types::PyBool; + use crate::IntoPyObject; use crate::Python; - use crate::ToPyObject; #[test] fn test_true() { @@ -263,7 +266,7 @@ mod tests { assert!(PyBool::new(py, true).is_true()); let t = PyBool::new(py, true); assert!(t.extract::().unwrap()); - assert!(true.to_object(py).is(&*PyBool::new(py, true))); + assert!(true.into_pyobject(py).unwrap().is(&*PyBool::new(py, true))); }); } @@ -273,7 +276,10 @@ mod tests { assert!(!PyBool::new(py, false).is_true()); let t = PyBool::new(py, false); assert!(!t.extract::().unwrap()); - assert!(false.to_object(py).is(&*PyBool::new(py, false))); + assert!(false + .into_pyobject(py) + .unwrap() + .is(&*PyBool::new(py, false))); }); } diff --git a/src/types/dict.rs b/src/types/dict.rs index ff7595afceb..9b7d8697d20 100644 --- a/src/types/dict.rs +++ b/src/types/dict.rs @@ -6,7 +6,7 @@ use crate::instance::{Borrowed, Bound}; use crate::py_result_ext::PyResultExt; use crate::types::any::PyAnyMethods; use crate::types::{PyAny, PyList}; -use crate::{ffi, BoundObject, Python}; +use crate::{ffi, BoundObject, IntoPyObject, Python}; /// Represents a Python `dict`. /// @@ -558,7 +558,6 @@ mod borrowed_iter { } } -use crate::prelude::IntoPyObject; pub(crate) use borrowed_iter::BorrowedDictIter; /// Conversion trait that allows a sequence of tuples to be converted into `PyDict` @@ -628,7 +627,6 @@ where mod tests { use super::*; use crate::types::PyTuple; - use crate::ToPyObject; use std::collections::{BTreeMap, HashMap}; #[test] @@ -713,13 +711,11 @@ mod tests { #[test] fn test_len() { Python::with_gil(|py| { - let mut v = HashMap::new(); - let ob = v.to_object(py); - let dict = ob.downcast_bound::(py).unwrap(); + let mut v = HashMap::::new(); + let dict = (&v).into_pyobject(py).unwrap(); assert_eq!(0, dict.len()); v.insert(7, 32); - let ob = v.to_object(py); - let dict2 = ob.downcast_bound::(py).unwrap(); + let dict2 = v.into_pyobject(py).unwrap(); assert_eq!(1, dict2.len()); }); } @@ -729,8 +725,7 @@ mod tests { Python::with_gil(|py| { let mut v = HashMap::new(); v.insert(7, 32); - let ob = v.to_object(py); - let dict = ob.downcast_bound::(py).unwrap(); + let dict = v.into_pyobject(py).unwrap(); assert!(dict.contains(7i32).unwrap()); assert!(!dict.contains(8i32).unwrap()); }); @@ -741,8 +736,7 @@ mod tests { Python::with_gil(|py| { let mut v = HashMap::new(); v.insert(7, 32); - let ob = v.to_object(py); - let dict = ob.downcast_bound::(py).unwrap(); + let dict = v.into_pyobject(py).unwrap(); assert_eq!( 32, dict.get_item(7i32) @@ -796,8 +790,7 @@ mod tests { Python::with_gil(|py| { let mut v = HashMap::new(); v.insert(7, 32); - let ob = v.to_object(py); - let dict = ob.downcast_bound::(py).unwrap(); + let dict = v.into_pyobject(py).unwrap(); assert!(dict.set_item(7i32, 42i32).is_ok()); // change assert!(dict.set_item(8i32, 123i32).is_ok()); // insert assert_eq!( @@ -839,8 +832,7 @@ mod tests { Python::with_gil(|py| { let mut v = HashMap::new(); v.insert(7, 32); - let ob = v.to_object(py); - let dict = ob.downcast_bound::(py).unwrap(); + let dict = (&v).into_pyobject(py).unwrap(); assert!(dict.set_item(7i32, 42i32).is_ok()); // change assert!(dict.set_item(8i32, 123i32).is_ok()); // insert assert_eq!(32i32, v[&7i32]); // not updated! @@ -853,8 +845,7 @@ mod tests { Python::with_gil(|py| { let mut v = HashMap::new(); v.insert(7, 32); - let ob = v.to_object(py); - let dict = ob.downcast_bound::(py).unwrap(); + let dict = v.into_pyobject(py).unwrap(); assert!(dict.del_item(7i32).is_ok()); assert_eq!(0, dict.len()); assert!(dict.get_item(7i32).unwrap().is_none()); @@ -866,8 +857,7 @@ mod tests { Python::with_gil(|py| { let mut v = HashMap::new(); v.insert(7, 32); - let ob = v.to_object(py); - let dict = ob.downcast_bound::(py).unwrap(); + let dict = (&v).into_pyobject(py).unwrap(); assert!(dict.del_item(7i32).is_ok()); // change assert_eq!(32i32, *v.get(&7i32).unwrap()); // not updated! }); @@ -880,8 +870,7 @@ mod tests { v.insert(7, 32); v.insert(8, 42); v.insert(9, 123); - let ob = v.to_object(py); - let dict = ob.downcast_bound::(py).unwrap(); + let dict = v.into_pyobject(py).unwrap(); // Can't just compare against a vector of tuples since we don't have a guaranteed ordering. let mut key_sum = 0; let mut value_sum = 0; @@ -902,8 +891,7 @@ mod tests { v.insert(7, 32); v.insert(8, 42); v.insert(9, 123); - let ob = v.to_object(py); - let dict = ob.downcast_bound::(py).unwrap(); + let dict = v.into_pyobject(py).unwrap(); // Can't just compare against a vector of tuples since we don't have a guaranteed ordering. let mut key_sum = 0; for el in dict.keys() { @@ -920,8 +908,7 @@ mod tests { v.insert(7, 32); v.insert(8, 42); v.insert(9, 123); - let ob = v.to_object(py); - let dict = ob.downcast_bound::(py).unwrap(); + let dict = v.into_pyobject(py).unwrap(); // Can't just compare against a vector of tuples since we don't have a guaranteed ordering. let mut values_sum = 0; for el in dict.values() { @@ -938,8 +925,7 @@ mod tests { v.insert(7, 32); v.insert(8, 42); v.insert(9, 123); - let ob = v.to_object(py); - let dict = ob.downcast_bound::(py).unwrap(); + let dict = v.into_pyobject(py).unwrap(); let mut key_sum = 0; let mut value_sum = 0; for (key, value) in dict { @@ -958,8 +944,7 @@ mod tests { v.insert(7, 32); v.insert(8, 42); v.insert(9, 123); - let ob = v.to_object(py); - let dict: &Bound<'_, PyDict> = ob.downcast_bound(py).unwrap(); + let dict = v.into_pyobject(py).unwrap(); let mut key_sum = 0; let mut value_sum = 0; for (key, value) in dict { @@ -979,10 +964,9 @@ mod tests { v.insert(8, 42); v.insert(9, 123); - let ob = v.to_object(py); - let dict = ob.downcast_bound::(py).unwrap(); + let dict = (&v).into_pyobject(py).unwrap(); - for (key, value) in dict { + for (key, value) in &dict { dict.set_item(key, value.extract::().unwrap() + 7) .unwrap(); } @@ -997,8 +981,7 @@ mod tests { for i in 0..10 { v.insert(i * 2, i * 2); } - let ob = v.to_object(py); - let dict = ob.downcast_bound::(py).unwrap(); + let dict = v.into_pyobject(py).unwrap(); for (i, (key, value)) in dict.iter().enumerate() { let key = key.extract::().unwrap(); @@ -1022,8 +1005,7 @@ mod tests { for i in 0..10 { v.insert(i * 2, i * 2); } - let ob = v.to_object(py); - let dict = ob.downcast_bound::(py).unwrap(); + let dict = v.into_pyobject(py).unwrap(); for (i, (key, value)) in dict.iter().enumerate() { let key = key.extract::().unwrap(); @@ -1046,8 +1028,7 @@ mod tests { v.insert(7, 32); v.insert(8, 42); v.insert(9, 123); - let ob = v.to_object(py); - let dict = ob.downcast_bound::(py).unwrap(); + let dict = (&v).into_pyobject(py).unwrap(); let mut iter = dict.iter(); assert_eq!(iter.size_hint(), (v.len(), Some(v.len()))); @@ -1072,8 +1053,7 @@ mod tests { v.insert(7, 32); v.insert(8, 42); v.insert(9, 123); - let ob = v.to_object(py); - let dict = ob.downcast_bound::(py).unwrap(); + let dict = v.into_pyobject(py).unwrap(); let mut key_sum = 0; let mut value_sum = 0; for (key, value) in dict { diff --git a/src/types/float.rs b/src/types/float.rs index 58fdba609af..f9bf673ac98 100644 --- a/src/types/float.rs +++ b/src/types/float.rs @@ -2,9 +2,11 @@ use super::any::PyAnyMethods; use crate::conversion::IntoPyObject; #[cfg(feature = "experimental-inspect")] use crate::inspect::types::TypeInfo; +#[allow(deprecated)] +use crate::ToPyObject; use crate::{ ffi, ffi_ptr_ext::FfiPtrExt, instance::Bound, Borrowed, FromPyObject, IntoPy, PyAny, PyErr, - PyObject, PyResult, Python, ToPyObject, + PyObject, PyResult, Python, }; use std::convert::Infallible; use std::os::raw::c_double; @@ -76,6 +78,7 @@ impl<'py> PyFloatMethods<'py> for Bound<'py, PyFloat> { } } +#[allow(deprecated)] impl ToPyObject for f64 { #[inline] fn to_object(&self, py: Python<'_>) -> PyObject { @@ -147,6 +150,7 @@ impl<'py> FromPyObject<'py> for f64 { } } +#[allow(deprecated)] impl ToPyObject for f32 { #[inline] fn to_object(&self, py: Python<'_>) -> PyObject { @@ -279,8 +283,9 @@ impl_partial_eq_for_float!(f32); #[cfg(test)] mod tests { use crate::{ - types::{PyFloat, PyFloatMethods}, - Python, ToPyObject, + conversion::IntoPyObject, + types::{PyAnyMethods, PyFloat, PyFloatMethods}, + Python, }; macro_rules! num_to_py_object_and_back ( @@ -292,8 +297,8 @@ mod tests { Python::with_gil(|py| { let val = 123 as $t1; - let obj = val.to_object(py); - assert_approx_eq!(obj.extract::<$t2>(py).unwrap(), val as $t2); + let obj = val.into_pyobject(py).unwrap(); + assert_approx_eq!(obj.extract::<$t2>().unwrap(), val as $t2); }); } ) diff --git a/src/types/frozenset.rs b/src/types/frozenset.rs index 524baaa2f08..3c5a62a01d8 100644 --- a/src/types/frozenset.rs +++ b/src/types/frozenset.rs @@ -6,7 +6,7 @@ use crate::{ ffi_ptr_ext::FfiPtrExt, py_result_ext::PyResultExt, types::any::PyAnyMethods, - Bound, PyAny, Python, ToPyObject, + Bound, PyAny, Python, }; use crate::{Borrowed, BoundObject}; use std::ptr; @@ -103,8 +103,9 @@ impl PyFrozenSet { /// Deprecated name for [`PyFrozenSet::new`]. #[deprecated(since = "0.23.0", note = "renamed to `PyFrozenSet::new`")] + #[allow(deprecated)] #[inline] - pub fn new_bound<'a, 'p, T: ToPyObject + 'a>( + pub fn new_bound<'a, 'p, T: crate::ToPyObject + 'a>( py: Python<'p>, elements: impl IntoIterator, ) -> PyResult> { diff --git a/src/types/iterator.rs b/src/types/iterator.rs index b86590e5de8..068ab1fce34 100644 --- a/src/types/iterator.rs +++ b/src/types/iterator.rs @@ -108,13 +108,12 @@ mod tests { use super::PyIterator; use crate::exceptions::PyTypeError; use crate::types::{PyAnyMethods, PyDict, PyList, PyListMethods}; - use crate::{ffi, Python, ToPyObject}; + use crate::{ffi, IntoPyObject, Python}; #[test] fn vec_iter() { Python::with_gil(|py| { - let obj = vec![10, 20].to_object(py); - let inst = obj.bind(py); + let inst = vec![10, 20].into_pyobject(py).unwrap(); let mut it = inst.try_iter().unwrap(); assert_eq!( 10_i32, @@ -131,9 +130,9 @@ mod tests { #[test] fn iter_refcnt() { let (obj, count) = Python::with_gil(|py| { - let obj = vec![10, 20].to_object(py); - let count = obj.get_refcnt(py); - (obj, count) + let obj = vec![10, 20].into_pyobject(py).unwrap(); + let count = obj.get_refcnt(); + (obj.unbind(), count) }); Python::with_gil(|py| { @@ -161,18 +160,14 @@ mod tests { list.append(10).unwrap(); list.append(&obj).unwrap(); count = obj.get_refcnt(); - list.to_object(py) + list }; { - let inst = list.bind(py); - let mut it = inst.try_iter().unwrap(); - - assert_eq!( - 10_i32, - it.next().unwrap().unwrap().extract::<'_, i32>().unwrap() - ); - assert!(it.next().unwrap().unwrap().is(&obj)); + let mut it = list.iter(); + + assert_eq!(10_i32, it.next().unwrap().extract::<'_, i32>().unwrap()); + assert!(it.next().unwrap().is(&obj)); assert!(it.next().is_none()); } assert_eq!(count, obj.get_refcnt()); @@ -243,8 +238,8 @@ def fibonacci(target): #[test] fn int_not_iterable() { Python::with_gil(|py| { - let x = 5.to_object(py); - let err = PyIterator::from_object(x.bind(py)).unwrap_err(); + let x = 5i32.into_pyobject(py).unwrap(); + let err = PyIterator::from_object(&x).unwrap_err(); assert!(err.is_instance_of::(py)); }); diff --git a/src/types/list.rs b/src/types/list.rs index a8952273341..ae5eda63a11 100644 --- a/src/types/list.rs +++ b/src/types/list.rs @@ -5,9 +5,8 @@ use crate::ffi::{self, Py_ssize_t}; use crate::ffi_ptr_ext::FfiPtrExt; use crate::internal_tricks::get_ssize_index; use crate::types::{PySequence, PyTuple}; -use crate::{Borrowed, Bound, BoundObject, PyAny, PyObject, Python, ToPyObject}; +use crate::{Borrowed, Bound, BoundObject, IntoPyObject, PyAny, PyObject, Python}; -use crate::prelude::IntoPyObject; use crate::types::any::PyAnyMethods; use crate::types::sequence::PySequenceMethods; @@ -116,6 +115,7 @@ impl PyList { /// Deprecated name for [`PyList::new`]. #[deprecated(since = "0.23.0", note = "renamed to `PyList::new`")] + #[allow(deprecated)] #[inline] #[track_caller] pub fn new_bound( @@ -123,7 +123,7 @@ impl PyList { elements: impl IntoIterator, ) -> Bound<'_, PyList> where - T: ToPyObject, + T: crate::ToPyObject, U: ExactSizeIterator, { Self::new(py, elements.into_iter().map(|e| e.to_object(py))).unwrap() @@ -574,7 +574,7 @@ mod tests { use crate::types::list::PyListMethods; use crate::types::sequence::PySequenceMethods; use crate::types::{PyList, PyTuple}; - use crate::{ffi, Python}; + use crate::{ffi, IntoPyObject, Python}; #[test] fn test_new() { @@ -978,7 +978,6 @@ mod tests { }); } - use crate::prelude::IntoPyObject; use std::ops::Range; // An iterator that lies about its `ExactSizeIterator` implementation. diff --git a/src/types/none.rs b/src/types/none.rs index 47a1ac25848..9a0c9b11f45 100644 --- a/src/types/none.rs +++ b/src/types/none.rs @@ -1,7 +1,8 @@ use crate::ffi_ptr_ext::FfiPtrExt; +#[allow(deprecated)] +use crate::ToPyObject; use crate::{ ffi, types::any::PyAnyMethods, Borrowed, Bound, IntoPy, PyAny, PyObject, PyTypeInfo, Python, - ToPyObject, }; /// Represents the Python `None` object. @@ -50,6 +51,7 @@ unsafe impl PyTypeInfo for PyNone { } /// `()` is converted to Python `None`. +#[allow(deprecated)] impl ToPyObject for () { fn to_object(&self, py: Python<'_>) -> PyObject { PyNone::get(py).into_py(py) @@ -67,7 +69,7 @@ impl IntoPy for () { mod tests { use crate::types::any::PyAnyMethods; use crate::types::{PyDict, PyNone}; - use crate::{IntoPy, PyObject, PyTypeInfo, Python, ToPyObject}; + use crate::{IntoPy, PyObject, PyTypeInfo, Python}; #[test] fn test_none_is_itself() { Python::with_gil(|py| { @@ -91,7 +93,9 @@ mod tests { } #[test] + #[allow(deprecated)] fn test_unit_to_object_is_none() { + use crate::ToPyObject; Python::with_gil(|py| { assert!(().to_object(py).downcast_bound::(py).is_ok()); }) diff --git a/src/types/sequence.rs b/src/types/sequence.rs index d427edbae5a..0801704f700 100644 --- a/src/types/sequence.rs +++ b/src/types/sequence.rs @@ -5,12 +5,11 @@ use crate::ffi_ptr_ext::FfiPtrExt; use crate::inspect::types::TypeInfo; use crate::instance::Bound; use crate::internal_tricks::get_ssize_index; -use crate::prelude::IntoPyObject; use crate::py_result_ext::PyResultExt; use crate::sync::GILOnceCell; use crate::type_object::PyTypeInfo; use crate::types::{any::PyAnyMethods, PyAny, PyList, PyString, PyTuple, PyType}; -use crate::{ffi, Borrowed, BoundObject, FromPyObject, Py, PyTypeCheck, Python}; +use crate::{ffi, Borrowed, BoundObject, FromPyObject, IntoPyObject, Py, PyTypeCheck, Python}; /// Represents a reference to a Python object supporting the sequence protocol. /// @@ -408,9 +407,8 @@ impl PyTypeCheck for PySequence { #[cfg(test)] mod tests { - use crate::prelude::IntoPyObject; use crate::types::{PyAnyMethods, PyList, PySequence, PySequenceMethods, PyTuple}; - use crate::{ffi, PyObject, Python}; + use crate::{ffi, IntoPyObject, PyObject, Python}; fn get_object() -> PyObject { // Convenience function for getting a single unique object diff --git a/src/types/set.rs b/src/types/set.rs index 3e28b2c20e0..622020dfb11 100644 --- a/src/types/set.rs +++ b/src/types/set.rs @@ -1,5 +1,7 @@ use crate::conversion::IntoPyObject; use crate::types::PyIterator; +#[allow(deprecated)] +use crate::ToPyObject; use crate::{ err::{self, PyErr, PyResult}, ffi_ptr_ext::FfiPtrExt, @@ -7,7 +9,7 @@ use crate::{ py_result_ext::PyResultExt, types::any::PyAnyMethods, }; -use crate::{ffi, Borrowed, BoundObject, PyAny, Python, ToPyObject}; +use crate::{ffi, Borrowed, BoundObject, PyAny, Python}; use std::ptr; /// Represents a Python `set`. @@ -55,6 +57,7 @@ impl PySet { /// Deprecated name for [`PySet::new`]. #[deprecated(since = "0.23.0", note = "renamed to `PySet::new`")] + #[allow(deprecated)] #[inline] pub fn new_bound<'a, 'p, T: ToPyObject + 'a>( py: Python<'p>, @@ -285,6 +288,7 @@ impl ExactSizeIterator for BoundSetIterator<'_> { } } +#[allow(deprecated)] #[inline] pub(crate) fn new_from_iter( py: Python<'_>, diff --git a/src/types/slice.rs b/src/types/slice.rs index 528e7893476..9ca2aa4ec43 100644 --- a/src/types/slice.rs +++ b/src/types/slice.rs @@ -1,9 +1,10 @@ use crate::err::{PyErr, PyResult}; use crate::ffi; use crate::ffi_ptr_ext::FfiPtrExt; -use crate::prelude::IntoPyObject; use crate::types::any::PyAnyMethods; -use crate::{Bound, PyAny, PyObject, Python, ToPyObject}; +#[allow(deprecated)] +use crate::ToPyObject; +use crate::{Bound, IntoPyObject, PyAny, PyObject, Python}; use std::convert::Infallible; /// Represents a Python `slice`. @@ -135,6 +136,7 @@ impl<'py> PySliceMethods<'py> for Bound<'py, PySlice> { } } +#[allow(deprecated)] impl ToPyObject for PySliceIndices { fn to_object(&self, py: Python<'_>) -> PyObject { PySlice::new(py, self.start, self.stop, self.step).into() diff --git a/src/types/string.rs b/src/types/string.rs index c4aea67423d..1bcd025d1ce 100644 --- a/src/types/string.rs +++ b/src/types/string.rs @@ -577,7 +577,7 @@ impl PartialEq> for &'_ str { #[cfg(test)] mod tests { use super::*; - use crate::{PyObject, ToPyObject}; + use crate::{IntoPyObject, PyObject}; #[test] fn test_to_cow_utf8() { @@ -650,8 +650,7 @@ mod tests { #[test] fn test_debug_string() { Python::with_gil(|py| { - let v = "Hello\n".to_object(py); - let s = v.downcast_bound::(py).unwrap(); + let s = "Hello\n".into_pyobject(py).unwrap(); assert_eq!(format!("{:?}", s), "'Hello\\n'"); }) } @@ -659,8 +658,7 @@ mod tests { #[test] fn test_display_string() { Python::with_gil(|py| { - let v = "Hello\n".to_object(py); - let s = v.downcast_bound::(py).unwrap(); + let s = "Hello\n".into_pyobject(py).unwrap(); assert_eq!(format!("{}", s), "Hello\n"); }) } diff --git a/src/types/tuple.rs b/src/types/tuple.rs index 7c8abfcae91..b2944741256 100644 --- a/src/types/tuple.rs +++ b/src/types/tuple.rs @@ -10,9 +10,11 @@ use crate::internal_tricks::get_ssize_index; use crate::types::{ any::PyAnyMethods, sequence::PySequenceMethods, PyDict, PyList, PySequence, PyString, }; +#[allow(deprecated)] +use crate::ToPyObject; use crate::{ exceptions, Bound, BoundObject, FromPyObject, IntoPy, Py, PyAny, PyErr, PyObject, PyResult, - Python, ToPyObject, + Python, }; #[inline] @@ -111,6 +113,7 @@ impl PyTuple { /// Deprecated name for [`PyTuple::new`]. #[deprecated(since = "0.23.0", note = "renamed to `PyTuple::new`")] + #[allow(deprecated)] #[track_caller] #[inline] pub fn new_bound( @@ -169,14 +172,13 @@ pub trait PyTupleMethods<'py>: crate::sealed::Sealed { /// Gets the tuple item at the specified index. /// # Example /// ``` - /// use pyo3::{prelude::*, types::PyTuple}; + /// use pyo3::prelude::*; /// /// # fn main() -> PyResult<()> { /// Python::with_gil(|py| -> PyResult<()> { - /// let ob = (1, 2, 3).to_object(py); - /// let tuple = ob.downcast_bound::(py).unwrap(); + /// let tuple = (1, 2, 3).into_pyobject(py)?; /// let obj = tuple.get_item(0); - /// assert_eq!(obj.unwrap().extract::().unwrap(), 1); + /// assert_eq!(obj?.extract::()?, 1); /// Ok(()) /// }) /// # } @@ -519,6 +521,7 @@ fn wrong_tuple_length(t: &Bound<'_, PyTuple>, expected_length: usize) -> PyErr { } macro_rules! tuple_conversion ({$length:expr,$(($refN:ident, $n:tt, $T:ident)),+} => { + #[allow(deprecated)] impl <$($T: ToPyObject),+> ToPyObject for ($($T,)+) { fn to_object(&self, py: Python<'_>) -> PyObject { array_into_tuple(py, [$(self.$n.to_object(py)),+]).into() @@ -818,8 +821,9 @@ tuple_conversion!( #[cfg(test)] mod tests { use crate::types::{any::PyAnyMethods, tuple::PyTupleMethods, PyList, PyTuple}; - use crate::Python; + use crate::{IntoPyObject, Python}; use std::collections::HashSet; + use std::ops::Range; #[test] fn test_new() { @@ -1139,9 +1143,6 @@ mod tests { }); } - use crate::prelude::IntoPyObject; - use std::ops::Range; - // An iterator that lies about its `ExactSizeIterator` implementation. // See https://github.com/PyO3/pyo3/issues/2118 struct FaultyIter(Range, usize); diff --git a/src/types/weakref/proxy.rs b/src/types/weakref/proxy.rs index a16c7583882..bf9d69295d2 100644 --- a/src/types/weakref/proxy.rs +++ b/src/types/weakref/proxy.rs @@ -3,7 +3,7 @@ use crate::ffi_ptr_ext::FfiPtrExt; use crate::py_result_ext::PyResultExt; use crate::type_object::PyTypeCheck; use crate::types::{any::PyAny, PyNone}; -use crate::{ffi, Bound, ToPyObject}; +use crate::{ffi, Bound, BoundObject, IntoPyObject}; use super::PyWeakrefMethods; @@ -148,7 +148,7 @@ impl PyWeakrefProxy { callback: C, ) -> PyResult> where - C: ToPyObject, + C: IntoPyObject<'py>, { fn inner<'py>( object: &Bound<'py, PyAny>, @@ -164,20 +164,28 @@ impl PyWeakrefProxy { } let py = object.py(); - inner(object, callback.to_object(py).into_bound(py)) + inner( + object, + callback + .into_pyobject(py) + .map(BoundObject::into_any) + .map(BoundObject::into_bound) + .map_err(Into::into)?, + ) } /// Deprecated name for [`PyWeakrefProxy::new_with`]. #[deprecated(since = "0.23.0", note = "renamed to `PyWeakrefProxy::new_with`")] + #[allow(deprecated)] #[inline] pub fn new_bound_with<'py, C>( object: &Bound<'py, PyAny>, callback: C, ) -> PyResult> where - C: ToPyObject, + C: crate::ToPyObject, { - Self::new_with(object, callback) + Self::new_with(object, callback.to_object(object.py())) } } diff --git a/src/types/weakref/reference.rs b/src/types/weakref/reference.rs index 40890e0f887..f26df4953ea 100644 --- a/src/types/weakref/reference.rs +++ b/src/types/weakref/reference.rs @@ -2,7 +2,7 @@ use crate::err::PyResult; use crate::ffi_ptr_ext::FfiPtrExt; use crate::py_result_ext::PyResultExt; use crate::types::{any::PyAny, PyNone}; -use crate::{ffi, Bound, ToPyObject}; +use crate::{ffi, Bound, BoundObject, IntoPyObject}; #[cfg(any(PyPy, GraalPy, Py_LIMITED_API))] use crate::type_object::PyTypeCheck; @@ -157,7 +157,7 @@ impl PyWeakrefReference { callback: C, ) -> PyResult> where - C: ToPyObject, + C: IntoPyObject<'py>, { fn inner<'py>( object: &Bound<'py, PyAny>, @@ -173,20 +173,28 @@ impl PyWeakrefReference { } let py = object.py(); - inner(object, callback.to_object(py).into_bound(py)) + inner( + object, + callback + .into_pyobject(py) + .map(BoundObject::into_any) + .map(BoundObject::into_bound) + .map_err(Into::into)?, + ) } /// Deprecated name for [`PyWeakrefReference::new_with`]. #[deprecated(since = "0.23.0", note = "renamed to `PyWeakrefReference::new_with`")] + #[allow(deprecated)] #[inline] pub fn new_bound_with<'py, C>( object: &Bound<'py, PyAny>, callback: C, ) -> PyResult> where - C: ToPyObject, + C: crate::ToPyObject, { - Self::new_with(object, callback) + Self::new_with(object, callback.to_object(object.py())) } } diff --git a/tests/test_class_conversion.rs b/tests/test_class_conversion.rs index ede8928f865..671dcd126b2 100644 --- a/tests/test_class_conversion.rs +++ b/tests/test_class_conversion.rs @@ -17,7 +17,7 @@ fn test_cloneable_pyclass() { let c = Cloneable { x: 10 }; Python::with_gil(|py| { - let py_c = Py::new(py, c.clone()).unwrap().to_object(py); + let py_c = Py::new(py, c.clone()).unwrap(); let c2: Cloneable = py_c.extract(py).unwrap(); assert_eq!(c, c2); @@ -69,8 +69,7 @@ fn test_polymorphic_container_stores_base_class() { inner: Py::new(py, BaseClass::default()).unwrap(), }, ) - .unwrap() - .to_object(py); + .unwrap(); py_assert!(py, p, "p.inner.foo() == 'BaseClass'"); }); @@ -85,8 +84,7 @@ fn test_polymorphic_container_stores_sub_class() { inner: Py::new(py, BaseClass::default()).unwrap(), }, ) - .unwrap() - .to_object(py); + .unwrap(); p.bind(py) .setattr( @@ -112,8 +110,7 @@ fn test_polymorphic_container_does_not_accept_other_types() { inner: Py::new(py, BaseClass::default()).unwrap(), }, ) - .unwrap() - .to_object(py); + .unwrap(); let setattr = |value: PyObject| p.bind(py).setattr("inner", value); diff --git a/tests/test_gc.rs b/tests/test_gc.rs index ab1302a9d88..8e70d6ee510 100644 --- a/tests/test_gc.rs +++ b/tests/test_gc.rs @@ -121,7 +121,7 @@ fn gc_integration() { .unwrap(); let mut borrow = inst.borrow_mut(); - borrow.self_ref = inst.to_object(py); + borrow.self_ref = inst.clone().into_any().unbind(); py_run!(py, inst, "import gc; assert inst in gc.get_objects()"); }); @@ -274,18 +274,16 @@ fn gc_during_borrow() { // create an object and check that traversing it works normally // when it's not borrowed let cell = Bound::new(py, TraversableClass::new()).unwrap(); - let obj = cell.to_object(py); assert!(!cell.borrow().traversed.load(Ordering::Relaxed)); - traverse(obj.as_ptr(), novisit, std::ptr::null_mut()); + traverse(cell.as_ptr(), novisit, std::ptr::null_mut()); assert!(cell.borrow().traversed.load(Ordering::Relaxed)); // create an object and check that it is not traversed if the GC // is invoked while it is already borrowed mutably let cell2 = Bound::new(py, TraversableClass::new()).unwrap(); - let obj2 = cell2.to_object(py); let guard = cell2.borrow_mut(); assert!(!guard.traversed.load(Ordering::Relaxed)); - traverse(obj2.as_ptr(), novisit, std::ptr::null_mut()); + traverse(cell2.as_ptr(), novisit, std::ptr::null_mut()); assert!(!guard.traversed.load(Ordering::Relaxed)); drop(guard); } @@ -431,9 +429,8 @@ fn traverse_cannot_be_hijacked() { let traverse = get_type_traverse(ty.as_type_ptr()).unwrap(); let cell = Bound::new(py, HijackedTraverse::new()).unwrap(); - let obj = cell.to_object(py); assert_eq!(cell.borrow().traversed_and_hijacked(), (false, false)); - traverse(obj.as_ptr(), novisit, std::ptr::null_mut()); + traverse(cell.as_ptr(), novisit, std::ptr::null_mut()); assert_eq!(cell.borrow().traversed_and_hijacked(), (true, false)); }) } diff --git a/tests/test_getter_setter.rs b/tests/test_getter_setter.rs index d8cc3af20e6..144e3f2b7eb 100644 --- a/tests/test_getter_setter.rs +++ b/tests/test_getter_setter.rs @@ -228,8 +228,8 @@ fn cell_getter_setter() { cell_inner: Cell::new(10), }; Python::with_gil(|py| { - let inst = Py::new(py, c).unwrap().to_object(py); - let cell = Cell::new(20).to_object(py); + let inst = Py::new(py, c).unwrap(); + let cell = Cell::new(20i32).into_pyobject(py).unwrap(); py_run!(py, cell, "assert cell == 20"); py_run!(py, inst, "assert inst.cell_inner == 10"); @@ -255,7 +255,7 @@ fn borrowed_value_with_lifetime_of_self() { } Python::with_gil(|py| { - let inst = Py::new(py, BorrowedValue {}).unwrap().to_object(py); + let inst = Py::new(py, BorrowedValue {}).unwrap(); py_run!(py, inst, "assert inst.value == 'value'"); }); @@ -276,8 +276,7 @@ fn frozen_py_field_get() { value: "value".into_py(py), }, ) - .unwrap() - .to_object(py); + .unwrap(); py_run!(py, inst, "assert inst.value == 'value'"); }); diff --git a/tests/test_inheritance.rs b/tests/test_inheritance.rs index 2cce50bba25..7190dd49555 100644 --- a/tests/test_inheritance.rs +++ b/tests/test_inheritance.rs @@ -285,7 +285,7 @@ mod inheriting_native_type { Some(&dict) ); let err = res.unwrap_err(); - assert!(err.matches(py, &cls), "{}", err); + assert!(err.matches(py, &cls).unwrap(), "{}", err); // catching the exception in Python also works: py_run!( diff --git a/tests/test_methods.rs b/tests/test_methods.rs index eb099bc0aa5..82258ab7b67 100644 --- a/tests/test_methods.rs +++ b/tests/test_methods.rs @@ -4,6 +4,7 @@ use pyo3::prelude::*; use pyo3::py_run; use pyo3::types::PySequence; use pyo3::types::{IntoPyDict, PyDict, PyList, PySet, PyString, PyTuple, PyType}; +use pyo3::BoundObject; #[path = "../src/tests/common.rs"] mod common; @@ -213,24 +214,33 @@ impl MethSignature { test } #[pyo3(signature = (*args, **kwargs))] - fn get_kwargs( + fn get_kwargs<'py>( &self, - py: Python<'_>, - args: &Bound<'_, PyTuple>, - kwargs: Option<&Bound<'_, PyDict>>, - ) -> PyObject { - [args.to_object(py), kwargs.to_object(py)].to_object(py) + py: Python<'py>, + args: &Bound<'py, PyTuple>, + kwargs: Option<&Bound<'py, PyDict>>, + ) -> PyResult> { + [ + args.as_any().clone(), + kwargs.into_pyobject(py)?.into_any().into_bound(), + ] + .into_pyobject(py) } #[pyo3(signature = (a, *args, **kwargs))] - fn get_pos_arg_kw( + fn get_pos_arg_kw<'py>( &self, - py: Python<'_>, + py: Python<'py>, a: i32, - args: &Bound<'_, PyTuple>, - kwargs: Option<&Bound<'_, PyDict>>, - ) -> PyObject { - [a.to_object(py), args.to_object(py), kwargs.to_object(py)].to_object(py) + args: &Bound<'py, PyTuple>, + kwargs: Option<&Bound<'py, PyDict>>, + ) -> PyResult> { + [ + a.into_pyobject(py)?.into_any().into_bound(), + args.as_any().clone(), + kwargs.into_pyobject(py)?.into_any().into_bound(), + ] + .into_pyobject(py) } #[pyo3(signature = (a, b, /))] @@ -274,8 +284,13 @@ impl MethSignature { py: Python<'_>, a: i32, kwargs: Option<&Bound<'_, PyDict>>, - ) -> PyObject { - [a.to_object(py), kwargs.to_object(py)].to_object(py) + ) -> PyResult { + [ + a.into_pyobject(py)?.into_any().into_bound(), + kwargs.into_pyobject(py)?.into_any().into_bound(), + ] + .into_pyobject(py) + .map(BoundObject::unbind) } #[pyo3(signature = (a=0, /, **kwargs))] @@ -284,8 +299,13 @@ impl MethSignature { py: Python<'_>, a: i32, kwargs: Option<&Bound<'_, PyDict>>, - ) -> PyObject { - [a.to_object(py), kwargs.to_object(py)].to_object(py) + ) -> PyResult { + [ + a.into_pyobject(py)?.into_any().into_bound(), + kwargs.into_pyobject(py)?.into_any().into_bound(), + ] + .into_pyobject(py) + .map(BoundObject::unbind) } #[pyo3(signature = (*, a = 2, b = 3))] @@ -309,8 +329,11 @@ impl MethSignature { py: Python<'_>, args: &Bound<'_, PyTuple>, a: i32, - ) -> PyObject { - (args, a).to_object(py) + ) -> PyResult { + (args, a) + .into_pyobject(py) + .map(BoundObject::into_any) + .map(BoundObject::unbind) } #[pyo3(signature = (a, b = 2, *, c = 3))] @@ -324,8 +347,18 @@ impl MethSignature { } #[pyo3(signature = (a, **kwargs))] - fn get_pos_kw(&self, py: Python<'_>, a: i32, kwargs: Option<&Bound<'_, PyDict>>) -> PyObject { - [a.to_object(py), kwargs.to_object(py)].to_object(py) + fn get_pos_kw( + &self, + py: Python<'_>, + a: i32, + kwargs: Option<&Bound<'_, PyDict>>, + ) -> PyResult { + [ + a.into_pyobject(py)?.into_any().into_bound(), + kwargs.into_pyobject(py)?.into_any().into_bound(), + ] + .into_pyobject(py) + .map(BoundObject::unbind) } // "args" can be anything that can be extracted from PyTuple diff --git a/tests/test_module.rs b/tests/test_module.rs index c33a8a27ee5..7b97fb3a889 100644 --- a/tests/test_module.rs +++ b/tests/test_module.rs @@ -5,6 +5,7 @@ use pyo3::prelude::*; use pyo3::py_run; use pyo3::types::PyString; use pyo3::types::{IntoPyDict, PyDict, PyTuple}; +use pyo3::BoundObject; use pyo3_ffi::c_str; #[path = "../src/tests/common.rs"] @@ -173,13 +174,12 @@ fn test_module_from_code_bound() { let add_func = adder_mod .getattr("add") - .expect("Add function should be in the module") - .to_object(py); + .expect("Add function should be in the module"); let ret_value: i32 = add_func - .call1(py, (1, 2)) + .call1((1, 2)) .expect("A value should be returned") - .extract(py) + .extract() .expect("The value should be able to be converted to an i32"); assert_eq!(ret_value, 3); @@ -321,14 +321,19 @@ fn test_module_nesting() { // Test that argument parsing specification works for pyfunctions #[pyfunction(signature = (a=5, *args))] -fn ext_vararg_fn(py: Python<'_>, a: i32, args: &Bound<'_, PyTuple>) -> PyObject { - [a.to_object(py), args.into_py(py)].to_object(py) +fn ext_vararg_fn(py: Python<'_>, a: i32, args: &Bound<'_, PyTuple>) -> PyResult { + [ + a.into_pyobject(py)?.into_any().into_bound(), + args.as_any().clone(), + ] + .into_pyobject(py) + .map(BoundObject::unbind) } #[pymodule] fn vararg_module(m: &Bound<'_, PyModule>) -> PyResult<()> { #[pyfn(m, signature = (a=5, *args))] - fn int_vararg_fn(py: Python<'_>, a: i32, args: &Bound<'_, PyTuple>) -> PyObject { + fn int_vararg_fn(py: Python<'_>, a: i32, args: &Bound<'_, PyTuple>) -> PyResult { ext_vararg_fn(py, a, args) } diff --git a/tests/test_sequence.rs b/tests/test_sequence.rs index fd7e9eb01c6..484e519fd21 100644 --- a/tests/test_sequence.rs +++ b/tests/test_sequence.rs @@ -264,7 +264,10 @@ struct GenericList { fn test_generic_list_get() { Python::with_gil(|py| { let list: PyObject = GenericList { - items: [1, 2, 3].iter().map(|i| i.to_object(py)).collect(), + items: [1i32, 2, 3] + .iter() + .map(|i| i.into_pyobject(py).unwrap().into_any().unbind()) + .collect(), } .into_py(py); diff --git a/tests/test_various.rs b/tests/test_various.rs index 27192aba3bb..a8aa6b7a71f 100644 --- a/tests/test_various.rs +++ b/tests/test_various.rs @@ -132,9 +132,9 @@ fn test_pickle() { pub fn __reduce__<'py>( slf: &Bound<'py, Self>, py: Python<'py>, - ) -> PyResult<(PyObject, Bound<'py, PyTuple>, PyObject)> { - let cls = slf.to_object(py).getattr(py, "__class__")?; - let dict = slf.to_object(py).getattr(py, "__dict__")?; + ) -> PyResult<(Bound<'py, PyAny>, Bound<'py, PyTuple>, Bound<'py, PyAny>)> { + let cls = slf.getattr("__class__")?; + let dict = slf.getattr("__dict__")?; Ok((cls, PyTuple::empty(py), dict)) } } From da95ab4d9cb64c81477128b2b1cdee3e14b00491 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Mon, 7 Oct 2024 21:31:48 +0100 Subject: [PATCH 307/495] avoid calling `PyType_GetSlot` on static types before Python 3.10 (#4599) * avoid calling `PyType_GetSlot` on static types before Python 3.10 * use the correct tp_free * fixup --- newsfragments/4599.fixed.md | 1 + src/internal.rs | 3 + src/internal/get_slot.rs | 234 ++++++++++++++++++++++++++++++++++++ src/lib.rs | 1 + src/pycell/impl_.rs | 27 +++-- src/pyclass_init.rs | 17 ++- src/type_object.rs | 29 ----- src/types/any.rs | 22 ++-- 8 files changed, 279 insertions(+), 55 deletions(-) create mode 100644 newsfragments/4599.fixed.md create mode 100644 src/internal.rs create mode 100644 src/internal/get_slot.rs diff --git a/newsfragments/4599.fixed.md b/newsfragments/4599.fixed.md new file mode 100644 index 00000000000..6ed2dea323b --- /dev/null +++ b/newsfragments/4599.fixed.md @@ -0,0 +1 @@ +Fix crash calling `PyType_GetSlot` on static types before Python 3.10. diff --git a/src/internal.rs b/src/internal.rs new file mode 100644 index 00000000000..d42cc2b87fc --- /dev/null +++ b/src/internal.rs @@ -0,0 +1,3 @@ +//! Holding place for code which is not intended to be reachable from outside of PyO3. + +pub(crate) mod get_slot; diff --git a/src/internal/get_slot.rs b/src/internal/get_slot.rs new file mode 100644 index 00000000000..c151e855a14 --- /dev/null +++ b/src/internal/get_slot.rs @@ -0,0 +1,234 @@ +use crate::{ + ffi, + types::{PyType, PyTypeMethods}, + Borrowed, Bound, +}; +use std::os::raw::c_int; + +impl Bound<'_, PyType> { + #[inline] + pub(crate) fn get_slot(&self, slot: Slot) -> as GetSlotImpl>::Type + where + Slot: GetSlotImpl, + { + slot.get_slot(self.as_borrowed()) + } +} + +impl Borrowed<'_, '_, PyType> { + #[inline] + pub(crate) fn get_slot(self, slot: Slot) -> as GetSlotImpl>::Type + where + Slot: GetSlotImpl, + { + slot.get_slot(self) + } +} + +pub(crate) trait GetSlotImpl { + type Type; + fn get_slot(self, tp: Borrowed<'_, '_, PyType>) -> Self::Type; +} + +#[derive(Copy, Clone)] +pub(crate) struct Slot; + +macro_rules! impl_slots { + ($($name:ident: ($slot:ident, $field:ident) -> $tp:ty),+ $(,)?) => { + $( + pub (crate) const $name: Slot<{ ffi::$slot }> = Slot; + + impl GetSlotImpl for Slot<{ ffi::$slot }> { + type Type = $tp; + + #[inline] + fn get_slot(self, tp: Borrowed<'_, '_, PyType>) -> Self::Type { + let ptr = tp.as_type_ptr(); + + #[cfg(not(Py_LIMITED_API))] + unsafe { + (*ptr).$field + } + + #[cfg(Py_LIMITED_API)] + { + #[cfg(not(Py_3_10))] + { + // Calling PyType_GetSlot on static types is not valid before Python 3.10 + // ... so the workaround is to first do a runtime check for these versions + // (3.7, 3.8, 3.9) and then look in the type object anyway. This is only ok + // because we know that the interpreter is not going to change the size + // of the type objects for these historical versions. + if !is_runtime_3_10(tp.py()) + && unsafe { ffi::PyType_HasFeature(ptr, ffi::Py_TPFLAGS_HEAPTYPE) } == 0 + { + return unsafe { (*ptr.cast::()).$field }; + } + } + + // SAFETY: slot type is set carefully to be valid + unsafe { std::mem::transmute(ffi::PyType_GetSlot(ptr, ffi::$slot)) } + } + } + } + )* + }; +} + +// Slots are implemented on-demand as needed. +impl_slots! { + TP_ALLOC: (Py_tp_alloc, tp_alloc) -> Option, + TP_DESCR_GET: (Py_tp_descr_get, tp_descr_get) -> Option, + TP_FREE: (Py_tp_free, tp_free) -> Option, +} + +#[cfg(all(Py_LIMITED_API, not(Py_3_10)))] +fn is_runtime_3_10(py: crate::Python<'_>) -> bool { + use crate::sync::GILOnceCell; + + static IS_RUNTIME_3_10: GILOnceCell = GILOnceCell::new(); + *IS_RUNTIME_3_10.get_or_init(py, || py.version_info() >= (3, 10)) +} + +#[repr(C)] +#[cfg(all(Py_LIMITED_API, not(Py_3_10)))] +pub struct PyNumberMethods39Snapshot { + pub nb_add: Option, + pub nb_subtract: Option, + pub nb_multiply: Option, + pub nb_remainder: Option, + pub nb_divmod: Option, + pub nb_power: Option, + pub nb_negative: Option, + pub nb_positive: Option, + pub nb_absolute: Option, + pub nb_bool: Option, + pub nb_invert: Option, + pub nb_lshift: Option, + pub nb_rshift: Option, + pub nb_and: Option, + pub nb_xor: Option, + pub nb_or: Option, + pub nb_int: Option, + pub nb_reserved: *mut std::os::raw::c_void, + pub nb_float: Option, + pub nb_inplace_add: Option, + pub nb_inplace_subtract: Option, + pub nb_inplace_multiply: Option, + pub nb_inplace_remainder: Option, + pub nb_inplace_power: Option, + pub nb_inplace_lshift: Option, + pub nb_inplace_rshift: Option, + pub nb_inplace_and: Option, + pub nb_inplace_xor: Option, + pub nb_inplace_or: Option, + pub nb_floor_divide: Option, + pub nb_true_divide: Option, + pub nb_inplace_floor_divide: Option, + pub nb_inplace_true_divide: Option, + pub nb_index: Option, + pub nb_matrix_multiply: Option, + pub nb_inplace_matrix_multiply: Option, +} + +#[repr(C)] +#[cfg(all(Py_LIMITED_API, not(Py_3_10)))] +pub struct PySequenceMethods39Snapshot { + pub sq_length: Option, + pub sq_concat: Option, + pub sq_repeat: Option, + pub sq_item: Option, + pub was_sq_slice: *mut std::os::raw::c_void, + pub sq_ass_item: Option, + pub was_sq_ass_slice: *mut std::os::raw::c_void, + pub sq_contains: Option, + pub sq_inplace_concat: Option, + pub sq_inplace_repeat: Option, +} + +#[repr(C)] +#[cfg(all(Py_LIMITED_API, not(Py_3_10)))] +pub struct PyMappingMethods39Snapshot { + pub mp_length: Option, + pub mp_subscript: Option, + pub mp_ass_subscript: Option, +} + +#[repr(C)] +#[cfg(all(Py_LIMITED_API, not(Py_3_10)))] +pub struct PyAsyncMethods39Snapshot { + pub am_await: Option, + pub am_aiter: Option, + pub am_anext: Option, +} + +#[repr(C)] +#[cfg(all(Py_LIMITED_API, not(Py_3_10)))] +pub struct PyBufferProcs39Snapshot { + // not available in limited api, but structure needs to have the right size + pub bf_getbuffer: *mut std::os::raw::c_void, + pub bf_releasebuffer: *mut std::os::raw::c_void, +} + +/// Snapshot of the structure of PyTypeObject for Python 3.7 through 3.9. +/// +/// This is used as a fallback for static types in abi3 when the Python version is less than 3.10; +/// this is a bit of a hack but there's no better option and the structure of the type object is +/// not going to change for those historical versions. +#[repr(C)] +#[cfg(all(Py_LIMITED_API, not(Py_3_10)))] +struct PyTypeObject39Snapshot { + pub ob_base: ffi::PyVarObject, + pub tp_name: *const std::os::raw::c_char, + pub tp_basicsize: ffi::Py_ssize_t, + pub tp_itemsize: ffi::Py_ssize_t, + pub tp_dealloc: Option, + #[cfg(not(Py_3_8))] + pub tp_print: *mut std::os::raw::c_void, // stubbed out, not available in limited API + #[cfg(Py_3_8)] + pub tp_vectorcall_offset: ffi::Py_ssize_t, + pub tp_getattr: Option, + pub tp_setattr: Option, + pub tp_as_async: *mut PyAsyncMethods39Snapshot, + pub tp_repr: Option, + pub tp_as_number: *mut PyNumberMethods39Snapshot, + pub tp_as_sequence: *mut PySequenceMethods39Snapshot, + pub tp_as_mapping: *mut PyMappingMethods39Snapshot, + pub tp_hash: Option, + pub tp_call: Option, + pub tp_str: Option, + pub tp_getattro: Option, + pub tp_setattro: Option, + pub tp_as_buffer: *mut PyBufferProcs39Snapshot, + pub tp_flags: std::os::raw::c_ulong, + pub tp_doc: *const std::os::raw::c_char, + pub tp_traverse: Option, + pub tp_clear: Option, + pub tp_richcompare: Option, + pub tp_weaklistoffset: ffi::Py_ssize_t, + pub tp_iter: Option, + pub tp_iternext: Option, + pub tp_methods: *mut ffi::PyMethodDef, + pub tp_members: *mut ffi::PyMemberDef, + pub tp_getset: *mut ffi::PyGetSetDef, + pub tp_base: *mut ffi::PyTypeObject, + pub tp_dict: *mut ffi::PyObject, + pub tp_descr_get: Option, + pub tp_descr_set: Option, + pub tp_dictoffset: ffi::Py_ssize_t, + pub tp_init: Option, + pub tp_alloc: Option, + pub tp_new: Option, + pub tp_free: Option, + pub tp_is_gc: Option, + pub tp_bases: *mut ffi::PyObject, + pub tp_mro: *mut ffi::PyObject, + pub tp_cache: *mut ffi::PyObject, + pub tp_subclasses: *mut ffi::PyObject, + pub tp_weaklist: *mut ffi::PyObject, + pub tp_del: Option, + pub tp_version_tag: std::os::raw::c_uint, + pub tp_finalize: Option, + #[cfg(Py_3_8)] + pub tp_vectorcall: Option, +} diff --git a/src/lib.rs b/src/lib.rs index 2cb189fb489..239b001b8ac 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -411,6 +411,7 @@ mod tests; #[macro_use] mod internal_tricks; +mod internal; pub mod buffer; #[doc(hidden)] diff --git a/src/pycell/impl_.rs b/src/pycell/impl_.rs index 1bd225de830..1b0724d8481 100644 --- a/src/pycell/impl_.rs +++ b/src/pycell/impl_.rs @@ -9,7 +9,9 @@ use std::sync::atomic::{AtomicUsize, Ordering}; use crate::impl_::pyclass::{ PyClassBaseType, PyClassDict, PyClassImpl, PyClassThreadChecker, PyClassWeakRef, }; -use crate::type_object::{get_tp_free, PyLayout, PySizedLayout}; +use crate::internal::get_slot::TP_FREE; +use crate::type_object::{PyLayout, PySizedLayout}; +use crate::types::{PyType, PyTypeMethods}; use crate::{ffi, PyClass, PyTypeInfo, Python}; use super::{PyBorrowError, PyBorrowMutError}; @@ -232,26 +234,37 @@ where Ok(()) } unsafe fn tp_dealloc(py: Python<'_>, slf: *mut ffi::PyObject) { - let type_obj = T::type_object_raw(py); + // FIXME: there is potentially subtle issues here if the base is overwritten + // at runtime? To be investigated. + let type_obj = T::type_object(py); + let type_ptr = type_obj.as_type_ptr(); + let actual_type = PyType::from_borrowed_type_ptr(py, ffi::Py_TYPE(slf)); + // For `#[pyclass]` types which inherit from PyAny, we can just call tp_free - if type_obj == std::ptr::addr_of_mut!(ffi::PyBaseObject_Type) { - return get_tp_free(ffi::Py_TYPE(slf))(slf.cast()); + if type_ptr == std::ptr::addr_of_mut!(ffi::PyBaseObject_Type) { + let tp_free = actual_type + .get_slot(TP_FREE) + .expect("PyBaseObject_Type should have tp_free"); + return tp_free(slf.cast()); } // More complex native types (e.g. `extends=PyDict`) require calling the base's dealloc. #[cfg(not(Py_LIMITED_API))] { - if let Some(dealloc) = (*type_obj).tp_dealloc { + // FIXME: should this be using actual_type.tp_dealloc? + if let Some(dealloc) = (*type_ptr).tp_dealloc { // Before CPython 3.11 BaseException_dealloc would use Py_GC_UNTRACK which // assumes the exception is currently GC tracked, so we have to re-track // before calling the dealloc so that it can safely call Py_GC_UNTRACK. #[cfg(not(any(Py_3_11, PyPy)))] - if ffi::PyType_FastSubclass(type_obj, ffi::Py_TPFLAGS_BASE_EXC_SUBCLASS) == 1 { + if ffi::PyType_FastSubclass(type_ptr, ffi::Py_TPFLAGS_BASE_EXC_SUBCLASS) == 1 { ffi::PyObject_GC_Track(slf.cast()); } dealloc(slf); } else { - get_tp_free(ffi::Py_TYPE(slf))(slf.cast()); + (*actual_type.as_type_ptr()) + .tp_free + .expect("type missing tp_free")(slf.cast()); } } diff --git a/src/pyclass_init.rs b/src/pyclass_init.rs index 12375964a7d..1d669ea1554 100644 --- a/src/pyclass_init.rs +++ b/src/pyclass_init.rs @@ -2,12 +2,13 @@ use crate::callback::IntoPyCallbackOutput; use crate::ffi_ptr_ext::FfiPtrExt; use crate::impl_::pyclass::{PyClassBaseType, PyClassDict, PyClassThreadChecker, PyClassWeakRef}; -use crate::types::PyAnyMethods; -use crate::{ffi, Bound, Py, PyClass, PyErr, PyResult, Python}; +use crate::internal::get_slot::TP_ALLOC; +use crate::types::{PyAnyMethods, PyType}; +use crate::{ffi, Borrowed, Bound, Py, PyClass, PyErr, PyResult, Python}; use crate::{ ffi::PyTypeObject, pycell::impl_::{PyClassBorrowChecker, PyClassMutability, PyClassObjectContents}, - type_object::{get_tp_alloc, PyTypeInfo}, + type_object::PyTypeInfo, }; use std::{ cell::UnsafeCell, @@ -50,8 +51,16 @@ impl PyObjectInit for PyNativeTypeInitializer { ) -> PyResult<*mut ffi::PyObject> { // HACK (due to FIXME below): PyBaseObject_Type's tp_new isn't happy with NULL arguments let is_base_object = type_object == std::ptr::addr_of_mut!(ffi::PyBaseObject_Type); + let subtype_borrowed: Borrowed<'_, '_, PyType> = subtype + .cast::() + .assume_borrowed_unchecked(py) + .downcast_unchecked(); + if is_base_object { - let alloc = get_tp_alloc(subtype).unwrap_or(ffi::PyType_GenericAlloc); + let alloc = subtype_borrowed + .get_slot(TP_ALLOC) + .unwrap_or(ffi::PyType_GenericAlloc); + let obj = alloc(subtype, 0); return if obj.is_null() { Err(PyErr::fetch(py)) diff --git a/src/type_object.rs b/src/type_object.rs index df359227365..b7cad4ab3b2 100644 --- a/src/type_object.rs +++ b/src/type_object.rs @@ -118,32 +118,3 @@ where T::is_type_of(object) } } - -#[inline] -pub(crate) unsafe fn get_tp_alloc(tp: *mut ffi::PyTypeObject) -> Option { - #[cfg(not(Py_LIMITED_API))] - { - (*tp).tp_alloc - } - - #[cfg(Py_LIMITED_API)] - { - let ptr = ffi::PyType_GetSlot(tp, ffi::Py_tp_alloc); - std::mem::transmute(ptr) - } -} - -#[inline] -pub(crate) unsafe fn get_tp_free(tp: *mut ffi::PyTypeObject) -> ffi::freefunc { - #[cfg(not(Py_LIMITED_API))] - { - (*tp).tp_free.unwrap() - } - - #[cfg(Py_LIMITED_API)] - { - let ptr = ffi::PyType_GetSlot(tp, ffi::Py_tp_free); - debug_assert_ne!(ptr, std::ptr::null_mut()); - std::mem::transmute(ptr) - } -} diff --git a/src/types/any.rs b/src/types/any.rs index 47cebb0363b..1cb953254a2 100644 --- a/src/types/any.rs +++ b/src/types/any.rs @@ -4,6 +4,7 @@ use crate::err::{DowncastError, DowncastIntoError, PyErr, PyResult}; use crate::exceptions::{PyAttributeError, PyTypeError}; use crate::ffi_ptr_ext::FfiPtrExt; use crate::instance::Bound; +use crate::internal::get_slot::TP_DESCR_GET; use crate::internal_tricks::ptr_from_ref; use crate::py_result_ext::PyResultExt; use crate::type_object::{PyTypeCheck, PyTypeInfo}; @@ -1609,23 +1610,14 @@ impl<'py> Bound<'py, PyAny> { return Ok(None); }; - // Manually resolve descriptor protocol. - if cfg!(Py_3_10) - || unsafe { ffi::PyType_HasFeature(attr.get_type_ptr(), ffi::Py_TPFLAGS_HEAPTYPE) } != 0 - { - // This is the preferred faster path, but does not work on static types (generally, - // types defined in extension modules) before Python 3.10. + // Manually resolve descriptor protocol. (Faster than going through Python.) + if let Some(descr_get) = attr.get_type().get_slot(TP_DESCR_GET) { + // attribute is a descriptor, resolve it unsafe { - let descr_get_ptr = ffi::PyType_GetSlot(attr.get_type_ptr(), ffi::Py_tp_descr_get); - if descr_get_ptr.is_null() { - return Ok(Some(attr)); - } - let descr_get: ffi::descrgetfunc = std::mem::transmute(descr_get_ptr); - let ret = descr_get(attr.as_ptr(), self.as_ptr(), self_type.as_ptr()); - ret.assume_owned_or_err(py).map(Some) + descr_get(attr.as_ptr(), self.as_ptr(), self_type.as_ptr()) + .assume_owned_or_err(py) + .map(Some) } - } else if let Ok(descr_get) = attr.get_type().getattr(crate::intern!(py, "__get__")) { - descr_get.call1((attr, self, self_type)).map(Some) } else { Ok(Some(attr)) } From 4cbf6e0a9b4626234ab9c58c03ff49c7a1ce559c Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Tue, 8 Oct 2024 07:30:50 +0100 Subject: [PATCH 308/495] make `GILOnceCell` threadsafe (#4512) * make `GILOnceCell` threadsafe * fix clippy * update documentation * implement `Drop` for `GILOnceCell` * suggestions from ngoldbaum Co-authored-by: Nathan Goldbaum * fix compile error, adjust comments * add print to failed assertion --------- Co-authored-by: Nathan Goldbaum --- guide/src/faq.md | 12 +-- guide/src/migration.md | 2 + newsfragments/4512.changed.md | 1 + src/conversions/chrono_tz.rs | 2 +- src/sync.rs | 191 +++++++++++++++++++++++++++------- 5 files changed, 165 insertions(+), 43 deletions(-) create mode 100644 newsfragments/4512.changed.md diff --git a/guide/src/faq.md b/guide/src/faq.md index bdccc6503cb..5752e14adbd 100644 --- a/guide/src/faq.md +++ b/guide/src/faq.md @@ -2,18 +2,18 @@ Sorry that you're having trouble using PyO3. If you can't find the answer to your problem in the list below, you can also reach out for help on [GitHub Discussions](https://github.com/PyO3/pyo3/discussions) and on [Discord](https://discord.gg/33kcChzH7f). -## I'm experiencing deadlocks using PyO3 with lazy_static or once_cell! +## I'm experiencing deadlocks using PyO3 with `std::sync::OnceLock`, `std::sync::LazyLock`, `lazy_static`, and `once_cell`! -`lazy_static` and `once_cell::sync` both use locks to ensure that initialization is performed only by a single thread. Because the Python GIL is an additional lock this can lead to deadlocks in the following way: +`OnceLock`, `LazyLock`, and their thirdparty predecessors use blocking to ensure only one thread ever initializes them. Because the Python GIL is an additional lock this can lead to deadlocks in the following way: -1. A thread (thread A) which has acquired the Python GIL starts initialization of a `lazy_static` value. +1. A thread (thread A) which has acquired the Python GIL starts initialization of a `OnceLock` value. 2. The initialization code calls some Python API which temporarily releases the GIL e.g. `Python::import`. -3. Another thread (thread B) acquires the Python GIL and attempts to access the same `lazy_static` value. -4. Thread B is blocked, because it waits for `lazy_static`'s initialization to lock to release. +3. Another thread (thread B) acquires the Python GIL and attempts to access the same `OnceLock` value. +4. Thread B is blocked, because it waits for `OnceLock`'s initialization to lock to release. 5. Thread A is blocked, because it waits to re-acquire the GIL which thread B still holds. 6. Deadlock. -PyO3 provides a struct [`GILOnceCell`] which works equivalently to `OnceCell` but relies solely on the Python GIL for thread safety. This means it can be used in place of `lazy_static` or `once_cell` where you are experiencing the deadlock described above. See the documentation for [`GILOnceCell`] for an example how to use it. +PyO3 provides a struct [`GILOnceCell`] which works similarly to these types but avoids risk of deadlocking with the Python GIL. This means it can be used in place of other choices when you are experiencing the deadlock described above. See the documentation for [`GILOnceCell`] for further details and an example how to use it. [`GILOnceCell`]: {{#PYO3_DOCS_URL}}/pyo3/sync/struct.GILOnceCell.html diff --git a/guide/src/migration.md b/guide/src/migration.md index ba20f39c3db..be6d0748b55 100644 --- a/guide/src/migration.md +++ b/guide/src/migration.md @@ -214,6 +214,8 @@ impl<'a, 'py> IntoPyObject<'py> for &'a MyPyObjectWrapper { PyO3 0.23 introduces preliminary support for the new free-threaded build of CPython 3.13. PyO3 features that implicitly assumed the existence of the GIL are not exposed in the free-threaded build, since they are no longer safe. +Other features, such as `GILOnceCell`, have been internally rewritten to be +threadsafe without the GIL. If you make use of these features then you will need to account for the unavailability of this API in the free-threaded build. One way to handle it is diff --git a/newsfragments/4512.changed.md b/newsfragments/4512.changed.md new file mode 100644 index 00000000000..1e86689c5ae --- /dev/null +++ b/newsfragments/4512.changed.md @@ -0,0 +1 @@ +`GILOnceCell` is now thread-safe for the Python 3.13 freethreaded builds. diff --git a/src/conversions/chrono_tz.rs b/src/conversions/chrono_tz.rs index ff014eb99d9..f766e9ec5c0 100644 --- a/src/conversions/chrono_tz.rs +++ b/src/conversions/chrono_tz.rs @@ -117,7 +117,7 @@ mod tests { fn test_into_pyobject() { Python::with_gil(|py| { let assert_eq = |l: Bound<'_, PyAny>, r: Bound<'_, PyAny>| { - assert!(l.eq(r).unwrap()); + assert!(l.eq(&r).unwrap(), "{:?} != {:?}", l, r); }; assert_eq( diff --git a/src/sync.rs b/src/sync.rs index 2320a5ec42a..65a81d06bd5 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -8,7 +8,7 @@ use crate::{ types::{any::PyAnyMethods, PyAny, PyString}, Bound, Py, PyResult, PyTypeCheck, Python, }; -use std::cell::UnsafeCell; +use std::{cell::UnsafeCell, marker::PhantomData, mem::MaybeUninit, sync::Once}; #[cfg(not(Py_GIL_DISABLED))] use crate::PyVisit; @@ -62,24 +62,36 @@ impl GILProtected { #[cfg(not(Py_GIL_DISABLED))] unsafe impl Sync for GILProtected where T: Send {} -/// A write-once cell similar to [`once_cell::OnceCell`](https://docs.rs/once_cell/latest/once_cell/). +/// A write-once primitive similar to [`std::sync::OnceLock`]. /// -/// Unlike `once_cell::sync` which blocks threads to achieve thread safety, this implementation -/// uses the Python GIL to mediate concurrent access. This helps in cases where `once_cell` or -/// `lazy_static`'s synchronization strategy can lead to deadlocks when interacting with the Python -/// GIL. For an example, see -#[doc = concat!("[the FAQ section](https://pyo3.rs/v", env!("CARGO_PKG_VERSION"), "/faq.html)")] +/// Unlike `OnceLock` which blocks threads to achieve thread safety, `GilOnceCell` +/// allows calls to [`get_or_init`][GILOnceCell::get_or_init] and +/// [`get_or_try_init`][GILOnceCell::get_or_try_init] to race to create an initialized value. +/// (It is still guaranteed that only one thread will ever write to the cell.) +/// +/// On Python versions that run with the Global Interpreter Lock (GIL), this helps to avoid +/// deadlocks between initialization and the GIL. For an example of such a deadlock, see +#[doc = concat!( + "[the FAQ section](https://pyo3.rs/v", + env!("CARGO_PKG_VERSION"), + "/faq.html#im-experiencing-deadlocks-using-pyo3-with-stdsynconcelock-stdsynclazylock-lazy_static-and-once_cell)" +)] /// of the guide. /// -/// Note that: -/// 1) `get_or_init` and `get_or_try_init` do not protect against infinite recursion -/// from reentrant initialization. -/// 2) If the initialization function `f` provided to `get_or_init` (or `get_or_try_init`) -/// temporarily releases the GIL (e.g. by calling `Python::import`) then it is possible -/// for a second thread to also begin initializing the `GITOnceCell`. Even when this -/// happens `GILOnceCell` guarantees that only **one** write to the cell ever occurs - -/// this is treated as a race, other threads will discard the value they compute and -/// return the result of the first complete computation. +/// Note that because the GIL blocks concurrent execution, in practice the means that +/// [`get_or_init`][GILOnceCell::get_or_init] and +/// [`get_or_try_init`][GILOnceCell::get_or_try_init] may race if the initialization +/// function leads to the GIL being released and a thread context switch. This can +/// happen when importing or calling any Python code, as long as it releases the +/// GIL at some point. On free-threaded Python without any GIL, the race is +/// more likely since there is no GIL to prevent races. In the future, PyO3 may change +/// the semantics of GILOnceCell to behave more like the GIL build in the future. +/// +/// # Re-entrant initialization +/// +/// [`get_or_init`][GILOnceCell::get_or_init] and +/// [`get_or_try_init`][GILOnceCell::get_or_try_init] do not protect against infinite recursion +/// from reentrant initialization. /// /// # Examples /// @@ -100,25 +112,64 @@ unsafe impl Sync for GILProtected where T: Send {} /// } /// # Python::with_gil(|py| assert_eq!(get_shared_list(py).len(), 0)); /// ``` -#[derive(Default)] -pub struct GILOnceCell(UnsafeCell>); +pub struct GILOnceCell { + once: Once, + data: UnsafeCell>, + + /// (Copied from std::sync::OnceLock) + /// + /// `PhantomData` to make sure dropck understands we're dropping T in our Drop impl. + /// + /// ```compile_error,E0597 + /// use pyo3::Python; + /// use pyo3::sync::GILOnceCell; + /// + /// struct A<'a>(#[allow(dead_code)] &'a str); + /// + /// impl<'a> Drop for A<'a> { + /// fn drop(&mut self) {} + /// } + /// + /// let cell = GILOnceCell::new(); + /// { + /// let s = String::new(); + /// let _ = Python::with_gil(|py| cell.set(py,A(&s))); + /// } + /// ``` + _marker: PhantomData, +} + +impl Default for GILOnceCell { + fn default() -> Self { + Self::new() + } +} // T: Send is needed for Sync because the thread which drops the GILOnceCell can be different -// to the thread which fills it. +// to the thread which fills it. (e.g. think scoped thread which fills the cell and then exits, +// leaving the cell to be dropped by the main thread). unsafe impl Sync for GILOnceCell {} unsafe impl Send for GILOnceCell {} impl GILOnceCell { /// Create a `GILOnceCell` which does not yet contain a value. pub const fn new() -> Self { - Self(UnsafeCell::new(None)) + Self { + once: Once::new(), + data: UnsafeCell::new(MaybeUninit::uninit()), + _marker: PhantomData, + } } /// Get a reference to the contained value, or `None` if the cell has not yet been written. #[inline] pub fn get(&self, _py: Python<'_>) -> Option<&T> { - // Safe because if the cell has not yet been written, None is returned. - unsafe { &*self.0.get() }.as_ref() + if self.once.is_completed() { + // SAFETY: the cell has been written. + Some(unsafe { (*self.data.get()).assume_init_ref() }) + } else { + None + } } /// Get a reference to the contained value, initializing it if needed using the provided @@ -163,6 +214,10 @@ impl GILOnceCell { // Note that f() could temporarily release the GIL, so it's possible that another thread // writes to this GILOnceCell before f() finishes. That's fine; we'll just have to discard // the value computed here and accept a bit of wasted computation. + + // TODO: on the freethreaded build, consider wrapping this pair of operations in a + // critical section (requires a critical section API which can use a PyMutex without + // an object.) let value = f()?; let _ = self.set(py, value); @@ -172,7 +227,12 @@ 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> { - self.0.get_mut().as_mut() + if self.once.is_completed() { + // SAFETY: the cell has been written. + Some(unsafe { (*self.data.get()).assume_init_mut() }) + } else { + None + } } /// Set the value in the cell. @@ -180,37 +240,64 @@ impl GILOnceCell { /// If the cell has already been written, `Err(value)` will be returned containing the new /// value which was not written. pub fn set(&self, _py: Python<'_>, value: T) -> Result<(), T> { - // Safe because GIL is held, so no other thread can be writing to this cell concurrently. - let inner = unsafe { &mut *self.0.get() }; - if inner.is_some() { - return Err(value); - } + let mut value = Some(value); + // NB this can block, but since this is only writing a single value and + // does not call arbitrary python code, we don't need to worry about + // deadlocks with the GIL. + self.once.call_once_force(|_| { + // SAFETY: no other threads can be writing this value, because we are + // inside the `call_once_force` closure. + unsafe { + // `.take().unwrap()` will never panic + (*self.data.get()).write(value.take().unwrap()); + } + }); - *inner = Some(value); - Ok(()) + match value { + // Some other thread wrote to the cell first + Some(value) => Err(value), + None => 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() + if self.once.is_completed() { + // Reset the cell to its default state so that it won't try to + // drop the value again. + self.once = Once::new(); + // SAFETY: the cell has been written. `self.once` has been reset, + // so when `self` is dropped the value won't be read again. + Some(unsafe { self.data.get_mut().assume_init_read() }) + } else { + None + } } /// 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() + pub fn into_inner(mut self) -> Option { + self.take() } } impl GILOnceCell> { - /// Create a new cell that contains a new Python reference to the same contained object. + /// Creates a new cell that contains a new Python reference to the same contained object. /// - /// Returns an uninitialised cell if `self` has not yet been initialised. + /// Returns an uninitialized cell if `self` has not yet been initialized. pub fn clone_ref(&self, py: Python<'_>) -> Self { - Self(UnsafeCell::new(self.get(py).map(|ob| ob.clone_ref(py)))) + let cloned = Self { + once: Once::new(), + data: UnsafeCell::new(MaybeUninit::uninit()), + _marker: PhantomData, + }; + if let Some(value) = self.get(py) { + let _ = cloned.set(py, value.clone_ref(py)); + } + cloned } } @@ -266,6 +353,15 @@ where } } +impl Drop for GILOnceCell { + fn drop(&mut self) { + if self.once.is_completed() { + // SAFETY: the cell has been written. + unsafe { MaybeUninit::assume_init_drop(self.data.get_mut()) } + } + } +} + /// Interns `text` as a Python string and stores a reference to it in static storage. /// /// A reference to the same Python string is returned on each invocation. @@ -429,6 +525,29 @@ mod tests { }) } + #[test] + fn test_once_cell_drop() { + #[derive(Debug)] + struct RecordDrop<'a>(&'a mut bool); + + impl Drop for RecordDrop<'_> { + fn drop(&mut self) { + *self.0 = true; + } + } + + Python::with_gil(|py| { + let mut dropped = false; + let cell = GILOnceCell::new(); + cell.set(py, RecordDrop(&mut dropped)).unwrap(); + let drop_container = cell.get(py).unwrap(); + + assert!(!*drop_container.0); + drop(cell); + assert!(dropped); + }); + } + #[cfg(feature = "macros")] #[cfg(not(target_arch = "wasm32"))] // We are building wasm Python with pthreads disabled #[test] From 96da64bc5eb05e55332fd7bb381171d4d69971bb Mon Sep 17 00:00:00 2001 From: Cheuk Ting Ho Date: Wed, 9 Oct 2024 18:30:37 +0100 Subject: [PATCH 309/495] seal PyAddToModule (#4606) * seal PyAddToModule * adding change log * fix changelog name --- newsfragments/4606.changed.md | 1 + src/impl_/pymodule.rs | 2 +- src/sealed.rs | 10 ++++++++++ 3 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 newsfragments/4606.changed.md diff --git a/newsfragments/4606.changed.md b/newsfragments/4606.changed.md new file mode 100644 index 00000000000..173252992c1 --- /dev/null +++ b/newsfragments/4606.changed.md @@ -0,0 +1 @@ +Seal `PyAddToModule` trait. diff --git a/src/impl_/pymodule.rs b/src/impl_/pymodule.rs index 97eb2103dfe..da17fe4bbdc 100644 --- a/src/impl_/pymodule.rs +++ b/src/impl_/pymodule.rs @@ -144,7 +144,7 @@ impl ModuleDef { /// Trait to add an element (class, function...) to a module. /// /// Currently only implemented for classes. -pub trait PyAddToModule { +pub trait PyAddToModule: crate::sealed::Sealed { fn add_to_module(&'static self, module: &Bound<'_, PyModule>) -> PyResult<()>; } diff --git a/src/sealed.rs b/src/sealed.rs index 20f31f82e01..fef7a02aca7 100644 --- a/src/sealed.rs +++ b/src/sealed.rs @@ -5,6 +5,11 @@ use crate::types::{ }; use crate::{ffi, Bound, PyAny, PyResult}; +use crate::impl_::{ + pymethods::PyMethodDef, + pymodule::{AddClassToModule, AddTypeToModule, ModuleDef}, +}; + pub trait Sealed {} // for FfiPtrExt @@ -36,3 +41,8 @@ impl Sealed for Bound<'_, PyType> {} impl Sealed for Bound<'_, PyWeakref> {} impl Sealed for Bound<'_, PyWeakrefProxy> {} impl Sealed for Bound<'_, PyWeakrefReference> {} + +impl Sealed for AddTypeToModule {} +impl Sealed for AddClassToModule {} +impl Sealed for PyMethodDef {} +impl Sealed for ModuleDef {} From eacebb8db101d05ae4ccf0396e314b098455ecd3 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Thu, 10 Oct 2024 09:44:07 +0100 Subject: [PATCH 310/495] ci: fix more ubuntu-24.04 failures (#4610) * ci: fix more ubuntu-24.04 failures * use 22.04 to test 3.7 * disable zoneinfo test on free-threading --- .github/workflows/ci.yml | 18 ++++++++++++++++++ src/conversions/chrono_tz.rs | 1 + 2 files changed, 19 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f53f2c31292..bfe2c0f6b39 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -312,6 +312,15 @@ jobs: python-architecture: "x64", rust-target: "x86_64-apple-darwin", } + # ubuntu-latest (24.04) no longer supports 3.7, so run on 22.04 + - rust: stable + python-version: "3.7" + platform: + { + os: "ubuntu-22.04", + python-architecture: "x64", + rust-target: "x86_64-unknown-linux-gnu", + } # arm64 macOS Python not available on GitHub Actions until 3.10 # so backfill 3.7-3.9 with x64 macOS runners @@ -341,6 +350,9 @@ jobs: } exclude: + # ubuntu-latest (24.04) no longer supports 3.7 + - python-version: "3.7" + platform: { os: "ubuntu-latest" } # arm64 macOS Python not available on GitHub Actions until 3.10 - rust: stable python-version: "3.7" @@ -572,6 +584,9 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.12' - uses: Swatinem/rust-cache@v2 with: save-if: ${{ github.event_name != 'merge_group' }} @@ -641,6 +656,9 @@ jobs: target: "x86_64-apple-darwin" steps: - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.12' - uses: Swatinem/rust-cache@v2 with: workspaces: diff --git a/src/conversions/chrono_tz.rs b/src/conversions/chrono_tz.rs index f766e9ec5c0..8d324bfcacb 100644 --- a/src/conversions/chrono_tz.rs +++ b/src/conversions/chrono_tz.rs @@ -114,6 +114,7 @@ mod tests { } #[test] + #[cfg(not(Py_GIL_DISABLED))] // https://github.com/python/cpython/issues/116738#issuecomment-2404360445 fn test_into_pyobject() { Python::with_gil(|py| { let assert_eq = |l: Bound<'_, PyAny>, r: Bound<'_, PyAny>| { From 2125c0ece47d82fe101fbe9080d2a2dcb09f6741 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Thu, 10 Oct 2024 10:22:14 +0100 Subject: [PATCH 311/495] ci: run benchmarks on ubuntu 22.04 (#4609) --- .github/workflows/benches.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/benches.yml b/.github/workflows/benches.yml index 19a10adaa40..97b882dc858 100644 --- a/.github/workflows/benches.yml +++ b/.github/workflows/benches.yml @@ -15,7 +15,8 @@ concurrency: jobs: benchmarks: - runs-on: ubuntu-latest + # No support for 24.04, see https://github.com/CodSpeedHQ/runner/issues/42 + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 From 446676d6e48e51a000a186ad7a75abd90f96b81b Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Thu, 10 Oct 2024 10:22:48 +0100 Subject: [PATCH 312/495] deprecate `PyWeakRefMethods::get_object` (#4597) * deprecate `PyWeakRefMethods::get_object` * newsfragment * fixup example --- newsfragments/4597.changed.md | 1 + src/types/weakref/anyref.rs | 36 ++++++++++++++++++---------------- src/types/weakref/proxy.rs | 33 ++++++++++++++++--------------- src/types/weakref/reference.rs | 20 +++++++++---------- 4 files changed, 47 insertions(+), 43 deletions(-) create mode 100644 newsfragments/4597.changed.md diff --git a/newsfragments/4597.changed.md b/newsfragments/4597.changed.md new file mode 100644 index 00000000000..7ec760451bd --- /dev/null +++ b/newsfragments/4597.changed.md @@ -0,0 +1 @@ +Deprecate `PyWeakrefMethods::get_option`. diff --git a/src/types/weakref/anyref.rs b/src/types/weakref/anyref.rs index ffbc86da98c..d5af956ac9e 100644 --- a/src/types/weakref/anyref.rs +++ b/src/types/weakref/anyref.rs @@ -5,7 +5,7 @@ use crate::types::{ any::{PyAny, PyAnyMethods}, PyNone, }; -use crate::{ffi, Bound}; +use crate::{ffi, Bound, Python}; /// Represents any Python `weakref` reference. /// @@ -315,20 +315,12 @@ pub trait PyWeakrefMethods<'py>: crate::sealed::Sealed { /// /// # Panics /// This function panics is the current object is invalid. - /// If used propperly this is never the case. (NonNull and actually a weakref type) + /// If used properly this is never the case. (NonNull and actually a weakref type) /// /// [`PyWeakref_GetRef`]: https://docs.python.org/3/c-api/weakref.html#c.PyWeakref_GetRef /// [`weakref.ReferenceType`]: https://docs.python.org/3/library/weakref.html#weakref.ReferenceType /// [`weakref.ref`]: https://docs.python.org/3/library/weakref.html#weakref.ref - fn upgrade(&self) -> Option> { - let object = self.get_object(); - - if object.is_none() { - None - } else { - Some(object) - } - } + fn upgrade(&self) -> Option>; /// Retrieve to a Bound object pointed to by the weakref. /// @@ -346,6 +338,7 @@ pub trait PyWeakrefMethods<'py>: crate::sealed::Sealed { all(feature = "macros", not(all(Py_LIMITED_API, not(Py_3_9)))), doc = "```rust" )] + /// #![allow(deprecated)] /// use pyo3::prelude::*; /// use pyo3::types::PyWeakrefReference; /// @@ -386,18 +379,25 @@ pub trait PyWeakrefMethods<'py>: crate::sealed::Sealed { /// [`PyWeakref_GetRef`]: https://docs.python.org/3/c-api/weakref.html#c.PyWeakref_GetRef /// [`weakref.ReferenceType`]: https://docs.python.org/3/library/weakref.html#weakref.ReferenceType /// [`weakref.ref`]: https://docs.python.org/3/library/weakref.html#weakref.ref - fn get_object(&self) -> Bound<'py, PyAny>; + #[deprecated(since = "0.23.0", note = "Use `upgrade` instead")] + fn get_object(&self) -> Bound<'py, PyAny> { + self.upgrade().unwrap_or_else(|| { + // Safety: upgrade() returns `Bound<'py, PyAny>` with a lifetime `'py` if it exists, we + // can safely assume the same lifetime here. + PyNone::get(unsafe { Python::assume_gil_acquired() }) + .to_owned() + .into_any() + }) + } } impl<'py> PyWeakrefMethods<'py> for Bound<'py, PyWeakref> { - fn get_object(&self) -> Bound<'py, PyAny> { + fn upgrade(&self) -> Option> { let mut obj: *mut ffi::PyObject = std::ptr::null_mut(); match unsafe { ffi::compat::PyWeakref_GetRef(self.as_ptr(), &mut obj) } { std::os::raw::c_int::MIN..=-1 => panic!("The 'weakref' weak reference instance should be valid (non-null and actually a weakref reference)"), - 0 => PyNone::get(self.py()).to_owned().into_any(), - // Safety: positive return value from `PyWeakRef_GetRef` guarantees the return value is - // a valid strong reference. - 1..=std::os::raw::c_int::MAX => unsafe { obj.assume_owned_unchecked(self.py()) }, + 0 => None, + 1..=std::os::raw::c_int::MAX => Some(unsafe { obj.assume_owned_unchecked(self.py()) }), } } } @@ -547,6 +547,7 @@ mod tests { } #[test] + #[allow(deprecated)] fn test_weakref_get_object() -> PyResult<()> { fn inner( create_reference: impl for<'py> FnOnce( @@ -698,6 +699,7 @@ mod tests { } #[test] + #[allow(deprecated)] fn test_weakref_get_object() -> PyResult<()> { fn inner( create_reference: impl for<'py> FnOnce( diff --git a/src/types/weakref/proxy.rs b/src/types/weakref/proxy.rs index bf9d69295d2..6b20a29b8c2 100644 --- a/src/types/weakref/proxy.rs +++ b/src/types/weakref/proxy.rs @@ -2,7 +2,7 @@ use crate::err::PyResult; use crate::ffi_ptr_ext::FfiPtrExt; use crate::py_result_ext::PyResultExt; use crate::type_object::PyTypeCheck; -use crate::types::{any::PyAny, PyNone}; +use crate::types::any::PyAny; use crate::{ffi, Bound, BoundObject, IntoPyObject}; use super::PyWeakrefMethods; @@ -190,14 +190,12 @@ impl PyWeakrefProxy { } impl<'py> PyWeakrefMethods<'py> for Bound<'py, PyWeakrefProxy> { - fn get_object(&self) -> Bound<'py, PyAny> { + fn upgrade(&self) -> Option> { let mut obj: *mut ffi::PyObject = std::ptr::null_mut(); match unsafe { ffi::compat::PyWeakref_GetRef(self.as_ptr(), &mut obj) } { std::os::raw::c_int::MIN..=-1 => panic!("The 'weakref.ProxyType' (or `weakref.CallableProxyType`) instance should be valid (non-null and actually a weakref reference)"), - 0 => PyNone::get(self.py()).to_owned().into_any(), - // Safety: positive return value from `PyWeakRef_GetRef` guarantees the return value is - // a valid strong reference. - 1..=std::os::raw::c_int::MAX => unsafe { obj.assume_owned_unchecked(self.py()) }, + 0 => None, + 1..=std::os::raw::c_int::MAX => Some(unsafe { obj.assume_owned_unchecked(self.py()) }), } } } @@ -283,7 +281,7 @@ mod tests { let reference = PyWeakrefProxy::new(&object)?; assert!(!reference.is(&object)); - assert!(reference.get_object().is(&object)); + assert!(reference.upgrade().unwrap().is(&object)); #[cfg(not(Py_LIMITED_API))] assert_eq!( @@ -311,7 +309,7 @@ mod tests { drop(object); - assert!(reference.get_object().is_none()); + assert!(reference.upgrade().is_none()); assert!(reference .getattr("__class__") .err() @@ -426,11 +424,11 @@ mod tests { let object = class.call0()?; let reference = PyWeakrefProxy::new(&object)?; - assert!(reference.get_object().is(&object)); + assert!(reference.upgrade().unwrap().is(&object)); drop(object); - assert!(reference.get_object().is_none()); + assert!(reference.upgrade().is_none()); Ok(()) }) @@ -454,7 +452,7 @@ mod tests { let reference = PyWeakrefProxy::new(&object)?; assert!(!reference.is(&object)); - assert!(reference.get_object().is(&object)); + assert!(reference.upgrade().unwrap().is(&object)); #[cfg(not(Py_LIMITED_API))] assert_eq!( reference.get_type().to_string(), @@ -484,7 +482,7 @@ mod tests { drop(object); - assert!(reference.get_object().is_none()); + assert!(reference.upgrade().is_none()); assert!(reference .getattr("__class__") .err() @@ -584,6 +582,7 @@ mod tests { } #[test] + #[allow(deprecated)] fn test_weakref_get_object() -> PyResult<()> { Python::with_gil(|py| { let object = Py::new(py, WeakrefablePyClass {})?; @@ -633,7 +632,7 @@ mod tests { let reference = PyWeakrefProxy::new(&object)?; assert!(!reference.is(&object)); - assert!(reference.get_object().is(&object)); + assert!(reference.upgrade().unwrap().is(&object)); #[cfg(not(Py_LIMITED_API))] assert_eq!(reference.get_type().to_string(), CLASS_NAME); @@ -650,7 +649,7 @@ mod tests { drop(object); - assert!(reference.get_object().is_none()); + assert!(reference.upgrade().is_none()); assert!(reference .getattr("__class__") .err() @@ -757,6 +756,7 @@ mod tests { } #[test] + #[allow(deprecated)] fn test_weakref_get_object() -> PyResult<()> { Python::with_gil(|py| { let class = get_type(py)?; @@ -798,7 +798,7 @@ mod tests { let reference = PyWeakrefProxy::new(&object)?; assert!(!reference.is(&object)); - assert!(reference.get_object().is(&object)); + assert!(reference.upgrade().unwrap().is(&object)); #[cfg(not(Py_LIMITED_API))] assert_eq!(reference.get_type().to_string(), CLASS_NAME); @@ -818,7 +818,7 @@ mod tests { drop(object); - assert!(reference.get_object().is_none()); + assert!(reference.upgrade().is_none()); assert!(reference .getattr("__class__") .err() @@ -916,6 +916,7 @@ mod tests { } #[test] + #[allow(deprecated)] fn test_weakref_get_object() -> PyResult<()> { Python::with_gil(|py| { let object = Py::new(py, WeakrefablePyClass {})?; diff --git a/src/types/weakref/reference.rs b/src/types/weakref/reference.rs index f26df4953ea..dc7ea4a272a 100644 --- a/src/types/weakref/reference.rs +++ b/src/types/weakref/reference.rs @@ -1,7 +1,7 @@ use crate::err::PyResult; use crate::ffi_ptr_ext::FfiPtrExt; use crate::py_result_ext::PyResultExt; -use crate::types::{any::PyAny, PyNone}; +use crate::types::any::PyAny; use crate::{ffi, Bound, BoundObject, IntoPyObject}; #[cfg(any(PyPy, GraalPy, Py_LIMITED_API))] @@ -199,14 +199,12 @@ impl PyWeakrefReference { } impl<'py> PyWeakrefMethods<'py> for Bound<'py, PyWeakrefReference> { - fn get_object(&self) -> Bound<'py, PyAny> { + fn upgrade(&self) -> Option> { let mut obj: *mut ffi::PyObject = std::ptr::null_mut(); match unsafe { ffi::compat::PyWeakref_GetRef(self.as_ptr(), &mut obj) } { std::os::raw::c_int::MIN..=-1 => panic!("The 'weakref.ReferenceType' instance should be valid (non-null and actually a weakref reference)"), - 0 => PyNone::get(self.py()).to_owned().into_any(), - // Safety: positive return value from `PyWeakRef_GetRef` guarantees the return value is - // a valid strong reference. - 1..=std::os::raw::c_int::MAX => unsafe { obj.assume_owned_unchecked(self.py()) }, + 0 => None, + 1..=std::os::raw::c_int::MAX => Some(unsafe { obj.assume_owned_unchecked(self.py()) }), } } } @@ -278,7 +276,7 @@ mod tests { let reference = PyWeakrefReference::new(&object)?; assert!(!reference.is(&object)); - assert!(reference.get_object().is(&object)); + assert!(reference.upgrade().unwrap().is(&object)); #[cfg(not(Py_LIMITED_API))] assert_eq!(reference.get_type().to_string(), CLASS_NAME); @@ -297,7 +295,7 @@ mod tests { drop(object); - assert!(reference.get_object().is_none()); + assert!(reference.upgrade().is_none()); #[cfg(not(Py_LIMITED_API))] assert_eq!(reference.getattr("__class__")?.to_string(), CLASS_NAME); check_repr(&reference, None)?; @@ -397,6 +395,7 @@ mod tests { } #[test] + #[allow(deprecated)] fn test_weakref_get_object() -> PyResult<()> { Python::with_gil(|py| { let class = get_type(py)?; @@ -433,7 +432,7 @@ mod tests { let reference = PyWeakrefReference::new(&object)?; assert!(!reference.is(&object)); - assert!(reference.get_object().is(&object)); + assert!(reference.upgrade().unwrap().is(&object)); #[cfg(not(Py_LIMITED_API))] assert_eq!(reference.get_type().to_string(), CLASS_NAME); @@ -450,7 +449,7 @@ mod tests { drop(object); - assert!(reference.get_object().is_none()); + assert!(reference.upgrade().is_none()); #[cfg(not(Py_LIMITED_API))] assert_eq!(reference.getattr("__class__")?.to_string(), CLASS_NAME); check_repr(&reference, None)?; @@ -541,6 +540,7 @@ mod tests { } #[test] + #[allow(deprecated)] fn test_weakref_get_object() -> PyResult<()> { Python::with_gil(|py| { let object = Py::new(py, WeakrefablePyClass {})?; From 50a254dd8431c2eb680184898db3997c0c2cd77e Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Thu, 10 Oct 2024 19:14:52 +0200 Subject: [PATCH 313/495] migrate `IntoPyCallbackOutput` to use `IntoPyObject` (#4607) --- src/callback.rs | 55 +++++++++++++++++++++++------------------- src/impl_/pymethods.rs | 32 ++++++++++++------------ src/pyclass_init.rs | 2 +- src/types/function.rs | 6 ++--- src/types/module.rs | 4 +-- 5 files changed, 52 insertions(+), 47 deletions(-) diff --git a/src/callback.rs b/src/callback.rs index 1e446039904..9f06340bc59 100644 --- a/src/callback.rs +++ b/src/callback.rs @@ -3,7 +3,7 @@ use crate::err::{PyErr, PyResult}; use crate::exceptions::PyOverflowError; use crate::ffi::{self, Py_hash_t}; -use crate::{IntoPy, PyObject, Python}; +use crate::{BoundObject, IntoPyObject, PyObject, Python}; use std::os::raw::c_int; /// A type which can be the return type of a python C-API callback @@ -25,17 +25,17 @@ impl PyCallbackOutput for ffi::Py_ssize_t { } /// Convert the result of callback function into the appropriate return value. -pub trait IntoPyCallbackOutput { - fn convert(self, py: Python<'_>) -> PyResult; +pub trait IntoPyCallbackOutput<'py, Target> { + fn convert(self, py: Python<'py>) -> PyResult; } -impl IntoPyCallbackOutput for Result +impl<'py, T, E, U> IntoPyCallbackOutput<'py, U> for Result where - T: IntoPyCallbackOutput, + T: IntoPyCallbackOutput<'py, U>, E: Into, { #[inline] - fn convert(self, py: Python<'_>) -> PyResult { + fn convert(self, py: Python<'py>) -> PyResult { match self { Ok(v) => v.convert(py), Err(e) => Err(e.into()), @@ -43,45 +43,47 @@ where } } -impl IntoPyCallbackOutput<*mut ffi::PyObject> for T +impl<'py, T> IntoPyCallbackOutput<'py, *mut ffi::PyObject> for T where - T: IntoPy, + T: IntoPyObject<'py>, { #[inline] - fn convert(self, py: Python<'_>) -> PyResult<*mut ffi::PyObject> { - Ok(self.into_py(py).into_ptr()) + fn convert(self, py: Python<'py>) -> PyResult<*mut ffi::PyObject> { + self.into_pyobject(py) + .map(BoundObject::into_ptr) + .map_err(Into::into) } } -impl IntoPyCallbackOutput for *mut ffi::PyObject { +impl IntoPyCallbackOutput<'_, Self> for *mut ffi::PyObject { #[inline] fn convert(self, _: Python<'_>) -> PyResult { Ok(self) } } -impl IntoPyCallbackOutput for () { +impl IntoPyCallbackOutput<'_, std::os::raw::c_int> for () { #[inline] fn convert(self, _: Python<'_>) -> PyResult { Ok(0) } } -impl IntoPyCallbackOutput for bool { +impl IntoPyCallbackOutput<'_, std::os::raw::c_int> for bool { #[inline] fn convert(self, _: Python<'_>) -> PyResult { Ok(self as c_int) } } -impl IntoPyCallbackOutput<()> for () { +impl IntoPyCallbackOutput<'_, ()> for () { #[inline] fn convert(self, _: Python<'_>) -> PyResult<()> { Ok(()) } } -impl IntoPyCallbackOutput for usize { +impl IntoPyCallbackOutput<'_, ffi::Py_ssize_t> for usize { #[inline] fn convert(self, _py: Python<'_>) -> PyResult { self.try_into().map_err(|_err| PyOverflowError::new_err(())) @@ -90,27 +92,30 @@ impl IntoPyCallbackOutput for usize { // Converters needed for `#[pyproto]` implementations -impl IntoPyCallbackOutput for bool { +impl IntoPyCallbackOutput<'_, bool> for bool { #[inline] fn convert(self, _: Python<'_>) -> PyResult { Ok(self) } } -impl IntoPyCallbackOutput for usize { +impl IntoPyCallbackOutput<'_, usize> for usize { #[inline] fn convert(self, _: Python<'_>) -> PyResult { Ok(self) } } -impl IntoPyCallbackOutput for T +impl<'py, T> IntoPyCallbackOutput<'py, PyObject> for T where - T: IntoPy, + T: IntoPyObject<'py>, { #[inline] - fn convert(self, py: Python<'_>) -> PyResult { - Ok(self.into_py(py)) + fn convert(self, py: Python<'py>) -> PyResult { + self.into_pyobject(py) + .map(BoundObject::into_any) + .map(BoundObject::unbind) + .map_err(Into::into) } } @@ -141,7 +146,7 @@ wrapping_cast!(i64, Py_hash_t); pub struct HashCallbackOutput(Py_hash_t); -impl IntoPyCallbackOutput for HashCallbackOutput { +impl IntoPyCallbackOutput<'_, Py_hash_t> for HashCallbackOutput { #[inline] fn convert(self, _py: Python<'_>) -> PyResult { let hash = self.0; @@ -153,7 +158,7 @@ impl IntoPyCallbackOutput for HashCallbackOutput { } } -impl IntoPyCallbackOutput for T +impl IntoPyCallbackOutput<'_, HashCallbackOutput> for T where T: WrappingCastTo, { @@ -165,9 +170,9 @@ where #[doc(hidden)] #[inline] -pub fn convert(py: Python<'_>, value: T) -> PyResult +pub fn convert<'py, T, U>(py: Python<'py>, value: T) -> PyResult where - T: IntoPyCallbackOutput, + T: IntoPyCallbackOutput<'py, U>, { value.convert(py) } diff --git a/src/impl_/pymethods.rs b/src/impl_/pymethods.rs index 617bad52ea4..c4d09a8fdd3 100644 --- a/src/impl_/pymethods.rs +++ b/src/impl_/pymethods.rs @@ -334,9 +334,9 @@ pub struct IterBaseTag; impl IterBaseTag { #[inline] - pub fn convert(self, py: Python<'_>, value: Value) -> PyResult + pub fn convert<'py, Value, Target>(self, py: Python<'py>, value: Value) -> PyResult where - Value: IntoPyCallbackOutput, + Value: IntoPyCallbackOutput<'py, Target>, { value.convert(py) } @@ -355,13 +355,13 @@ pub struct IterOptionTag; impl IterOptionTag { #[inline] - pub fn convert( + pub fn convert<'py, Value>( self, - py: Python<'_>, + py: Python<'py>, value: Option, ) -> PyResult<*mut ffi::PyObject> where - Value: IntoPyCallbackOutput<*mut ffi::PyObject>, + Value: IntoPyCallbackOutput<'py, *mut ffi::PyObject>, { match value { Some(value) => value.convert(py), @@ -383,13 +383,13 @@ pub struct IterResultOptionTag; impl IterResultOptionTag { #[inline] - pub fn convert( + pub fn convert<'py, Value, Error>( self, - py: Python<'_>, + py: Python<'py>, value: Result, Error>, ) -> PyResult<*mut ffi::PyObject> where - Value: IntoPyCallbackOutput<*mut ffi::PyObject>, + Value: IntoPyCallbackOutput<'py, *mut ffi::PyObject>, Error: Into, { match value { @@ -415,9 +415,9 @@ pub struct AsyncIterBaseTag; impl AsyncIterBaseTag { #[inline] - pub fn convert(self, py: Python<'_>, value: Value) -> PyResult + pub fn convert<'py, Value, Target>(self, py: Python<'py>, value: Value) -> PyResult where - Value: IntoPyCallbackOutput, + Value: IntoPyCallbackOutput<'py, Target>, { value.convert(py) } @@ -436,13 +436,13 @@ pub struct AsyncIterOptionTag; impl AsyncIterOptionTag { #[inline] - pub fn convert( + pub fn convert<'py, Value>( self, - py: Python<'_>, + py: Python<'py>, value: Option, ) -> PyResult<*mut ffi::PyObject> where - Value: IntoPyCallbackOutput<*mut ffi::PyObject>, + Value: IntoPyCallbackOutput<'py, *mut ffi::PyObject>, { match value { Some(value) => value.convert(py), @@ -464,13 +464,13 @@ pub struct AsyncIterResultOptionTag; impl AsyncIterResultOptionTag { #[inline] - pub fn convert( + pub fn convert<'py, Value, Error>( self, - py: Python<'_>, + py: Python<'py>, value: Result, Error>, ) -> PyResult<*mut ffi::PyObject> where - Value: IntoPyCallbackOutput<*mut ffi::PyObject>, + Value: IntoPyCallbackOutput<'py, *mut ffi::PyObject>, Error: Into, { match value { diff --git a/src/pyclass_init.rs b/src/pyclass_init.rs index 1d669ea1554..77976321adf 100644 --- a/src/pyclass_init.rs +++ b/src/pyclass_init.rs @@ -344,7 +344,7 @@ impl<'py, T: PyClass> From> for PyClassInitializer { // Implementation used by proc macros to allow anything convertible to PyClassInitializer to be // the return value of pyclass #[new] method (optionally wrapped in `Result`). -impl IntoPyCallbackOutput> for U +impl IntoPyCallbackOutput<'_, PyClassInitializer> for U where T: PyClass, U: Into>, diff --git a/src/types/function.rs b/src/types/function.rs index 936176add22..fdc4bc9e7bf 100644 --- a/src/types/function.rs +++ b/src/types/function.rs @@ -104,7 +104,7 @@ impl PyCFunction { ) -> PyResult> where F: Fn(&Bound<'_, PyTuple>, Option<&Bound<'_, PyDict>>) -> R + Send + 'static, - R: crate::callback::IntoPyCallbackOutput<*mut ffi::PyObject>, + for<'p> R: crate::callback::IntoPyCallbackOutput<'p, *mut ffi::PyObject>, { let name = name.unwrap_or(ffi::c_str!("pyo3-closure")); let doc = doc.unwrap_or(ffi::c_str!("")); @@ -142,7 +142,7 @@ impl PyCFunction { ) -> PyResult> where F: Fn(&Bound<'_, PyTuple>, Option<&Bound<'_, PyDict>>) -> R + Send + 'static, - R: crate::callback::IntoPyCallbackOutput<*mut ffi::PyObject>, + for<'p> R: crate::callback::IntoPyCallbackOutput<'p, *mut ffi::PyObject>, { Self::new_closure(py, name, doc, closure) } @@ -185,7 +185,7 @@ unsafe extern "C" fn run_closure( ) -> *mut ffi::PyObject where F: Fn(&Bound<'_, PyTuple>, Option<&Bound<'_, PyDict>>) -> R + Send + 'static, - R: crate::callback::IntoPyCallbackOutput<*mut ffi::PyObject>, + for<'py> R: crate::callback::IntoPyCallbackOutput<'py, *mut ffi::PyObject>, { use crate::types::any::PyAnyMethods; diff --git a/src/types/module.rs b/src/types/module.rs index 4a81a4806fa..a0398c42be4 100644 --- a/src/types/module.rs +++ b/src/types/module.rs @@ -305,7 +305,7 @@ pub trait PyModuleMethods<'py>: crate::sealed::Sealed { /// instead. fn add_wrapped(&self, wrapper: &impl Fn(Python<'py>) -> T) -> PyResult<()> where - T: IntoPyCallbackOutput; + T: IntoPyCallbackOutput<'py, PyObject>; /// Adds a submodule to a module. /// @@ -490,7 +490,7 @@ impl<'py> PyModuleMethods<'py> for Bound<'py, PyModule> { fn add_wrapped(&self, wrapper: &impl Fn(Python<'py>) -> T) -> PyResult<()> where - T: IntoPyCallbackOutput, + T: IntoPyCallbackOutput<'py, PyObject>, { fn inner(module: &Bound<'_, PyModule>, object: Bound<'_, PyAny>) -> PyResult<()> { let name = object.getattr(__name__(module.py()))?; From d9258212cb0cab7deb897415c138f9d7f24dad96 Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Fri, 11 Oct 2024 10:08:52 +0200 Subject: [PATCH 314/495] move `callback` module into `impl_` (#4613) --- pyo3-macros-backend/src/method.rs | 4 ++-- pyo3-macros-backend/src/pymethod.rs | 14 +++++++------- src/impl_.rs | 1 + src/{ => impl_}/callback.rs | 0 src/impl_/pyclass.rs | 2 +- src/impl_/pymethods.rs | 2 +- src/impl_/trampoline.rs | 2 +- src/lib.rs | 2 -- src/pycell.rs | 2 +- src/pyclass_init.rs | 2 +- src/types/function.rs | 8 ++++---- src/types/module.rs | 2 +- 12 files changed, 20 insertions(+), 21 deletions(-) rename src/{ => impl_}/callback.rs (100%) diff --git a/pyo3-macros-backend/src/method.rs b/pyo3-macros-backend/src/method.rs index 633083dea95..50f70d8440a 100644 --- a/pyo3-macros-backend/src/method.rs +++ b/pyo3-macros-backend/src/method.rs @@ -312,7 +312,7 @@ impl ExtractErrorMode { ExtractErrorMode::NotImplemented => quote! { match #extract { ::std::result::Result::Ok(value) => value, - ::std::result::Result::Err(_) => { return #pyo3_path::callback::convert(py, py.NotImplemented()); }, + ::std::result::Result::Err(_) => { return #pyo3_path::impl_::callback::convert(py, py.NotImplemented()); }, } }, } @@ -836,7 +836,7 @@ impl<'a> FnSpec<'a> { _args: *mut #pyo3_path::ffi::PyObject, _kwargs: *mut #pyo3_path::ffi::PyObject ) -> #pyo3_path::PyResult<*mut #pyo3_path::ffi::PyObject> { - use #pyo3_path::callback::IntoPyCallbackOutput; + use #pyo3_path::impl_::callback::IntoPyCallbackOutput; #deprecation let _slf_ref = &_slf; let function = #rust_name; // Shadow the function name to avoid #3017 diff --git a/pyo3-macros-backend/src/pymethod.rs b/pyo3-macros-backend/src/pymethod.rs index e7e50145e04..ec8b264884d 100644 --- a/pyo3-macros-backend/src/pymethod.rs +++ b/pyo3-macros-backend/src/pymethod.rs @@ -684,7 +684,7 @@ pub fn impl_py_setter_def( #init_holders #extract let result = #setter_impl; - #pyo3_path::callback::convert(py, result) + #pyo3_path::impl_::callback::convert(py, result) } }; @@ -813,7 +813,7 @@ pub fn impl_py_getter_def( let wrapper_ident = format_ident!("__pymethod_get_{}__", spec.name); let call = impl_call_getter(cls, spec, self_type, &mut holders, ctx)?; let body = quote! { - #pyo3_path::callback::convert(py, #call) + #pyo3_path::impl_::callback::convert(py, #call) }; let init_holders = holders.init_holders(ctx); @@ -916,7 +916,7 @@ pub const __REPR__: SlotDef = SlotDef::new("Py_tp_repr", "reprfunc"); pub const __HASH__: SlotDef = SlotDef::new("Py_tp_hash", "hashfunc") .ret_ty(Ty::PyHashT) .return_conversion(TokenGenerator( - |Ctx { pyo3_path, .. }: &Ctx| quote! { #pyo3_path::callback::HashCallbackOutput }, + |Ctx { pyo3_path, .. }: &Ctx| quote! { #pyo3_path::impl_::callback::HashCallbackOutput }, )); pub const __RICHCMP__: SlotDef = SlotDef::new("Py_tp_richcompare", "richcmpfunc") .extract_error_mode(ExtractErrorMode::NotImplemented) @@ -1169,8 +1169,8 @@ impl ReturnMode { ReturnMode::Conversion(conversion) => { let conversion = TokenGeneratorCtx(*conversion, ctx); quote! { - let _result: #pyo3_path::PyResult<#conversion> = #pyo3_path::callback::convert(py, #call); - #pyo3_path::callback::convert(py, _result) + let _result: #pyo3_path::PyResult<#conversion> = #pyo3_path::impl_::callback::convert(py, #call); + #pyo3_path::impl_::callback::convert(py, _result) } } ReturnMode::SpecializedConversion(traits, tag) => { @@ -1183,7 +1183,7 @@ impl ReturnMode { } } ReturnMode::ReturnSelf => quote! { - let _result: #pyo3_path::PyResult<()> = #pyo3_path::callback::convert(py, #call); + let _result: #pyo3_path::PyResult<()> = #pyo3_path::impl_::callback::convert(py, #call); _result?; #pyo3_path::ffi::Py_XINCREF(_raw_slf); ::std::result::Result::Ok(_raw_slf) @@ -1356,7 +1356,7 @@ fn generate_method_body( } else { quote! { let result = #call; - #pyo3_path::callback::convert(py, result) + #pyo3_path::impl_::callback::convert(py, result) } }) } diff --git a/src/impl_.rs b/src/impl_.rs index 5bfeda39f65..d6b918c6820 100644 --- a/src/impl_.rs +++ b/src/impl_.rs @@ -6,6 +6,7 @@ //! APIs may may change at any time without documentation in the CHANGELOG and without //! breaking semver guarantees. +pub mod callback; #[cfg(feature = "experimental-async")] pub mod coroutine; pub mod exceptions; diff --git a/src/callback.rs b/src/impl_/callback.rs similarity index 100% rename from src/callback.rs rename to src/impl_/callback.rs diff --git a/src/impl_/pyclass.rs b/src/impl_/pyclass.rs index 4e3ed140a76..01b824c69ab 100644 --- a/src/impl_/pyclass.rs +++ b/src/impl_/pyclass.rs @@ -446,7 +446,7 @@ macro_rules! define_pyclass_setattr_slot { value, |py, _slf, attr, value| { use ::std::option::Option::*; - use $crate::callback::IntoPyCallbackOutput; + use $crate::impl_::callback::IntoPyCallbackOutput; use $crate::impl_::pyclass::*; let collector = PyClassImplCollector::<$cls>::new(); if let Some(value) = ::std::ptr::NonNull::new(value) { diff --git a/src/impl_/pymethods.rs b/src/impl_/pymethods.rs index c4d09a8fdd3..1c6e136deac 100644 --- a/src/impl_/pymethods.rs +++ b/src/impl_/pymethods.rs @@ -1,6 +1,6 @@ -use crate::callback::IntoPyCallbackOutput; use crate::exceptions::PyStopAsyncIteration; use crate::gil::LockGIL; +use crate::impl_::callback::IntoPyCallbackOutput; use crate::impl_::panic::PanicTrap; use crate::impl_::pycell::{PyClassObject, PyClassObjectLayout}; use crate::pycell::impl_::PyClassBorrowChecker as _; diff --git a/src/impl_/trampoline.rs b/src/impl_/trampoline.rs index 2892095e736..7ffad8abdcd 100644 --- a/src/impl_/trampoline.rs +++ b/src/impl_/trampoline.rs @@ -11,7 +11,7 @@ use std::{ use crate::gil::GILGuard; use crate::{ - callback::PyCallbackOutput, ffi, ffi_ptr_ext::FfiPtrExt, impl_::panic::PanicTrap, + ffi, ffi_ptr_ext::FfiPtrExt, impl_::callback::PyCallbackOutput, impl_::panic::PanicTrap, impl_::pymethods::IPowModulo, panic::PanicException, types::PyModule, Py, PyResult, Python, }; diff --git a/src/lib.rs b/src/lib.rs index 239b001b8ac..7de32ca264f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -414,8 +414,6 @@ mod internal_tricks; mod internal; pub mod buffer; -#[doc(hidden)] -pub mod callback; pub mod conversion; mod conversions; #[cfg(feature = "experimental-async")] diff --git a/src/pycell.rs b/src/pycell.rs index a330e4962dc..1451e5499ce 100644 --- a/src/pycell.rs +++ b/src/pycell.rs @@ -68,7 +68,7 @@ //! .downcast::<_pyo3::PyCell>()?; //! let mut _ref = _cell.try_borrow_mut()?; //! let _slf: &mut Number = &mut *_ref; -//! _pyo3::callback::convert(py, Number::increment(_slf)) +//! _pyo3::impl_::callback::convert(py, Number::increment(_slf)) //! }) //! } //! ``` diff --git a/src/pyclass_init.rs b/src/pyclass_init.rs index 77976321adf..c164dd50315 100644 --- a/src/pyclass_init.rs +++ b/src/pyclass_init.rs @@ -1,6 +1,6 @@ //! Contains initialization utilities for `#[pyclass]`. -use crate::callback::IntoPyCallbackOutput; use crate::ffi_ptr_ext::FfiPtrExt; +use crate::impl_::callback::IntoPyCallbackOutput; use crate::impl_::pyclass::{PyClassBaseType, PyClassDict, PyClassThreadChecker, PyClassWeakRef}; use crate::internal::get_slot::TP_ALLOC; use crate::types::{PyAnyMethods, PyType}; diff --git a/src/types/function.rs b/src/types/function.rs index fdc4bc9e7bf..f443403aa67 100644 --- a/src/types/function.rs +++ b/src/types/function.rs @@ -104,7 +104,7 @@ impl PyCFunction { ) -> PyResult> where F: Fn(&Bound<'_, PyTuple>, Option<&Bound<'_, PyDict>>) -> R + Send + 'static, - for<'p> R: crate::callback::IntoPyCallbackOutput<'p, *mut ffi::PyObject>, + for<'p> R: crate::impl_::callback::IntoPyCallbackOutput<'p, *mut ffi::PyObject>, { let name = name.unwrap_or(ffi::c_str!("pyo3-closure")); let doc = doc.unwrap_or(ffi::c_str!("")); @@ -142,7 +142,7 @@ impl PyCFunction { ) -> PyResult> where F: Fn(&Bound<'_, PyTuple>, Option<&Bound<'_, PyDict>>) -> R + Send + 'static, - for<'p> R: crate::callback::IntoPyCallbackOutput<'p, *mut ffi::PyObject>, + for<'p> R: crate::impl_::callback::IntoPyCallbackOutput<'p, *mut ffi::PyObject>, { Self::new_closure(py, name, doc, closure) } @@ -185,7 +185,7 @@ unsafe extern "C" fn run_closure( ) -> *mut ffi::PyObject where F: Fn(&Bound<'_, PyTuple>, Option<&Bound<'_, PyDict>>) -> R + Send + 'static, - for<'py> R: crate::callback::IntoPyCallbackOutput<'py, *mut ffi::PyObject>, + for<'py> R: crate::impl_::callback::IntoPyCallbackOutput<'py, *mut ffi::PyObject>, { use crate::types::any::PyAnyMethods; @@ -202,7 +202,7 @@ where .as_ref() .map(|b| b.downcast_unchecked::()); let result = (boxed_fn.closure)(args, kwargs); - crate::callback::convert(py, result) + crate::impl_::callback::convert(py, result) }, ) } diff --git a/src/types/module.rs b/src/types/module.rs index a0398c42be4..3822ed86714 100644 --- a/src/types/module.rs +++ b/src/types/module.rs @@ -1,7 +1,7 @@ -use crate::callback::IntoPyCallbackOutput; use crate::conversion::IntoPyObject; use crate::err::{PyErr, PyResult}; use crate::ffi_ptr_ext::FfiPtrExt; +use crate::impl_::callback::IntoPyCallbackOutput; use crate::py_result_ext::PyResultExt; use crate::pyclass::PyClass; use crate::types::{ From 3788c370cf149212a738f7ed8e0b14a366795408 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 11 Oct 2024 10:28:51 +0000 Subject: [PATCH 315/495] Update hashbrown requirement from >= 0.9, < 0.15 to >= 0.9, < 0.16 (#4604) * Update hashbrown requirement from >= 0.9, < 0.15 to >= 0.9, < 0.16 Updates the requirements on [hashbrown](https://github.com/rust-lang/hashbrown) to permit the latest version. - [Changelog](https://github.com/rust-lang/hashbrown/blob/master/CHANGELOG.md) - [Commits](https://github.com/rust-lang/hashbrown/compare/v0.9.0...v0.15.0) --- updated-dependencies: - dependency-name: hashbrown dependency-type: direct:production ... Signed-off-by: dependabot[bot] * add CHANGELOG entry * pin hashbrown version used on MSRV * bump hashbrown in benchmarks, add dependabot for CI packages * also pin indexmap for msrv job --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: David Hewitt --- .github/dependabot.yml | 14 ++++++++++++-- Cargo.toml | 2 +- newsfragments/4604.packaging.md | 1 + noxfile.py | 2 ++ pyo3-benches/Cargo.toml | 2 +- 5 files changed, 17 insertions(+), 4 deletions(-) create mode 100644 newsfragments/4604.packaging.md diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 33f29d79418..f8a257e28f3 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -5,8 +5,18 @@ version: 2 updates: - - package-ecosystem: "cargo" # See documentation for possible values - directory: "/" # Location of package manifests + - package-ecosystem: "cargo" + directory: "/" + schedule: + interval: "weekly" + + - package-ecosystem: "cargo" + directory: "/pyo3-benches/" + schedule: + interval: "weekly" + + - package-ecosystem: "cargo" + directory: "/pyo3-ffi-check/" schedule: interval: "weekly" diff --git a/Cargo.toml b/Cargo.toml index 28d341f5e75..b0715fc878b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,7 +37,7 @@ chrono = { version = "0.4.25", default-features = false, optional = true } chrono-tz = { version = ">= 0.6, < 0.11", 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 } +hashbrown = { version = ">= 0.9, < 0.16", optional = true } indexmap = { version = ">= 1.6, < 3", optional = true } num-bigint = { version = "0.4.2", optional = true } num-complex = { version = ">= 0.2, < 0.5", optional = true } diff --git a/newsfragments/4604.packaging.md b/newsfragments/4604.packaging.md new file mode 100644 index 00000000000..c6dd6a60cf9 --- /dev/null +++ b/newsfragments/4604.packaging.md @@ -0,0 +1 @@ +Extend range of supported versions of `hashbrown` optional dependency to include version 0.15 diff --git a/noxfile.py b/noxfile.py index b526c71f2f3..17ffb9aba85 100644 --- a/noxfile.py +++ b/noxfile.py @@ -570,6 +570,8 @@ def set_msrv_package_versions(session: nox.Session): "trybuild": "1.0.89", "eyre": "0.6.8", "allocator-api2": "0.2.10", + "indexmap": "2.5.0", # to be compatible with hashbrown 0.14 + "hashbrown": "0.14.5", # https://github.com/rust-lang/hashbrown/issues/574 } # run cargo update first to ensure that everything is at highest diff --git a/pyo3-benches/Cargo.toml b/pyo3-benches/Cargo.toml index 4b4add8a542..24ec9b5d76e 100644 --- a/pyo3-benches/Cargo.toml +++ b/pyo3-benches/Cargo.toml @@ -17,7 +17,7 @@ codspeed-criterion-compat = "2.3" criterion = "0.5.1" num-bigint = "0.4.3" rust_decimal = { version = "1.0.0", default-features = false } -hashbrown = "0.14" +hashbrown = "0.15" [[bench]] name = "bench_any" From d2a8251b89a7a6bfa3f97f09c4bcbb2bfc960a0b Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Fri, 11 Oct 2024 13:36:28 +0100 Subject: [PATCH 316/495] fix garbage collection in inheritance cases (#4563) * fix garbage collection in inheritance cases * clippy fixes * more clippy fixups * newsfragment * use `get_slot` helper for reading slots * fixup abi3 case * fix new `to_object` deprecation warnings * fix MSRV build --- newsfragments/4563.fixed.md | 1 + pyo3-macros-backend/src/pymethod.rs | 54 +++++++++++- src/impl_/pymethods.rs | 131 ++++++++++++++++++++++++++++ src/internal/get_slot.rs | 74 +++++++++++++--- src/pyclass/create_type_object.rs | 25 +++++- tests/test_gc.rs | 128 +++++++++++++++++++++++++++ 6 files changed, 396 insertions(+), 17 deletions(-) create mode 100644 newsfragments/4563.fixed.md diff --git a/newsfragments/4563.fixed.md b/newsfragments/4563.fixed.md new file mode 100644 index 00000000000..c0249a81a8b --- /dev/null +++ b/newsfragments/4563.fixed.md @@ -0,0 +1 @@ +Fix `__traverse__` functions for base classes not being called by subclasses created with `#[pyclass(extends = ...)]`. diff --git a/pyo3-macros-backend/src/pymethod.rs b/pyo3-macros-backend/src/pymethod.rs index ec8b264884d..0425e467be2 100644 --- a/pyo3-macros-backend/src/pymethod.rs +++ b/pyo3-macros-backend/src/pymethod.rs @@ -97,7 +97,6 @@ impl PyMethodKind { "__ior__" => PyMethodKind::Proto(PyMethodProtoKind::Slot(&__IOR__)), "__getbuffer__" => PyMethodKind::Proto(PyMethodProtoKind::Slot(&__GETBUFFER__)), "__releasebuffer__" => PyMethodKind::Proto(PyMethodProtoKind::Slot(&__RELEASEBUFFER__)), - "__clear__" => PyMethodKind::Proto(PyMethodProtoKind::Slot(&__CLEAR__)), // Protocols implemented through traits "__getattribute__" => { PyMethodKind::Proto(PyMethodProtoKind::SlotFragment(&__GETATTRIBUTE__)) @@ -146,6 +145,7 @@ impl PyMethodKind { // Some tricky protocols which don't fit the pattern of the rest "__call__" => PyMethodKind::Proto(PyMethodProtoKind::Call), "__traverse__" => PyMethodKind::Proto(PyMethodProtoKind::Traverse), + "__clear__" => PyMethodKind::Proto(PyMethodProtoKind::Clear), // Not a proto _ => PyMethodKind::Fn, } @@ -156,6 +156,7 @@ enum PyMethodProtoKind { Slot(&'static SlotDef), Call, Traverse, + Clear, SlotFragment(&'static SlotFragmentDef), } @@ -217,6 +218,9 @@ pub fn gen_py_method( PyMethodProtoKind::Traverse => { GeneratedPyMethod::Proto(impl_traverse_slot(cls, spec, ctx)?) } + PyMethodProtoKind::Clear => { + GeneratedPyMethod::Proto(impl_clear_slot(cls, spec, ctx)?) + } PyMethodProtoKind::SlotFragment(slot_fragment_def) => { let proto = slot_fragment_def.generate_pyproto_fragment(cls, spec, ctx)?; GeneratedPyMethod::SlotTraitImpl(method.method_name, proto) @@ -462,7 +466,7 @@ fn impl_traverse_slot( visit: #pyo3_path::ffi::visitproc, arg: *mut ::std::os::raw::c_void, ) -> ::std::os::raw::c_int { - #pyo3_path::impl_::pymethods::_call_traverse::<#cls>(slf, #cls::#rust_fn_ident, visit, arg) + #pyo3_path::impl_::pymethods::_call_traverse::<#cls>(slf, #cls::#rust_fn_ident, visit, arg, #cls::__pymethod_traverse__) } }; let slot_def = quote! { @@ -477,6 +481,52 @@ fn impl_traverse_slot( }) } +fn impl_clear_slot(cls: &syn::Type, spec: &FnSpec<'_>, ctx: &Ctx) -> syn::Result { + let Ctx { pyo3_path, .. } = ctx; + let (py_arg, args) = split_off_python_arg(&spec.signature.arguments); + let self_type = match &spec.tp { + FnType::Fn(self_type) => self_type, + _ => bail_spanned!(spec.name.span() => "expected instance method for `__clear__` function"), + }; + let mut holders = Holders::new(); + let slf = self_type.receiver(cls, ExtractErrorMode::Raise, &mut holders, ctx); + + if let [arg, ..] = args { + bail_spanned!(arg.ty().span() => "`__clear__` function expected to have no arguments"); + } + + let name = &spec.name; + let holders = holders.init_holders(ctx); + let fncall = if py_arg.is_some() { + quote!(#cls::#name(#slf, py)) + } else { + quote!(#cls::#name(#slf)) + }; + + let associated_method = quote! { + pub unsafe extern "C" fn __pymethod_clear__( + _slf: *mut #pyo3_path::ffi::PyObject, + ) -> ::std::os::raw::c_int { + #pyo3_path::impl_::pymethods::_call_clear(_slf, |py, _slf| { + #holders + let result = #fncall; + let result = #pyo3_path::impl_::wrap::converter(&result).wrap(result)?; + Ok(result) + }, #cls::__pymethod_clear__) + } + }; + let slot_def = quote! { + #pyo3_path::ffi::PyType_Slot { + slot: #pyo3_path::ffi::Py_tp_clear, + pfunc: #cls::__pymethod_clear__ as #pyo3_path::ffi::inquiry as _ + } + }; + Ok(MethodAndSlotDef { + associated_method, + slot_def, + }) +} + fn impl_py_class_attribute( cls: &syn::Type, spec: &FnSpec<'_>, diff --git a/src/impl_/pymethods.rs b/src/impl_/pymethods.rs index 1c6e136deac..58d0c93c240 100644 --- a/src/impl_/pymethods.rs +++ b/src/impl_/pymethods.rs @@ -3,10 +3,12 @@ use crate::gil::LockGIL; use crate::impl_::callback::IntoPyCallbackOutput; use crate::impl_::panic::PanicTrap; use crate::impl_::pycell::{PyClassObject, PyClassObjectLayout}; +use crate::internal::get_slot::{get_slot, TP_BASE, TP_CLEAR, TP_TRAVERSE}; use crate::pycell::impl_::PyClassBorrowChecker as _; use crate::pycell::{PyBorrowError, PyBorrowMutError}; use crate::pyclass::boolean_struct::False; use crate::types::any::PyAnyMethods; +use crate::types::PyType; use crate::{ ffi, Bound, DowncastError, Py, PyAny, PyClass, PyClassInitializer, PyErr, PyObject, PyRef, PyRefMut, PyResult, PyTraverseError, PyTypeCheck, PyVisit, Python, @@ -18,6 +20,8 @@ use std::os::raw::{c_int, c_void}; use std::panic::{catch_unwind, AssertUnwindSafe}; use std::ptr::null_mut; +use super::trampoline; + /// Python 3.8 and up - __ipow__ has modulo argument correctly populated. #[cfg(Py_3_8)] #[repr(transparent)] @@ -275,6 +279,7 @@ pub unsafe fn _call_traverse( impl_: fn(&T, PyVisit<'_>) -> Result<(), PyTraverseError>, visit: ffi::visitproc, arg: *mut c_void, + current_traverse: ffi::traverseproc, ) -> c_int where T: PyClass, @@ -289,6 +294,11 @@ where let trap = PanicTrap::new("uncaught panic inside __traverse__ handler"); let lock = LockGIL::during_traverse(); + let super_retval = call_super_traverse(slf, visit, arg, current_traverse); + if super_retval != 0 { + return super_retval; + } + // SAFETY: `slf` is a valid Python object pointer to a class object of type T, and // traversal is running so no mutations can occur. let class_object: &PyClassObject = &*slf.cast(); @@ -328,6 +338,127 @@ where retval } +/// Call super-type traverse method, if necessary. +/// +/// Adapted from +/// +/// TODO: There are possible optimizations over looking up the base type in this way +/// - if the base type is known in this module, can potentially look it up directly in module state +/// (when we have it) +/// - if the base type is a Python builtin, can jut call the C function directly +/// - if the base type is a PyO3 type defined in the same module, can potentially do similar to +/// tp_alloc where we solve this at compile time +unsafe fn call_super_traverse( + obj: *mut ffi::PyObject, + visit: ffi::visitproc, + arg: *mut c_void, + current_traverse: ffi::traverseproc, +) -> c_int { + // SAFETY: in this function here it's ok to work with raw type objects `ffi::Py_TYPE` + // because the GC is running and so + // - (a) we cannot do refcounting and + // - (b) the type of the object cannot change. + let mut ty = ffi::Py_TYPE(obj); + let mut traverse: Option; + + // First find the current type by the current_traverse function + loop { + traverse = get_slot(ty, TP_TRAVERSE); + if traverse == Some(current_traverse) { + break; + } + ty = get_slot(ty, TP_BASE); + if ty.is_null() { + // FIXME: return an error if current type not in the MRO? Should be impossible. + return 0; + } + } + + // Get first base which has a different traverse function + while traverse == Some(current_traverse) { + ty = get_slot(ty, TP_BASE); + if ty.is_null() { + break; + } + traverse = get_slot(ty, TP_TRAVERSE); + } + + // If we found a type with a different traverse function, call it + if let Some(traverse) = traverse { + return traverse(obj, visit, arg); + } + + // FIXME same question as cython: what if the current type is not in the MRO? + 0 +} + +/// Calls an implementation of __clear__ for tp_clear +pub unsafe fn _call_clear( + slf: *mut ffi::PyObject, + impl_: for<'py> unsafe fn(Python<'py>, *mut ffi::PyObject) -> PyResult<()>, + current_clear: ffi::inquiry, +) -> c_int { + trampoline::trampoline(move |py| { + let super_retval = call_super_clear(py, slf, current_clear); + if super_retval != 0 { + return Err(PyErr::fetch(py)); + } + impl_(py, slf)?; + Ok(0) + }) +} + +/// Call super-type traverse method, if necessary. +/// +/// Adapted from +/// +/// TODO: There are possible optimizations over looking up the base type in this way +/// - if the base type is known in this module, can potentially look it up directly in module state +/// (when we have it) +/// - if the base type is a Python builtin, can jut call the C function directly +/// - if the base type is a PyO3 type defined in the same module, can potentially do similar to +/// tp_alloc where we solve this at compile time +unsafe fn call_super_clear( + py: Python<'_>, + obj: *mut ffi::PyObject, + current_clear: ffi::inquiry, +) -> c_int { + let mut ty = PyType::from_borrowed_type_ptr(py, ffi::Py_TYPE(obj)); + let mut clear: Option; + + // First find the current type by the current_clear function + loop { + clear = ty.get_slot(TP_CLEAR); + if clear == Some(current_clear) { + break; + } + let base = ty.get_slot(TP_BASE); + if base.is_null() { + // FIXME: return an error if current type not in the MRO? Should be impossible. + return 0; + } + ty = PyType::from_borrowed_type_ptr(py, base); + } + + // Get first base which has a different clear function + while clear == Some(current_clear) { + let base = ty.get_slot(TP_BASE); + if base.is_null() { + break; + } + ty = PyType::from_borrowed_type_ptr(py, base); + clear = ty.get_slot(TP_CLEAR); + } + + // If we found a type with a different clear function, call it + if let Some(clear) = clear { + return clear(obj); + } + + // FIXME same question as cython: what if the current type is not in the MRO? + 0 +} + // Autoref-based specialization for handling `__next__` returning `Option` pub struct IterBaseTag; diff --git a/src/internal/get_slot.rs b/src/internal/get_slot.rs index c151e855a14..260893d4204 100644 --- a/src/internal/get_slot.rs +++ b/src/internal/get_slot.rs @@ -11,7 +11,14 @@ impl Bound<'_, PyType> { where Slot: GetSlotImpl, { - slot.get_slot(self.as_borrowed()) + // SAFETY: `self` is a valid type object. + unsafe { + slot.get_slot( + self.as_type_ptr(), + #[cfg(all(Py_LIMITED_API, not(Py_3_10)))] + is_runtime_3_10(self.py()), + ) + } } } @@ -21,13 +28,50 @@ impl Borrowed<'_, '_, PyType> { where Slot: GetSlotImpl, { - slot.get_slot(self) + // SAFETY: `self` is a valid type object. + unsafe { + slot.get_slot( + self.as_type_ptr(), + #[cfg(all(Py_LIMITED_API, not(Py_3_10)))] + is_runtime_3_10(self.py()), + ) + } } } +/// Gets a slot from a raw FFI pointer. +/// +/// Safety: +/// - `ty` must be a valid non-null pointer to a `PyTypeObject`. +/// - The Python runtime must be initialized +pub(crate) unsafe fn get_slot( + ty: *mut ffi::PyTypeObject, + slot: Slot, +) -> as GetSlotImpl>::Type +where + Slot: GetSlotImpl, +{ + slot.get_slot( + ty, + // SAFETY: the Python runtime is initialized + #[cfg(all(Py_LIMITED_API, not(Py_3_10)))] + is_runtime_3_10(crate::Python::assume_gil_acquired()), + ) +} + pub(crate) trait GetSlotImpl { type Type; - fn get_slot(self, tp: Borrowed<'_, '_, PyType>) -> Self::Type; + + /// Gets the requested slot from a type object. + /// + /// Safety: + /// - `ty` must be a valid non-null pointer to a `PyTypeObject`. + /// - `is_runtime_3_10` must be `false` if the runtime is not Python 3.10 or later. + unsafe fn get_slot( + self, + ty: *mut ffi::PyTypeObject, + #[cfg(all(Py_LIMITED_API, not(Py_3_10)))] is_runtime_3_10: bool, + ) -> Self::Type; } #[derive(Copy, Clone)] @@ -42,12 +86,14 @@ macro_rules! impl_slots { type Type = $tp; #[inline] - fn get_slot(self, tp: Borrowed<'_, '_, PyType>) -> Self::Type { - let ptr = tp.as_type_ptr(); - + unsafe fn get_slot( + self, + ty: *mut ffi::PyTypeObject, + #[cfg(all(Py_LIMITED_API, not(Py_3_10)))] is_runtime_3_10: bool + ) -> Self::Type { #[cfg(not(Py_LIMITED_API))] - unsafe { - (*ptr).$field + { + (*ty).$field } #[cfg(Py_LIMITED_API)] @@ -59,15 +105,14 @@ macro_rules! impl_slots { // (3.7, 3.8, 3.9) and then look in the type object anyway. This is only ok // because we know that the interpreter is not going to change the size // of the type objects for these historical versions. - if !is_runtime_3_10(tp.py()) - && unsafe { ffi::PyType_HasFeature(ptr, ffi::Py_TPFLAGS_HEAPTYPE) } == 0 + if !is_runtime_3_10 && ffi::PyType_HasFeature(ty, ffi::Py_TPFLAGS_HEAPTYPE) == 0 { - return unsafe { (*ptr.cast::()).$field }; + return (*ty.cast::()).$field; } } // SAFETY: slot type is set carefully to be valid - unsafe { std::mem::transmute(ffi::PyType_GetSlot(ptr, ffi::$slot)) } + std::mem::transmute(ffi::PyType_GetSlot(ty, ffi::$slot)) } } } @@ -75,11 +120,14 @@ macro_rules! impl_slots { }; } -// Slots are implemented on-demand as needed. +// Slots are implemented on-demand as needed.) impl_slots! { TP_ALLOC: (Py_tp_alloc, tp_alloc) -> Option, + TP_BASE: (Py_tp_base, tp_base) -> *mut ffi::PyTypeObject, + TP_CLEAR: (Py_tp_clear, tp_clear) -> Option, TP_DESCR_GET: (Py_tp_descr_get, tp_descr_get) -> Option, TP_FREE: (Py_tp_free, tp_free) -> Option, + TP_TRAVERSE: (Py_tp_traverse, tp_traverse) -> Option, } #[cfg(all(Py_LIMITED_API, not(Py_3_10)))] diff --git a/src/pyclass/create_type_object.rs b/src/pyclass/create_type_object.rs index ea601c41dfa..8a02baa8ad1 100644 --- a/src/pyclass/create_type_object.rs +++ b/src/pyclass/create_type_object.rs @@ -7,7 +7,7 @@ use crate::{ assign_sequence_item_from_mapping, get_sequence_item_from_mapping, tp_dealloc, tp_dealloc_with_gc, MaybeRuntimePyMethodDef, PyClassItemsIter, }, - pymethods::{Getter, PyGetterDef, PyMethodDefType, PySetterDef, Setter}, + pymethods::{Getter, PyGetterDef, PyMethodDefType, PySetterDef, Setter, _call_clear}, trampoline::trampoline, }, internal_tricks::ptr_from_ref, @@ -432,7 +432,8 @@ impl PyTypeBuilder { unsafe { self.push_slot(ffi::Py_tp_new, no_constructor_defined as *mut c_void) } } - let tp_dealloc = if self.has_traverse || unsafe { ffi::PyType_IS_GC(self.tp_base) == 1 } { + let base_is_gc = unsafe { ffi::PyType_IS_GC(self.tp_base) == 1 }; + let tp_dealloc = if self.has_traverse || base_is_gc { self.tp_dealloc_with_gc } else { self.tp_dealloc @@ -446,6 +447,22 @@ impl PyTypeBuilder { ))); } + // If this type is a GC type, and the base also is, we may need to add + // `tp_traverse` / `tp_clear` implementations to call the base, if this type didn't + // define `__traverse__` or `__clear__`. + // + // This is because when Py_TPFLAGS_HAVE_GC is set, then `tp_traverse` and + // `tp_clear` are not inherited. + if ((self.class_flags & ffi::Py_TPFLAGS_HAVE_GC) != 0) && base_is_gc { + // If this assertion breaks, need to consider doing the same for __traverse__. + assert!(self.has_traverse); // Py_TPFLAGS_HAVE_GC is set when a `__traverse__` method is found + + if !self.has_clear { + // Safety: This is the correct slot type for Py_tp_clear + unsafe { self.push_slot(ffi::Py_tp_clear, call_super_clear as *mut c_void) } + } + } + // For sequences, implement sq_length instead of mp_length if self.is_sequence { for slot in &mut self.slots { @@ -540,6 +557,10 @@ unsafe extern "C" fn no_constructor_defined( }) } +unsafe extern "C" fn call_super_clear(slf: *mut ffi::PyObject) -> c_int { + _call_clear(slf, |_, _| Ok(()), call_super_clear) +} + #[derive(Default)] struct GetSetDefBuilder { doc: Option<&'static CStr>, diff --git a/tests/test_gc.rs b/tests/test_gc.rs index 8e70d6ee510..576c4474e60 100644 --- a/tests/test_gc.rs +++ b/tests/test_gc.rs @@ -579,6 +579,134 @@ fn unsendable_are_not_traversed_on_foreign_thread() { }); } +#[test] +fn test_traverse_subclass() { + #[pyclass(subclass)] + struct Base { + cycle: Option, + drop_called: Arc, + } + + #[pymethods] + impl Base { + fn __traverse__(&self, visit: PyVisit<'_>) -> Result<(), PyTraverseError> { + visit.call(&self.cycle)?; + Ok(()) + } + + fn __clear__(&mut self) { + self.cycle = None; + } + } + + impl Drop for Base { + fn drop(&mut self) { + self.drop_called.store(true, Ordering::Relaxed); + } + } + + #[pyclass(extends = Base)] + struct Sub {} + + #[pymethods] + impl Sub { + #[allow(clippy::unnecessary_wraps)] + fn __traverse__(&self, _visit: PyVisit<'_>) -> Result<(), PyTraverseError> { + // subclass traverse overrides the base class traverse + Ok(()) + } + } + + let drop_called = Arc::new(AtomicBool::new(false)); + + Python::with_gil(|py| { + let base = Base { + cycle: None, + drop_called: drop_called.clone(), + }; + let obj = Bound::new(py, PyClassInitializer::from(base).add_subclass(Sub {})).unwrap(); + obj.borrow_mut().as_super().cycle = Some(obj.clone().into_any().unbind()); + + drop(obj); + assert!(!drop_called.load(Ordering::Relaxed)); + + // due to the internal GC mechanism, we may need multiple + // (but not too many) collections to get `inst` actually dropped. + for _ in 0..10 { + py.run(ffi::c_str!("import gc; gc.collect()"), None, None) + .unwrap(); + } + + assert!(drop_called.load(Ordering::Relaxed)); + }); +} + +#[test] +fn test_traverse_subclass_override_clear() { + #[pyclass(subclass)] + struct Base { + cycle: Option, + drop_called: Arc, + } + + #[pymethods] + impl Base { + fn __traverse__(&self, visit: PyVisit<'_>) -> Result<(), PyTraverseError> { + visit.call(&self.cycle)?; + Ok(()) + } + + fn __clear__(&mut self) { + self.cycle = None; + } + } + + impl Drop for Base { + fn drop(&mut self) { + self.drop_called.store(true, Ordering::Relaxed); + } + } + + #[pyclass(extends = Base)] + struct Sub {} + + #[pymethods] + impl Sub { + #[allow(clippy::unnecessary_wraps)] + fn __traverse__(&self, _visit: PyVisit<'_>) -> Result<(), PyTraverseError> { + // subclass traverse overrides the base class traverse + Ok(()) + } + + fn __clear__(&self) { + // subclass clear overrides the base class clear + } + } + + let drop_called = Arc::new(AtomicBool::new(false)); + + Python::with_gil(|py| { + let base = Base { + cycle: None, + drop_called: drop_called.clone(), + }; + let obj = Bound::new(py, PyClassInitializer::from(base).add_subclass(Sub {})).unwrap(); + obj.borrow_mut().as_super().cycle = Some(obj.clone().into_any().unbind()); + + drop(obj); + assert!(!drop_called.load(Ordering::Relaxed)); + + // due to the internal GC mechanism, we may need multiple + // (but not too many) collections to get `inst` actually dropped. + for _ in 0..10 { + py.run(ffi::c_str!("import gc; gc.collect()"), None, None) + .unwrap(); + } + + assert!(drop_called.load(Ordering::Relaxed)); + }); +} + // Manual traversal utilities unsafe fn get_type_traverse(tp: *mut pyo3::ffi::PyTypeObject) -> Option { From 12cac557d631f87555dc20f5cb03431e9fbc96b3 Mon Sep 17 00:00:00 2001 From: Cheuk Ting Ho Date: Sun, 13 Oct 2024 10:57:32 +0100 Subject: [PATCH 317/495] Moving `PyObjectInit` into `impl_` and sealing the trait (#4611) * Sealed PyObjectInit * Refactor into impl_ * adding changelog * remove old sealing in pyclass_init * remove blanket implementation --- newsfragments/4611.changed.md | 1 + src/impl_.rs | 1 + src/impl_/pyclass.rs | 2 +- src/impl_/pyclass_init.rs | 89 ++++++++++++++++++++++++++++++++ src/pyclass_init.rs | 95 ++--------------------------------- src/sealed.rs | 6 +++ src/types/any.rs | 2 +- src/types/mod.rs | 2 +- 8 files changed, 103 insertions(+), 95 deletions(-) create mode 100644 newsfragments/4611.changed.md create mode 100644 src/impl_/pyclass_init.rs diff --git a/newsfragments/4611.changed.md b/newsfragments/4611.changed.md new file mode 100644 index 00000000000..950de4305ea --- /dev/null +++ b/newsfragments/4611.changed.md @@ -0,0 +1 @@ +`PyNativeTypeInitializer` and `PyObjectInit` are moved into `impl_`. `PyObjectInit` is now a Sealed trait diff --git a/src/impl_.rs b/src/impl_.rs index d6b918c6820..7ac71399854 100644 --- a/src/impl_.rs +++ b/src/impl_.rs @@ -17,6 +17,7 @@ pub(crate) mod not_send; pub mod panic; pub mod pycell; pub mod pyclass; +pub mod pyclass_init; pub mod pyfunction; pub mod pymethods; pub mod pymodule; diff --git a/src/impl_/pyclass.rs b/src/impl_/pyclass.rs index 01b824c69ab..53c8dacff35 100644 --- a/src/impl_/pyclass.rs +++ b/src/impl_/pyclass.rs @@ -7,10 +7,10 @@ use crate::{ impl_::{ freelist::FreeList, pycell::{GetBorrowChecker, PyClassMutability, PyClassObjectLayout}, + pyclass_init::PyObjectInit, pymethods::{PyGetterDef, PyMethodDefType}, }, pycell::PyBorrowError, - pyclass_init::PyObjectInit, types::{any::PyAnyMethods, PyBool}, Borrowed, BoundObject, IntoPy, Py, PyAny, PyClass, PyErr, PyRef, PyResult, PyTypeInfo, Python, }; diff --git a/src/impl_/pyclass_init.rs b/src/impl_/pyclass_init.rs new file mode 100644 index 00000000000..7242b6186d9 --- /dev/null +++ b/src/impl_/pyclass_init.rs @@ -0,0 +1,89 @@ +//! Contains initialization utilities for `#[pyclass]`. +use crate::ffi_ptr_ext::FfiPtrExt; +use crate::internal::get_slot::TP_ALLOC; +use crate::types::PyType; +use crate::{ffi, Borrowed, PyErr, PyResult, Python}; +use crate::{ffi::PyTypeObject, sealed::Sealed, type_object::PyTypeInfo}; +use std::marker::PhantomData; + +/// Initializer for Python types. +/// +/// This trait is intended to use internally for distinguishing `#[pyclass]` and +/// Python native types. +pub trait PyObjectInit: Sized + Sealed { + /// # Safety + /// - `subtype` must be a valid pointer to a type object of T or a subclass. + unsafe fn into_new_object( + self, + py: Python<'_>, + subtype: *mut PyTypeObject, + ) -> PyResult<*mut ffi::PyObject>; + + #[doc(hidden)] + fn can_be_subclassed(&self) -> bool; +} + +/// Initializer for Python native types, like `PyDict`. +pub struct PyNativeTypeInitializer(pub PhantomData); + +impl PyObjectInit for PyNativeTypeInitializer { + unsafe fn into_new_object( + self, + py: Python<'_>, + subtype: *mut PyTypeObject, + ) -> PyResult<*mut ffi::PyObject> { + unsafe fn inner( + py: Python<'_>, + type_object: *mut PyTypeObject, + subtype: *mut PyTypeObject, + ) -> PyResult<*mut ffi::PyObject> { + // HACK (due to FIXME below): PyBaseObject_Type's tp_new isn't happy with NULL arguments + let is_base_object = type_object == std::ptr::addr_of_mut!(ffi::PyBaseObject_Type); + let subtype_borrowed: Borrowed<'_, '_, PyType> = subtype + .cast::() + .assume_borrowed_unchecked(py) + .downcast_unchecked(); + + if is_base_object { + let alloc = subtype_borrowed + .get_slot(TP_ALLOC) + .unwrap_or(ffi::PyType_GenericAlloc); + + let obj = alloc(subtype, 0); + return if obj.is_null() { + Err(PyErr::fetch(py)) + } else { + Ok(obj) + }; + } + + #[cfg(Py_LIMITED_API)] + unreachable!("subclassing native types is not possible with the `abi3` feature"); + + #[cfg(not(Py_LIMITED_API))] + { + match (*type_object).tp_new { + // FIXME: Call __new__ with actual arguments + Some(newfunc) => { + let obj = newfunc(subtype, std::ptr::null_mut(), std::ptr::null_mut()); + if obj.is_null() { + Err(PyErr::fetch(py)) + } else { + Ok(obj) + } + } + None => Err(crate::exceptions::PyTypeError::new_err( + "base type without tp_new", + )), + } + } + } + let type_object = T::type_object_raw(py); + inner(py, type_object, subtype) + } + + #[inline] + fn can_be_subclassed(&self) -> bool { + true + } +} diff --git a/src/pyclass_init.rs b/src/pyclass_init.rs index c164dd50315..6dc6ec12c6b 100644 --- a/src/pyclass_init.rs +++ b/src/pyclass_init.rs @@ -2,13 +2,12 @@ use crate::ffi_ptr_ext::FfiPtrExt; use crate::impl_::callback::IntoPyCallbackOutput; use crate::impl_::pyclass::{PyClassBaseType, PyClassDict, PyClassThreadChecker, PyClassWeakRef}; -use crate::internal::get_slot::TP_ALLOC; -use crate::types::{PyAnyMethods, PyType}; -use crate::{ffi, Borrowed, Bound, Py, PyClass, PyErr, PyResult, Python}; +use crate::impl_::pyclass_init::{PyNativeTypeInitializer, PyObjectInit}; +use crate::types::PyAnyMethods; +use crate::{ffi, Bound, Py, PyClass, PyResult, Python}; use crate::{ ffi::PyTypeObject, pycell::impl_::{PyClassBorrowChecker, PyClassMutability, PyClassObjectContents}, - type_object::PyTypeInfo, }; use std::{ cell::UnsafeCell, @@ -16,92 +15,6 @@ use std::{ mem::{ManuallyDrop, MaybeUninit}, }; -/// Initializer for Python types. -/// -/// This trait is intended to use internally for distinguishing `#[pyclass]` and -/// Python native types. -pub trait PyObjectInit: Sized { - /// # Safety - /// - `subtype` must be a valid pointer to a type object of T or a subclass. - unsafe fn into_new_object( - self, - py: Python<'_>, - subtype: *mut PyTypeObject, - ) -> PyResult<*mut ffi::PyObject>; - - #[doc(hidden)] - fn can_be_subclassed(&self) -> bool; - - private_decl! {} -} - -/// Initializer for Python native types, like `PyDict`. -pub struct PyNativeTypeInitializer(PhantomData); - -impl PyObjectInit for PyNativeTypeInitializer { - unsafe fn into_new_object( - self, - py: Python<'_>, - subtype: *mut PyTypeObject, - ) -> PyResult<*mut ffi::PyObject> { - unsafe fn inner( - py: Python<'_>, - type_object: *mut PyTypeObject, - subtype: *mut PyTypeObject, - ) -> PyResult<*mut ffi::PyObject> { - // HACK (due to FIXME below): PyBaseObject_Type's tp_new isn't happy with NULL arguments - let is_base_object = type_object == std::ptr::addr_of_mut!(ffi::PyBaseObject_Type); - let subtype_borrowed: Borrowed<'_, '_, PyType> = subtype - .cast::() - .assume_borrowed_unchecked(py) - .downcast_unchecked(); - - if is_base_object { - let alloc = subtype_borrowed - .get_slot(TP_ALLOC) - .unwrap_or(ffi::PyType_GenericAlloc); - - let obj = alloc(subtype, 0); - return if obj.is_null() { - Err(PyErr::fetch(py)) - } else { - Ok(obj) - }; - } - - #[cfg(Py_LIMITED_API)] - unreachable!("subclassing native types is not possible with the `abi3` feature"); - - #[cfg(not(Py_LIMITED_API))] - { - match (*type_object).tp_new { - // FIXME: Call __new__ with actual arguments - Some(newfunc) => { - let obj = newfunc(subtype, std::ptr::null_mut(), std::ptr::null_mut()); - if obj.is_null() { - Err(PyErr::fetch(py)) - } else { - Ok(obj) - } - } - None => Err(crate::exceptions::PyTypeError::new_err( - "base type without tp_new", - )), - } - } - } - let type_object = T::type_object_raw(py); - inner(py, type_object, subtype) - } - - #[inline] - fn can_be_subclassed(&self) -> bool { - true - } - - private_impl! {} -} - /// Initializer for our `#[pyclass]` system. /// /// You can use this type to initialize complicatedly nested `#[pyclass]`. @@ -299,8 +212,6 @@ impl PyObjectInit for PyClassInitializer { fn can_be_subclassed(&self) -> bool { !matches!(self.0, PyClassInitializerImpl::Existing(..)) } - - private_impl! {} } impl From for PyClassInitializer diff --git a/src/sealed.rs b/src/sealed.rs index fef7a02aca7..62b47e131a7 100644 --- a/src/sealed.rs +++ b/src/sealed.rs @@ -5,7 +5,10 @@ use crate::types::{ }; use crate::{ffi, Bound, PyAny, PyResult}; +use crate::pyclass_init::PyClassInitializer; + use crate::impl_::{ + pyclass_init::PyNativeTypeInitializer, pymethods::PyMethodDef, pymodule::{AddClassToModule, AddTypeToModule, ModuleDef}, }; @@ -46,3 +49,6 @@ impl Sealed for AddTypeToModule {} impl Sealed for AddClassToModule {} impl Sealed for PyMethodDef {} impl Sealed for ModuleDef {} + +impl Sealed for PyNativeTypeInitializer {} +impl Sealed for PyClassInitializer {} diff --git a/src/types/any.rs b/src/types/any.rs index 1cb953254a2..63c38e11c51 100644 --- a/src/types/any.rs +++ b/src/types/any.rs @@ -49,7 +49,7 @@ pyobject_native_type_sized!(PyAny, ffi::PyObject); impl crate::impl_::pyclass::PyClassBaseType for PyAny { type LayoutAsBase = crate::impl_::pycell::PyClassObjectBase; type BaseNativeType = PyAny; - type Initializer = crate::pyclass_init::PyNativeTypeInitializer; + type Initializer = crate::impl_::pyclass_init::PyNativeTypeInitializer; type PyClassMutability = crate::pycell::impl_::ImmutableClass; } diff --git a/src/types/mod.rs b/src/types/mod.rs index bd33e5a3ded..c074196ccc1 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -200,7 +200,7 @@ macro_rules! pyobject_subclassable_native_type { impl<$($generics,)*> $crate::impl_::pyclass::PyClassBaseType for $name { type LayoutAsBase = $crate::impl_::pycell::PyClassObjectBase<$layout>; type BaseNativeType = $name; - type Initializer = $crate::pyclass_init::PyNativeTypeInitializer; + type Initializer = $crate::impl_::pyclass_init::PyNativeTypeInitializer; type PyClassMutability = $crate::pycell::impl_::ImmutableClass; } } From ca4bea3e3b5d8aa02d96483f00a6a9b1b478e8e4 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Sun, 13 Oct 2024 20:50:17 +0100 Subject: [PATCH 318/495] release: 0.22.4 (#4615) --- CHANGELOG.md | 22 ++++++++++++++++++- README.md | 4 ++-- 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/4450.changed.md | 1 - newsfragments/4528.added.md | 2 -- newsfragments/4563.fixed.md | 1 - newsfragments/4574.fixed.md | 2 -- newsfragments/4599.fixed.md | 1 - 12 files changed, 28 insertions(+), 15 deletions(-) delete mode 100644 newsfragments/4450.changed.md delete mode 100644 newsfragments/4528.added.md delete mode 100644 newsfragments/4563.fixed.md delete mode 100644 newsfragments/4574.fixed.md delete mode 100644 newsfragments/4599.fixed.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c9afeed6df..8e6c1392eb2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,25 @@ To see unreleased changes, please see the [CHANGELOG on the main branch guide](h +## [0.22.4] - 2024-10-12 + +### Added + +- Add FFI definition `PyWeakref_GetRef` and `compat::PyWeakref_GetRef`. [#4528](https://github.com/PyO3/pyo3/pull/4528) + +### Changed + +- Deprecate `_borrowed` methods on `PyWeakRef` and `PyWeakrefProxy` (just use the owning forms). [#4590](https://github.com/PyO3/pyo3/pull/4590) + +### Fixed + +- Revert removal of private FFI function `_PyLong_NumBits` on Python 3.13 and later. [#4450](https://github.com/PyO3/pyo3/pull/4450) +- Fix `__traverse__` functions for base classes not being called by subclasses created with `#[pyclass(extends = ...)]`. [#4563](https://github.com/PyO3/pyo3/pull/4563) +- Fix regression in 0.22.3 failing compiles under `#![forbid(unsafe_code)]`. [#4574](https://github.com/PyO3/pyo3/pull/4574) +- Workaround possible use-after-free in `_borrowed` methods on `PyWeakRef` and `PyWeakrefProxy` by leaking their contents. [#4590](https://github.com/PyO3/pyo3/pull/4590) +- Fix crash calling `PyType_GetSlot` on static types before Python 3.10. [#4599](https://github.com/PyO3/pyo3/pull/4599) + + ## [0.22.3] - 2024-09-15 ### Added @@ -1873,7 +1892,8 @@ Yanked - Initial release -[Unreleased]: https://github.com/pyo3/pyo3/compare/v0.22.3...HEAD +[Unreleased]: https://github.com/pyo3/pyo3/compare/v0.22.4...HEAD +[0.22.4]: https://github.com/pyo3/pyo3/compare/v0.22.3...v0.22.4 [0.22.3]: https://github.com/pyo3/pyo3/compare/v0.22.2...v0.22.3 [0.22.2]: https://github.com/pyo3/pyo3/compare/v0.22.1...v0.22.2 [0.22.1]: https://github.com/pyo3/pyo3/compare/v0.22.0...v0.22.1 diff --git a/README.md b/README.md index e9562bf7e12..051f35a5d28 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,7 @@ name = "string_sum" crate-type = ["cdylib"] [dependencies] -pyo3 = { version = "0.22.3", features = ["extension-module"] } +pyo3 = { version = "0.22.4", features = ["extension-module"] } ``` **`src/lib.rs`** @@ -140,7 +140,7 @@ Start a new project with `cargo new` and add `pyo3` to the `Cargo.toml` like th ```toml [dependencies.pyo3] -version = "0.22.3" +version = "0.22.4" features = ["auto-initialize"] ``` diff --git a/examples/decorator/.template/pre-script.rhai b/examples/decorator/.template/pre-script.rhai index 32b20a55d17..29b5f4d09c5 100644 --- a/examples/decorator/.template/pre-script.rhai +++ b/examples/decorator/.template/pre-script.rhai @@ -1,4 +1,4 @@ -variable::set("PYO3_VERSION", "0.22.3"); +variable::set("PYO3_VERSION", "0.22.4"); 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 32b20a55d17..29b5f4d09c5 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.22.3"); +variable::set("PYO3_VERSION", "0.22.4"); 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 ee37346e10d..ee4c26ae8b3 100644 --- a/examples/plugin/.template/pre-script.rhai +++ b/examples/plugin/.template/pre-script.rhai @@ -1,4 +1,4 @@ -variable::set("PYO3_VERSION", "0.22.3"); +variable::set("PYO3_VERSION", "0.22.4"); 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 8ceb8df28bb..e743211006c 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.22.3"); +variable::set("PYO3_VERSION", "0.22.4"); 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 32b20a55d17..29b5f4d09c5 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.22.3"); +variable::set("PYO3_VERSION", "0.22.4"); file::rename(".template/Cargo.toml", "Cargo.toml"); file::rename(".template/pyproject.toml", "pyproject.toml"); file::delete(".template"); diff --git a/newsfragments/4450.changed.md b/newsfragments/4450.changed.md deleted file mode 100644 index efc02fc3032..00000000000 --- a/newsfragments/4450.changed.md +++ /dev/null @@ -1 +0,0 @@ -Restore `_PyLong_NumBits` on Python 3.13 and later diff --git a/newsfragments/4528.added.md b/newsfragments/4528.added.md deleted file mode 100644 index 0991f9a5261..00000000000 --- a/newsfragments/4528.added.md +++ /dev/null @@ -1,2 +0,0 @@ -* Added bindings for `pyo3_ffi::PyWeakref_GetRef` on Python 3.13 and newer and - `py03_ffi::compat::PyWeakref_GetRef` for older Python versions. diff --git a/newsfragments/4563.fixed.md b/newsfragments/4563.fixed.md deleted file mode 100644 index c0249a81a8b..00000000000 --- a/newsfragments/4563.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Fix `__traverse__` functions for base classes not being called by subclasses created with `#[pyclass(extends = ...)]`. diff --git a/newsfragments/4574.fixed.md b/newsfragments/4574.fixed.md deleted file mode 100644 index c996e927289..00000000000 --- a/newsfragments/4574.fixed.md +++ /dev/null @@ -1,2 +0,0 @@ -Fixes `#[forbid(unsafe_code)]` regression by reverting #4396. -Fixes unintentional `unsafe_code` trigger by adjusting macro hygiene. \ No newline at end of file diff --git a/newsfragments/4599.fixed.md b/newsfragments/4599.fixed.md deleted file mode 100644 index 6ed2dea323b..00000000000 --- a/newsfragments/4599.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Fix crash calling `PyType_GetSlot` on static types before Python 3.10. From 29c6f4bd3988429c1d8214008bd53af45253957c Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Mon, 14 Oct 2024 09:39:33 +0200 Subject: [PATCH 319/495] fix `__clear__` slot naming collision with `clear` method (#4619) * fix `__clear__` slot naming collision with `clear` method * add newsfragment --- newsfragments/4619.fixed.md | 1 + pyo3-macros-backend/src/pymethod.rs | 8 ++++---- src/tests/hygiene/pymethods.rs | 18 ++++++++++++++++++ 3 files changed, 23 insertions(+), 4 deletions(-) create mode 100644 newsfragments/4619.fixed.md diff --git a/newsfragments/4619.fixed.md b/newsfragments/4619.fixed.md new file mode 100644 index 00000000000..c0ff9dbb16a --- /dev/null +++ b/newsfragments/4619.fixed.md @@ -0,0 +1 @@ +fixed `__clear__` slot naming collision with `clear` method \ No newline at end of file diff --git a/pyo3-macros-backend/src/pymethod.rs b/pyo3-macros-backend/src/pymethod.rs index 0425e467be2..d825609cd77 100644 --- a/pyo3-macros-backend/src/pymethod.rs +++ b/pyo3-macros-backend/src/pymethod.rs @@ -504,21 +504,21 @@ fn impl_clear_slot(cls: &syn::Type, spec: &FnSpec<'_>, ctx: &Ctx) -> syn::Result }; let associated_method = quote! { - pub unsafe extern "C" fn __pymethod_clear__( + pub unsafe extern "C" fn __pymethod___clear____( _slf: *mut #pyo3_path::ffi::PyObject, ) -> ::std::os::raw::c_int { #pyo3_path::impl_::pymethods::_call_clear(_slf, |py, _slf| { #holders let result = #fncall; let result = #pyo3_path::impl_::wrap::converter(&result).wrap(result)?; - Ok(result) - }, #cls::__pymethod_clear__) + ::std::result::Result::Ok(result) + }, #cls::__pymethod___clear____) } }; let slot_def = quote! { #pyo3_path::ffi::PyType_Slot { slot: #pyo3_path::ffi::Py_tp_clear, - pfunc: #cls::__pymethod_clear__ as #pyo3_path::ffi::inquiry as _ + pfunc: #cls::__pymethod___clear____ as #pyo3_path::ffi::inquiry as _ } }; Ok(MethodAndSlotDef { diff --git a/src/tests/hygiene/pymethods.rs b/src/tests/hygiene/pymethods.rs index 5c027a5c3ae..d6d294c558d 100644 --- a/src/tests/hygiene/pymethods.rs +++ b/src/tests/hygiene/pymethods.rs @@ -415,6 +415,24 @@ impl Dummy { // Buffer protocol? } +#[crate::pyclass(crate = "crate")] +struct Clear; + +#[crate::pymethods(crate = "crate")] +impl Clear { + pub fn __traverse__( + &self, + visit: crate::PyVisit<'_>, + ) -> ::std::result::Result<(), crate::PyTraverseError> { + ::std::result::Result::Ok(()) + } + + pub fn __clear__(&self) {} + + #[pyo3(signature=(*, reuse=false))] + pub fn clear(&self, reuse: bool) {} +} + // Ensure that crate argument is also accepted inline #[crate::pyclass(crate = "crate")] From 24b9a043a8c0b263c60a63306c3a71080a839ab8 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Tue, 15 Oct 2024 22:23:09 +0100 Subject: [PATCH 320/495] release: 0.22.5 (#4624) --- CHANGELOG.md | 10 +++++++++- README.md | 4 ++-- examples/decorator/.template/pre-script.rhai | 2 +- examples/maturin-starter/.template/pre-script.rhai | 2 +- examples/plugin/.template/pre-script.rhai | 2 +- .../setuptools-rust-starter/.template/pre-script.rhai | 2 +- examples/word-count/.template/pre-script.rhai | 2 +- newsfragments/4619.fixed.md | 1 - 8 files changed, 16 insertions(+), 9 deletions(-) delete mode 100644 newsfragments/4619.fixed.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e6c1392eb2..34727da8b24 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,13 @@ To see unreleased changes, please see the [CHANGELOG on the main branch guide](h +## [0.22.5] - 2024-10-15 + +### Fixed + +- Fix regression in 0.22.4 of naming collision in `__clear__` slot and `clear` method generated code. [#4619](https://github.com/PyO3/pyo3/pull/4619) + + ## [0.22.4] - 2024-10-12 ### Added @@ -1892,7 +1899,8 @@ Yanked - Initial release -[Unreleased]: https://github.com/pyo3/pyo3/compare/v0.22.4...HEAD +[Unreleased]: https://github.com/pyo3/pyo3/compare/v0.22.5...HEAD +[0.22.5]: https://github.com/pyo3/pyo3/compare/v0.22.4...v0.22.5 [0.22.4]: https://github.com/pyo3/pyo3/compare/v0.22.3...v0.22.4 [0.22.3]: https://github.com/pyo3/pyo3/compare/v0.22.2...v0.22.3 [0.22.2]: https://github.com/pyo3/pyo3/compare/v0.22.1...v0.22.2 diff --git a/README.md b/README.md index 051f35a5d28..7120e626cd5 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,7 @@ name = "string_sum" crate-type = ["cdylib"] [dependencies] -pyo3 = { version = "0.22.4", features = ["extension-module"] } +pyo3 = { version = "0.22.5", features = ["extension-module"] } ``` **`src/lib.rs`** @@ -140,7 +140,7 @@ Start a new project with `cargo new` and add `pyo3` to the `Cargo.toml` like th ```toml [dependencies.pyo3] -version = "0.22.4" +version = "0.22.5" features = ["auto-initialize"] ``` diff --git a/examples/decorator/.template/pre-script.rhai b/examples/decorator/.template/pre-script.rhai index 29b5f4d09c5..3f9199c0f2f 100644 --- a/examples/decorator/.template/pre-script.rhai +++ b/examples/decorator/.template/pre-script.rhai @@ -1,4 +1,4 @@ -variable::set("PYO3_VERSION", "0.22.4"); +variable::set("PYO3_VERSION", "0.22.5"); 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 29b5f4d09c5..3f9199c0f2f 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.22.4"); +variable::set("PYO3_VERSION", "0.22.5"); 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 ee4c26ae8b3..1d2e4657033 100644 --- a/examples/plugin/.template/pre-script.rhai +++ b/examples/plugin/.template/pre-script.rhai @@ -1,4 +1,4 @@ -variable::set("PYO3_VERSION", "0.22.4"); +variable::set("PYO3_VERSION", "0.22.5"); 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 e743211006c..9a92b25c613 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.22.4"); +variable::set("PYO3_VERSION", "0.22.5"); 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 29b5f4d09c5..3f9199c0f2f 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.22.4"); +variable::set("PYO3_VERSION", "0.22.5"); file::rename(".template/Cargo.toml", "Cargo.toml"); file::rename(".template/pyproject.toml", "pyproject.toml"); file::delete(".template"); diff --git a/newsfragments/4619.fixed.md b/newsfragments/4619.fixed.md deleted file mode 100644 index c0ff9dbb16a..00000000000 --- a/newsfragments/4619.fixed.md +++ /dev/null @@ -1 +0,0 @@ -fixed `__clear__` slot naming collision with `clear` method \ No newline at end of file From aa0109db90dcd84bd118fbc90f204724353183c2 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Fri, 18 Oct 2024 15:02:58 +0100 Subject: [PATCH 321/495] ci: fix for Rust 1.82 (#4629) --- examples/string-sum/src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/string-sum/src/lib.rs b/examples/string-sum/src/lib.rs index 23cdae7b5af..9f0d6c6435f 100644 --- a/examples/string-sum/src/lib.rs +++ b/examples/string-sum/src/lib.rs @@ -52,6 +52,7 @@ unsafe fn parse_arg_as_i32(obj: *mut PyObject, n_arg: usize) -> Option { let mut overflow = 0; let i_long: c_long = PyLong_AsLongAndOverflow(obj, &mut overflow); + #[allow(irrefutable_let_patterns)] // some platforms have c_long equal to i32 if overflow != 0 { raise_overflowerror(obj); None From a10605877ed932106ffe59443a7ca93e5c55cb67 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Fri, 18 Oct 2024 16:59:03 +0100 Subject: [PATCH 322/495] test_gc: refactor and silence flaky assertion on freethreaded build (#4628) --- tests/test_gc.rs | 767 +++++++++++++++++++++++------------------------ 1 file changed, 380 insertions(+), 387 deletions(-) diff --git a/tests/test_gc.rs b/tests/test_gc.rs index 576c4474e60..9483819c220 100644 --- a/tests/test_gc.rs +++ b/tests/test_gc.rs @@ -8,6 +8,7 @@ use pyo3::py_run; use std::cell::Cell; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; +use std::sync::Once; #[path = "../src/tests/common.rs"] mod common; @@ -34,128 +35,154 @@ fn class_with_freelist() { }); } -/// Tests that drop is eventually called on objects that are dropped when the -/// GIL is not held. -/// -/// On the free-threaded build, threads are resumed before tp_clear() calls -/// finish. Therefore, if the type needs __traverse__, drop might not necessarily -/// be called by the time the a test re-acquires a Python thread state and checks if -/// drop has been called. -/// -/// See https://peps.python.org/pep-0703/#stop-the-world -struct TestDropCall { - drop_called: Arc, +/// Helper function to create a pair of objects that can be used to test drops; +/// the first object is a guard that records when it has been dropped, the second +/// object is a check that can be used to assert that the guard has been dropped. +fn drop_check() -> (DropGuard, DropCheck) { + let flag = Arc::new(Once::new()); + (DropGuard(flag.clone()), DropCheck(flag)) } -impl Drop for TestDropCall { +/// Helper structure that records when it has been dropped in the cor +struct DropGuard(Arc); +impl Drop for DropGuard { fn drop(&mut self) { - self.drop_called.store(true, Ordering::Relaxed); + self.0.call_once(|| ()); } } -#[allow(dead_code)] -#[pyclass] -struct DataIsDropped { - member1: TestDropCall, - member2: TestDropCall, +struct DropCheck(Arc); +impl DropCheck { + #[track_caller] + fn assert_not_dropped(&self) { + assert!(!self.0.is_completed()); + } + + #[track_caller] + fn assert_dropped(&self) { + assert!(self.0.is_completed()); + } + + #[track_caller] + fn assert_drops_with_gc(&self, object: *mut pyo3::ffi::PyObject) { + // running the GC might take a few cycles to collect an object + for _ in 0..100 { + if self.0.is_completed() { + return; + } + + Python::with_gil(|py| { + py.run(ffi::c_str!("import gc; gc.collect()"), None, None) + .unwrap(); + }); + #[cfg(Py_GIL_DISABLED)] + { + // on the free-threaded build, the GC might be running in a separate thread, allow + // some time for this + std::thread::sleep(std::time::Duration::from_millis(5)); + } + } + + panic!( + "Object was not dropped after 100 GC cycles, refcount is {}", + // this could be garbage, but it's in a test and we're just printing the value + unsafe { ffi::Py_REFCNT(object) } + ); + } } #[test] fn data_is_dropped() { - let drop_called1 = Arc::new(AtomicBool::new(false)); - let drop_called2 = Arc::new(AtomicBool::new(false)); + #[pyclass] + struct DataIsDropped { + _guard1: DropGuard, + _guard2: DropGuard, + } + + let (guard1, check1) = drop_check(); + let (guard2, check2) = drop_check(); Python::with_gil(|py| { let data_is_dropped = DataIsDropped { - member1: TestDropCall { - drop_called: Arc::clone(&drop_called1), - }, - member2: TestDropCall { - drop_called: Arc::clone(&drop_called2), - }, + _guard1: guard1, + _guard2: guard2, }; let inst = Py::new(py, data_is_dropped).unwrap(); - assert!(!drop_called1.load(Ordering::Relaxed)); - assert!(!drop_called2.load(Ordering::Relaxed)); + check1.assert_not_dropped(); + check2.assert_not_dropped(); drop(inst); }); - assert!(drop_called1.load(Ordering::Relaxed)); - assert!(drop_called2.load(Ordering::Relaxed)); + check1.assert_dropped(); + check2.assert_dropped(); } -#[allow(dead_code)] -#[pyclass] -struct GcIntegration { - self_ref: PyObject, - dropped: TestDropCall, +#[pyclass(subclass)] +struct CycleWithClear { + cycle: Option, + _guard: DropGuard, } #[pymethods] -impl GcIntegration { +impl CycleWithClear { fn __traverse__(&self, visit: PyVisit<'_>) -> Result<(), PyTraverseError> { - visit.call(&self.self_ref) + visit.call(&self.cycle) } - fn __clear__(&mut self) { - Python::with_gil(|py| { - self.self_ref = py.None(); - }); + fn __clear__(slf: &Bound<'_, Self>) { + println!("clear run, refcount before {}", slf.get_refcnt()); + slf.borrow_mut().cycle = None; + println!("clear run, refcount after {}", slf.get_refcnt()); } } #[test] -fn gc_integration() { - let drop_called = Arc::new(AtomicBool::new(false)); +fn test_cycle_clear() { + let (guard, check) = drop_check(); - Python::with_gil(|py| { + let ptr = Python::with_gil(|py| { let inst = Bound::new( py, - GcIntegration { - self_ref: py.None(), - dropped: TestDropCall { - drop_called: Arc::clone(&drop_called), - }, + CycleWithClear { + cycle: None, + _guard: guard, }, ) .unwrap(); - let mut borrow = inst.borrow_mut(); - borrow.self_ref = inst.clone().into_any().unbind(); + inst.borrow_mut().cycle = Some(inst.clone().into_any().unbind()); py_run!(py, inst, "import gc; assert inst in gc.get_objects()"); + check.assert_not_dropped(); + inst.as_ptr() }); - Python::with_gil(|py| { - py.run(ffi::c_str!("import gc; gc.collect()"), None, None) - .unwrap(); - #[cfg(not(Py_GIL_DISABLED))] - assert!(drop_called.load(Ordering::Relaxed)); - }); -} - -#[pyclass] -struct GcNullTraversal { - cycle: Option>, - null: Option>, + check.assert_drops_with_gc(ptr); } -#[pymethods] -impl GcNullTraversal { - fn __traverse__(&self, visit: PyVisit<'_>) -> Result<(), PyTraverseError> { - visit.call(&self.cycle)?; - visit.call(&self.null)?; // Should not segfault - Ok(()) +/// Test that traversing `None` of `Option>` does not cause a segfault +#[test] +fn gc_null_traversal() { + #[pyclass] + struct GcNullTraversal { + cycle: Option>, + null: Option>, } - fn __clear__(&mut self) { - self.cycle = None; - self.null = None; + #[pymethods] + impl GcNullTraversal { + fn __traverse__(&self, visit: PyVisit<'_>) -> Result<(), PyTraverseError> { + visit.call(&self.cycle)?; + visit.call(&self.null)?; // Should not segfault + Ok(()) + } + + fn __clear__(&mut self) { + self.cycle = None; + self.null = None; + } } -} -#[test] -fn gc_null_traversal() { Python::with_gil(|py| { let obj = Py::new( py, @@ -173,273 +200,263 @@ fn gc_null_traversal() { }); } -#[pyclass(subclass)] -struct BaseClassWithDrop { - data: Option>, -} - -#[pymethods] -impl BaseClassWithDrop { - #[new] - fn new() -> BaseClassWithDrop { - BaseClassWithDrop { data: None } +#[test] +fn inheritance_with_new_methods_with_drop() { + #[pyclass(subclass)] + struct BaseClassWithDrop { + guard: Option, } -} -impl Drop for BaseClassWithDrop { - fn drop(&mut self) { - if let Some(data) = &self.data { - data.store(true, Ordering::Relaxed); + #[pymethods] + impl BaseClassWithDrop { + #[new] + fn new() -> BaseClassWithDrop { + BaseClassWithDrop { guard: None } } } -} -#[pyclass(extends = BaseClassWithDrop)] -struct SubClassWithDrop { - data: Option>, -} - -#[pymethods] -impl SubClassWithDrop { - #[new] - fn new() -> (Self, BaseClassWithDrop) { - ( - SubClassWithDrop { data: None }, - BaseClassWithDrop { data: None }, - ) + #[pyclass(extends = BaseClassWithDrop)] + struct SubClassWithDrop { + guard: Option, } -} -impl Drop for SubClassWithDrop { - fn drop(&mut self) { - if let Some(data) = &self.data { - data.store(true, Ordering::Relaxed); + #[pymethods] + impl SubClassWithDrop { + #[new] + fn new() -> (Self, BaseClassWithDrop) { + ( + SubClassWithDrop { guard: None }, + BaseClassWithDrop { guard: None }, + ) } } -} -#[test] -fn inheritance_with_new_methods_with_drop() { - let drop_called1 = Arc::new(AtomicBool::new(false)); - let drop_called2 = Arc::new(AtomicBool::new(false)); + let (guard_base, check_base) = drop_check(); + let (guard_sub, check_sub) = drop_check(); Python::with_gil(|py| { - let _typebase = py.get_type::(); let typeobj = py.get_type::(); - let inst = typeobj.call((), None).unwrap(); + let inst = typeobj + .call((), None) + .unwrap() + .downcast_into::() + .unwrap(); + + inst.as_super().borrow_mut().guard = Some(guard_base); + inst.borrow_mut().guard = Some(guard_sub); - let obj = inst.downcast::().unwrap(); - let mut obj_ref_mut = obj.borrow_mut(); - obj_ref_mut.data = Some(Arc::clone(&drop_called1)); - let base: &mut BaseClassWithDrop = obj_ref_mut.as_mut(); - base.data = Some(Arc::clone(&drop_called2)); + check_base.assert_not_dropped(); + check_sub.assert_not_dropped(); }); - assert!(drop_called1.load(Ordering::Relaxed)); - assert!(drop_called2.load(Ordering::Relaxed)); + check_base.assert_dropped(); + check_sub.assert_dropped(); } -#[pyclass] -struct TraversableClass { - traversed: AtomicBool, -} +#[test] +fn gc_during_borrow() { + #[pyclass] + struct TraversableClass { + traversed: AtomicBool, + } -impl TraversableClass { - fn new() -> Self { - Self { - traversed: AtomicBool::new(false), + impl TraversableClass { + fn new() -> Self { + Self { + traversed: AtomicBool::new(false), + } } } -} -#[pymethods] -impl TraversableClass { - fn __clear__(&mut self) {} + #[pymethods] + impl TraversableClass { + fn __clear__(&mut self) {} - #[allow(clippy::unnecessary_wraps)] - fn __traverse__(&self, _visit: PyVisit<'_>) -> Result<(), PyTraverseError> { - self.traversed.store(true, Ordering::Relaxed); - Ok(()) + #[allow(clippy::unnecessary_wraps)] + fn __traverse__(&self, _visit: PyVisit<'_>) -> Result<(), PyTraverseError> { + self.traversed.store(true, Ordering::Relaxed); + Ok(()) + } } -} -#[test] -fn gc_during_borrow() { Python::with_gil(|py| { - unsafe { - // get the traverse function - let ty = py.get_type::(); - let traverse = get_type_traverse(ty.as_type_ptr()).unwrap(); - - // create an object and check that traversing it works normally - // when it's not borrowed - let cell = Bound::new(py, TraversableClass::new()).unwrap(); - assert!(!cell.borrow().traversed.load(Ordering::Relaxed)); - traverse(cell.as_ptr(), novisit, std::ptr::null_mut()); - assert!(cell.borrow().traversed.load(Ordering::Relaxed)); - - // create an object and check that it is not traversed if the GC - // is invoked while it is already borrowed mutably - let cell2 = Bound::new(py, TraversableClass::new()).unwrap(); - let guard = cell2.borrow_mut(); - assert!(!guard.traversed.load(Ordering::Relaxed)); - traverse(cell2.as_ptr(), novisit, std::ptr::null_mut()); - assert!(!guard.traversed.load(Ordering::Relaxed)); - drop(guard); - } + // get the traverse function + let ty = py.get_type::(); + let traverse = unsafe { get_type_traverse(ty.as_type_ptr()).unwrap() }; + + // create an object and check that traversing it works normally + // when it's not borrowed + let cell = Bound::new(py, TraversableClass::new()).unwrap(); + assert!(!cell.borrow().traversed.load(Ordering::Relaxed)); + unsafe { traverse(cell.as_ptr(), novisit, std::ptr::null_mut()) }; + assert!(cell.borrow().traversed.load(Ordering::Relaxed)); + + // create an object and check that it is not traversed if the GC + // is invoked while it is already borrowed mutably + let cell2 = Bound::new(py, TraversableClass::new()).unwrap(); + let guard = cell2.borrow_mut(); + assert!(!guard.traversed.load(Ordering::Relaxed)); + unsafe { traverse(cell2.as_ptr(), novisit, std::ptr::null_mut()) }; + assert!(!guard.traversed.load(Ordering::Relaxed)); + drop(guard); }); } -#[pyclass] -struct PartialTraverse { - member: PyObject, -} +#[test] +fn traverse_partial() { + #[pyclass] + struct PartialTraverse { + member: PyObject, + } -impl PartialTraverse { - fn new(py: Python<'_>) -> Self { - Self { member: py.None() } + impl PartialTraverse { + fn new(py: Python<'_>) -> Self { + Self { member: py.None() } + } } -} -#[pymethods] -impl PartialTraverse { - fn __traverse__(&self, visit: PyVisit<'_>) -> Result<(), PyTraverseError> { - visit.call(&self.member)?; - // In the test, we expect this to never be hit - unreachable!() + #[pymethods] + impl PartialTraverse { + fn __traverse__(&self, visit: PyVisit<'_>) -> Result<(), PyTraverseError> { + visit.call(&self.member)?; + // In the test, we expect this to never be hit + unreachable!() + } } -} -#[test] -fn traverse_partial() { - Python::with_gil(|py| unsafe { + Python::with_gil(|py| { // get the traverse function let ty = py.get_type::(); - let traverse = get_type_traverse(ty.as_type_ptr()).unwrap(); + let traverse = unsafe { get_type_traverse(ty.as_type_ptr()).unwrap() }; // confirm that traversing errors let obj = Py::new(py, PartialTraverse::new(py)).unwrap(); assert_eq!( - traverse(obj.as_ptr(), visit_error, std::ptr::null_mut()), + unsafe { traverse(obj.as_ptr(), visit_error, std::ptr::null_mut()) }, -1 ); }) } -#[pyclass] -struct PanickyTraverse { - member: PyObject, -} +#[test] +fn traverse_panic() { + #[pyclass] + struct PanickyTraverse { + member: PyObject, + } -impl PanickyTraverse { - fn new(py: Python<'_>) -> Self { - Self { member: py.None() } + impl PanickyTraverse { + fn new(py: Python<'_>) -> Self { + Self { member: py.None() } + } } -} -#[pymethods] -impl PanickyTraverse { - fn __traverse__(&self, visit: PyVisit<'_>) -> Result<(), PyTraverseError> { - visit.call(&self.member)?; - panic!("at the disco"); + #[pymethods] + impl PanickyTraverse { + fn __traverse__(&self, visit: PyVisit<'_>) -> Result<(), PyTraverseError> { + visit.call(&self.member)?; + panic!("at the disco"); + } } -} -#[test] -fn traverse_panic() { - Python::with_gil(|py| unsafe { + Python::with_gil(|py| { // get the traverse function let ty = py.get_type::(); - let traverse = get_type_traverse(ty.as_type_ptr()).unwrap(); + let traverse = unsafe { get_type_traverse(ty.as_type_ptr()).unwrap() }; // confirm that traversing errors let obj = Py::new(py, PanickyTraverse::new(py)).unwrap(); - assert_eq!(traverse(obj.as_ptr(), novisit, std::ptr::null_mut()), -1); + assert_eq!( + unsafe { traverse(obj.as_ptr(), novisit, std::ptr::null_mut()) }, + -1 + ); }) } -#[pyclass] -struct TriesGILInTraverse {} +#[test] +fn tries_gil_in_traverse() { + #[pyclass] + struct TriesGILInTraverse {} -#[pymethods] -impl TriesGILInTraverse { - fn __traverse__(&self, _visit: PyVisit<'_>) -> Result<(), PyTraverseError> { - Python::with_gil(|_py| Ok(())) + #[pymethods] + impl TriesGILInTraverse { + fn __traverse__(&self, _visit: PyVisit<'_>) -> Result<(), PyTraverseError> { + Python::with_gil(|_py| Ok(())) + } } -} -#[test] -fn tries_gil_in_traverse() { - Python::with_gil(|py| unsafe { + Python::with_gil(|py| { // get the traverse function let ty = py.get_type::(); - let traverse = get_type_traverse(ty.as_type_ptr()).unwrap(); + let traverse = unsafe { get_type_traverse(ty.as_type_ptr()).unwrap() }; // confirm that traversing panicks let obj = Py::new(py, TriesGILInTraverse {}).unwrap(); - assert_eq!(traverse(obj.as_ptr(), novisit, std::ptr::null_mut()), -1); + assert_eq!( + unsafe { traverse(obj.as_ptr(), novisit, std::ptr::null_mut()) }, + -1 + ); }) } -#[pyclass] -struct HijackedTraverse { - traversed: Cell, - hijacked: Cell, -} +#[test] +fn traverse_cannot_be_hijacked() { + #[pyclass] + struct HijackedTraverse { + traversed: Cell, + hijacked: Cell, + } -impl HijackedTraverse { - fn new() -> Self { - Self { - traversed: Cell::new(false), - hijacked: Cell::new(false), + impl HijackedTraverse { + fn new() -> Self { + Self { + traversed: Cell::new(false), + hijacked: Cell::new(false), + } } - } - fn traversed_and_hijacked(&self) -> (bool, bool) { - (self.traversed.get(), self.hijacked.get()) + fn traversed_and_hijacked(&self) -> (bool, bool) { + (self.traversed.get(), self.hijacked.get()) + } } -} -#[pymethods] -impl HijackedTraverse { - #[allow(clippy::unnecessary_wraps)] - fn __traverse__(&self, _visit: PyVisit<'_>) -> Result<(), PyTraverseError> { - self.traversed.set(true); - Ok(()) + #[pymethods] + impl HijackedTraverse { + #[allow(clippy::unnecessary_wraps)] + fn __traverse__(&self, _visit: PyVisit<'_>) -> Result<(), PyTraverseError> { + self.traversed.set(true); + Ok(()) + } } -} -#[allow(dead_code)] -trait Traversable { - fn __traverse__(&self, visit: PyVisit<'_>) -> Result<(), PyTraverseError>; -} + #[allow(dead_code)] + trait Traversable { + fn __traverse__(&self, visit: PyVisit<'_>) -> Result<(), PyTraverseError>; + } -impl Traversable for PyRef<'_, HijackedTraverse> { - fn __traverse__(&self, _visit: PyVisit<'_>) -> Result<(), PyTraverseError> { - self.hijacked.set(true); - Ok(()) + impl Traversable for PyRef<'_, HijackedTraverse> { + fn __traverse__(&self, _visit: PyVisit<'_>) -> Result<(), PyTraverseError> { + self.hijacked.set(true); + Ok(()) + } } -} -#[test] -fn traverse_cannot_be_hijacked() { - Python::with_gil(|py| unsafe { + Python::with_gil(|py| { // get the traverse function let ty = py.get_type::(); - let traverse = get_type_traverse(ty.as_type_ptr()).unwrap(); + let traverse = unsafe { get_type_traverse(ty.as_type_ptr()).unwrap() }; let cell = Bound::new(py, HijackedTraverse::new()).unwrap(); assert_eq!(cell.borrow().traversed_and_hijacked(), (false, false)); - traverse(cell.as_ptr(), novisit, std::ptr::null_mut()); + unsafe { traverse(cell.as_ptr(), novisit, std::ptr::null_mut()) }; assert_eq!(cell.borrow().traversed_and_hijacked(), (true, false)); }) } -#[allow(dead_code)] #[pyclass] struct DropDuringTraversal { cycle: Cell>>, - dropped: TestDropCall, + _guard: DropGuard, } #[pymethods] @@ -449,111 +466,99 @@ impl DropDuringTraversal { self.cycle.take(); Ok(()) } - - fn __clear__(&mut self) { - self.cycle.take(); - } } #[cfg(not(pyo3_disable_reference_pool))] #[test] fn drop_during_traversal_with_gil() { - let drop_called = Arc::new(AtomicBool::new(false)); + let (guard, check) = drop_check(); - Python::with_gil(|py| { + let ptr = Python::with_gil(|py| { + let cycle = Cell::new(None); let inst = Py::new( py, DropDuringTraversal { - cycle: Cell::new(None), - dropped: TestDropCall { - drop_called: Arc::clone(&drop_called), - }, + cycle, + _guard: guard, }, ) .unwrap(); inst.borrow_mut(py).cycle.set(Some(inst.clone_ref(py))); - drop(inst); + check.assert_not_dropped(); + let ptr = inst.as_ptr(); + drop(inst); // drop the object while holding the GIL + + #[cfg(not(Py_GIL_DISABLED))] + { + // other thread might have caused GC on free-threaded build + check.assert_not_dropped(); + } + + ptr }); - // due to the internal GC mechanism, we may need multiple - // (but not too many) collections to get `inst` actually dropped. - for _ in 0..10 { - Python::with_gil(|py| { - py.run(ffi::c_str!("import gc; gc.collect()"), None, None) - .unwrap(); - }); - } - #[cfg(not(Py_GIL_DISABLED))] - assert!(drop_called.load(Ordering::Relaxed)); + check.assert_drops_with_gc(ptr); } #[cfg(not(pyo3_disable_reference_pool))] #[test] fn drop_during_traversal_without_gil() { - let drop_called = Arc::new(AtomicBool::new(false)); + let (guard, check) = drop_check(); let inst = Python::with_gil(|py| { + let cycle = Cell::new(None); let inst = Py::new( py, DropDuringTraversal { - cycle: Cell::new(None), - dropped: TestDropCall { - drop_called: Arc::clone(&drop_called), - }, + cycle, + _guard: guard, }, ) .unwrap(); inst.borrow_mut(py).cycle.set(Some(inst.clone_ref(py))); + check.assert_not_dropped(); inst }); - drop(inst); + let ptr = inst.as_ptr(); + drop(inst); // drop the object without holding the GIL - // due to the internal GC mechanism, we may need multiple - // (but not too many) collections to get `inst` actually dropped. - for _ in 0..10 { - Python::with_gil(|py| { - py.run(ffi::c_str!("import gc; gc.collect()"), None, None) - .unwrap(); - }); - } - #[cfg(not(Py_GIL_DISABLED))] - assert!(drop_called.load(Ordering::Relaxed)); + check.assert_drops_with_gc(ptr); } -#[pyclass(unsendable)] -struct UnsendableTraversal { - traversed: Cell, -} +#[test] +#[cfg(not(target_arch = "wasm32"))] // We are building wasm Python with pthreads disabled +fn unsendable_are_not_traversed_on_foreign_thread() { + #[pyclass(unsendable)] + struct UnsendableTraversal { + traversed: Cell, + } -#[pymethods] -impl UnsendableTraversal { - fn __clear__(&mut self) {} + #[pymethods] + impl UnsendableTraversal { + fn __clear__(&mut self) {} - #[allow(clippy::unnecessary_wraps)] - fn __traverse__(&self, _visit: PyVisit<'_>) -> Result<(), PyTraverseError> { - self.traversed.set(true); - Ok(()) + #[allow(clippy::unnecessary_wraps)] + fn __traverse__(&self, _visit: PyVisit<'_>) -> Result<(), PyTraverseError> { + self.traversed.set(true); + Ok(()) + } } -} -#[test] -#[cfg(not(target_arch = "wasm32"))] // We are building wasm Python with pthreads disabled -fn unsendable_are_not_traversed_on_foreign_thread() { #[derive(Clone, Copy)] struct SendablePtr(*mut pyo3::ffi::PyObject); unsafe impl Send for SendablePtr {} - Python::with_gil(|py| unsafe { + Python::with_gil(|py| { let ty = py.get_type::(); - let traverse = get_type_traverse(ty.as_type_ptr()).unwrap(); + let traverse = unsafe { get_type_traverse(ty.as_type_ptr()).unwrap() }; - let obj = Py::new( + let obj = Bound::new( py, UnsendableTraversal { traversed: Cell::new(false), @@ -565,51 +570,33 @@ fn unsendable_are_not_traversed_on_foreign_thread() { std::thread::spawn(move || { // traversal on foreign thread is a no-op - assert_eq!(traverse({ ptr }.0, novisit, std::ptr::null_mut()), 0); + assert_eq!( + unsafe { traverse({ ptr }.0, novisit, std::ptr::null_mut()) }, + 0 + ); }) .join() .unwrap(); - assert!(!obj.borrow(py).traversed.get()); + assert!(!obj.borrow().traversed.get()); // traversal on home thread still works - assert_eq!(traverse({ ptr }.0, novisit, std::ptr::null_mut()), 0); + assert_eq!( + unsafe { traverse({ ptr }.0, novisit, std::ptr::null_mut()) }, + 0 + ); - assert!(obj.borrow(py).traversed.get()); + assert!(obj.borrow().traversed.get()); }); } #[test] fn test_traverse_subclass() { - #[pyclass(subclass)] - struct Base { - cycle: Option, - drop_called: Arc, - } + #[pyclass(extends = CycleWithClear)] + struct SubOverrideTraverse {} #[pymethods] - impl Base { - fn __traverse__(&self, visit: PyVisit<'_>) -> Result<(), PyTraverseError> { - visit.call(&self.cycle)?; - Ok(()) - } - - fn __clear__(&mut self) { - self.cycle = None; - } - } - - impl Drop for Base { - fn drop(&mut self) { - self.drop_called.store(true, Ordering::Relaxed); - } - } - - #[pyclass(extends = Base)] - struct Sub {} - - #[pymethods] - impl Sub { + impl SubOverrideTraverse { #[allow(clippy::unnecessary_wraps)] fn __traverse__(&self, _visit: PyVisit<'_>) -> Result<(), PyTraverseError> { // subclass traverse overrides the base class traverse @@ -617,64 +604,56 @@ fn test_traverse_subclass() { } } - let drop_called = Arc::new(AtomicBool::new(false)); + let (guard, check) = drop_check(); Python::with_gil(|py| { - let base = Base { + let base = CycleWithClear { cycle: None, - drop_called: drop_called.clone(), + _guard: guard, }; - let obj = Bound::new(py, PyClassInitializer::from(base).add_subclass(Sub {})).unwrap(); + let obj = Bound::new( + py, + PyClassInitializer::from(base).add_subclass(SubOverrideTraverse {}), + ) + .unwrap(); obj.borrow_mut().as_super().cycle = Some(obj.clone().into_any().unbind()); + check.assert_not_dropped(); + let ptr = obj.as_ptr(); drop(obj); - assert!(!drop_called.load(Ordering::Relaxed)); + #[cfg(not(Py_GIL_DISABLED))] + { + // other thread might have caused GC on free-threaded build + check.assert_not_dropped(); + } - // due to the internal GC mechanism, we may need multiple - // (but not too many) collections to get `inst` actually dropped. - for _ in 0..10 { - py.run(ffi::c_str!("import gc; gc.collect()"), None, None) - .unwrap(); + #[cfg(not(Py_GIL_DISABLED))] + { + // FIXME: seems like a bug that this is flaky on the free-threaded build + // https://github.com/PyO3/pyo3/issues/4627 + check.assert_drops_with_gc(ptr); } - assert!(drop_called.load(Ordering::Relaxed)); + #[cfg(Py_GIL_DISABLED)] + { + // silence unused ptr warning + let _ = ptr; + } }); } #[test] fn test_traverse_subclass_override_clear() { - #[pyclass(subclass)] - struct Base { - cycle: Option, - drop_called: Arc, - } + #[pyclass(extends = CycleWithClear)] + struct SubOverrideClear {} #[pymethods] - impl Base { - fn __traverse__(&self, visit: PyVisit<'_>) -> Result<(), PyTraverseError> { - visit.call(&self.cycle)?; - Ok(()) - } - - fn __clear__(&mut self) { - self.cycle = None; - } - } - - impl Drop for Base { - fn drop(&mut self) { - self.drop_called.store(true, Ordering::Relaxed); - } - } - - #[pyclass(extends = Base)] - struct Sub {} - - #[pymethods] - impl Sub { + impl SubOverrideClear { #[allow(clippy::unnecessary_wraps)] fn __traverse__(&self, _visit: PyVisit<'_>) -> Result<(), PyTraverseError> { - // subclass traverse overrides the base class traverse + // subclass traverse overrides the base class traverse, necessary for + // the sub clear to be called + // FIXME: should this really need to be the case? Ok(()) } @@ -683,27 +662,41 @@ fn test_traverse_subclass_override_clear() { } } - let drop_called = Arc::new(AtomicBool::new(false)); + let (guard, check) = drop_check(); Python::with_gil(|py| { - let base = Base { + let base = CycleWithClear { cycle: None, - drop_called: drop_called.clone(), + _guard: guard, }; - let obj = Bound::new(py, PyClassInitializer::from(base).add_subclass(Sub {})).unwrap(); + let obj = Bound::new( + py, + PyClassInitializer::from(base).add_subclass(SubOverrideClear {}), + ) + .unwrap(); obj.borrow_mut().as_super().cycle = Some(obj.clone().into_any().unbind()); + check.assert_not_dropped(); + let ptr = obj.as_ptr(); drop(obj); - assert!(!drop_called.load(Ordering::Relaxed)); + #[cfg(not(Py_GIL_DISABLED))] + { + // other thread might have caused GC on free-threaded build + check.assert_not_dropped(); + } - // due to the internal GC mechanism, we may need multiple - // (but not too many) collections to get `inst` actually dropped. - for _ in 0..10 { - py.run(ffi::c_str!("import gc; gc.collect()"), None, None) - .unwrap(); + #[cfg(not(Py_GIL_DISABLED))] + { + // FIXME: seems like a bug that this is flaky on the free-threaded build + // https://github.com/PyO3/pyo3/issues/4627 + check.assert_drops_with_gc(ptr); } - assert!(drop_called.load(Ordering::Relaxed)); + #[cfg(Py_GIL_DISABLED)] + { + // silence unused ptr warning + let _ = ptr; + } }); } From face5936ac275d386096b44ab42d1bca4089ef34 Mon Sep 17 00:00:00 2001 From: Xiaoying Wang Date: Fri, 18 Oct 2024 09:00:46 -0700 Subject: [PATCH 323/495] update readme: add connectorx as example (#4626) --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 7120e626cd5..8e2aa89c0b8 100644 --- a/README.md +++ b/README.md @@ -189,6 +189,7 @@ about this topic. - [ballista-python](https://github.com/apache/arrow-ballista-python) _A Python library that binds to Apache Arrow distributed query engine Ballista._ - [bed-reader](https://github.com/fastlmm/bed-reader) _Read and write the PLINK BED format, simply and efficiently._ - Shows Rayon/ndarray::parallel (including capturing errors, controlling thread num), Python types to Rust generics, Github Actions +- [connector-x](https://github.com/sfu-db/connector-x) _Fastest library to load data from DB to DataFrames in Rust and Python._ - [cryptography](https://github.com/pyca/cryptography/tree/main/src/rust) _Python cryptography library with some functionality in Rust._ - [css-inline](https://github.com/Stranger6667/css-inline/tree/master/bindings/python) _CSS inlining for Python implemented in Rust._ - [datafusion-python](https://github.com/apache/arrow-datafusion-python) _A Python library that binds to Apache Arrow in-memory query engine DataFusion._ From dc415fae9f5eaa8ac0f6d0ea769b9ab28860898f Mon Sep 17 00:00:00 2001 From: Paul-Erwan RIO Date: Fri, 18 Oct 2024 18:01:11 +0200 Subject: [PATCH 324/495] fix: use PYO3_CROSS_LIB_DIR value as lib_dir when cross-compiling (#4350) (#4389) Co-authored-by: Paul-Erwan RIO --- newsfragments/4389.fixed.md | 1 + pyo3-build-config/src/impl_.rs | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 newsfragments/4389.fixed.md diff --git a/newsfragments/4389.fixed.md b/newsfragments/4389.fixed.md new file mode 100644 index 00000000000..6702efd3a75 --- /dev/null +++ b/newsfragments/4389.fixed.md @@ -0,0 +1 @@ +Fix invalid library search path `lib_dir` when cross-compiling. diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index 571f9cb5a0c..ec65259115f 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -1438,7 +1438,10 @@ fn cross_compile_from_sysconfigdata( ) -> Result> { if let Some(path) = find_sysconfigdata(cross_compile_config)? { let data = parse_sysconfigdata(path)?; - let config = InterpreterConfig::from_sysconfigdata(&data)?; + let mut config = InterpreterConfig::from_sysconfigdata(&data)?; + if let Some(cross_lib_dir) = cross_compile_config.lib_dir_string() { + config.lib_dir = Some(cross_lib_dir) + } Ok(Some(config)) } else { From 6e7a578c7e2ccef2f53ab47e41d7370c85e36dd6 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Sat, 19 Oct 2024 13:51:10 -0600 Subject: [PATCH 325/495] Test free-threaded build on all PRs (#4631) * Test free-threaded build on all PRs * go back to deadsnakes * respond to review comments * skip abi3 tests on free-threaded build --- .github/workflows/ci.yml | 1 - noxfile.py | 10 ++++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bfe2c0f6b39..1efe6b06db1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -555,7 +555,6 @@ jobs: - run: python3 -m nox -s test test-free-threaded: - if: ${{ contains(github.event.pull_request.labels.*.name, 'CI-build-full') || github.event_name != 'pull_request' }} needs: [fmt] runs-on: ubuntu-latest env: diff --git a/noxfile.py b/noxfile.py index 17ffb9aba85..f29bb45c109 100644 --- a/noxfile.py +++ b/noxfile.py @@ -5,6 +5,7 @@ import shutil import subprocess import sys +import sysconfig import tempfile from functools import lru_cache from glob import glob @@ -32,6 +33,7 @@ PYO3_DOCS_TARGET = PYO3_TARGET / "doc" PY_VERSIONS = ("3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13") PYPY_VERSIONS = ("3.9", "3.10") +FREE_THREADED_BUILD = bool(sysconfig.get_config_var("Py_GIL_DISABLED")) @nox.session(venv_backend="none") @@ -48,10 +50,14 @@ def test_rust(session: nox.Session): _run_cargo_test(session, package="pyo3-ffi") _run_cargo_test(session) - _run_cargo_test(session, features="abi3") + # the free-threaded build ignores abi3, so we skip abi3 + # tests to avoid unnecessarily running the tests twice + if not FREE_THREADED_BUILD: + _run_cargo_test(session, features="abi3") if "skip-full" not in session.posargs: _run_cargo_test(session, features="full") - _run_cargo_test(session, features="abi3 full") + if not FREE_THREADED_BUILD: + _run_cargo_test(session, features="abi3 full") @nox.session(name="test-py", venv_backend="none") From 3029aa5b26758ae4fa6b56174b2acadb9aaa9c2e Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Sat, 19 Oct 2024 22:40:55 +0100 Subject: [PATCH 326/495] ffi: add FFI definition `PyDateTime_CAPSULE_NAME` (#4634) --- newsfragments/4634.added.md | 1 + pyo3-ffi/src/datetime.rs | 26 +++++++++++--------------- 2 files changed, 12 insertions(+), 15 deletions(-) create mode 100644 newsfragments/4634.added.md diff --git a/newsfragments/4634.added.md b/newsfragments/4634.added.md new file mode 100644 index 00000000000..886e56911bd --- /dev/null +++ b/newsfragments/4634.added.md @@ -0,0 +1 @@ +Add FFI definition `PyDateTime_CAPSULE_NAME`. diff --git a/pyo3-ffi/src/datetime.rs b/pyo3-ffi/src/datetime.rs index 7283b6d4e52..ddf200cc473 100644 --- a/pyo3-ffi/src/datetime.rs +++ b/pyo3-ffi/src/datetime.rs @@ -4,16 +4,16 @@ //! and covers the various date and time related objects in the Python `datetime` //! standard library module. +#[cfg(not(PyPy))] +use crate::PyCapsule_Import; #[cfg(GraalPy)] use crate::{PyLong_AsLong, PyLong_Check, PyObject_GetAttrString, Py_DecRef}; use crate::{PyObject, PyObject_TypeCheck, PyTypeObject, Py_TYPE}; -use std::cell::UnsafeCell; #[cfg(not(GraalPy))] use std::os::raw::c_char; use std::os::raw::c_int; use std::ptr; -#[cfg(not(PyPy))] -use {crate::PyCapsule_Import, std::ffi::CString}; +use std::{cell::UnsafeCell, ffi::CStr}; #[cfg(not(any(PyPy, GraalPy)))] use {crate::Py_hash_t, std::os::raw::c_uchar}; // Type struct wrappers @@ -593,6 +593,8 @@ pub struct PyDateTime_CAPI { // Python already shares this object between threads, so it's no more evil for us to do it too! unsafe impl Sync for PyDateTime_CAPI {} +pub const PyDateTime_CAPSULE_NAME: &CStr = c_str!("datetime.datetime_CAPI"); + /// Returns a pointer to a `PyDateTime_CAPI` instance /// /// # Note @@ -603,11 +605,6 @@ pub unsafe fn PyDateTimeAPI() -> *mut PyDateTime_CAPI { *PyDateTimeAPI_impl.0.get() } -#[inline] -pub unsafe fn PyDateTime_TimeZone_UTC() -> *mut PyObject { - (*PyDateTimeAPI()).TimeZone_UTC -} - /// Populates the `PyDateTimeAPI` object pub unsafe fn PyDateTime_IMPORT() { // PyPy expects the C-API to be initialized via PyDateTime_Import, so trying to use @@ -616,17 +613,16 @@ pub unsafe fn PyDateTime_IMPORT() { let py_datetime_c_api = PyDateTime_Import(); #[cfg(not(PyPy))] - let py_datetime_c_api = { - // PyDateTime_CAPSULE_NAME is a macro in C - let PyDateTime_CAPSULE_NAME = CString::new("datetime.datetime_CAPI").unwrap(); - - PyCapsule_Import(PyDateTime_CAPSULE_NAME.as_ptr(), 1) as *mut PyDateTime_CAPI - }; + let py_datetime_c_api = + PyCapsule_Import(PyDateTime_CAPSULE_NAME.as_ptr(), 1) as *mut PyDateTime_CAPI; *PyDateTimeAPI_impl.0.get() = py_datetime_c_api; } -// skipped non-limited PyDateTime_TimeZone_UTC +#[inline] +pub unsafe fn PyDateTime_TimeZone_UTC() -> *mut PyObject { + (*PyDateTimeAPI()).TimeZone_UTC +} /// Type Check macros /// From 7c39f1c2feb626107b7a6628d2ae441bffbee2d4 Mon Sep 17 00:00:00 2001 From: Adam Basfop Cavendish Date: Sun, 20 Oct 2024 14:18:47 +0800 Subject: [PATCH 327/495] deps: update dependencies (#4617) - eyre: 0.4 => 0.6.8 - hashbrown: 0.9 => 0.14.5 - indexmap: 1.6 => 2.5.0 - num-complex: 0.2 => 0.4.6 - chrono-tz: 0.6 => 0.10 Eyre min-version is limited to 0.6.8 to be compatible with MSRV 1.63 Hashbrown min-version is limited to 0.14.5: https://github.com/rust-lang/hashbrown/issues/574 Indexmap min-version is limited to 2.5.0 to be compatible with hashbrown 0.14.5 --- Cargo.toml | 12 ++++++------ newsfragments/4617.packaging.md | 13 +++++++++++++ 2 files changed, 19 insertions(+), 6 deletions(-) create mode 100644 newsfragments/4617.packaging.md diff --git a/Cargo.toml b/Cargo.toml index b0715fc878b..9e931ed00b6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,13 +34,13 @@ inventory = { version = "0.3.0", optional = true } # crate integrations that can be added using the eponymous features anyhow = { version = "1.0.1", optional = true } chrono = { version = "0.4.25", default-features = false, optional = true } -chrono-tz = { version = ">= 0.6, < 0.11", default-features = false, optional = true } +chrono-tz = { version = ">= 0.10, < 0.11", default-features = false, optional = true } either = { version = "1.9", optional = true } -eyre = { version = ">= 0.4, < 0.7", optional = true } -hashbrown = { version = ">= 0.9, < 0.16", optional = true } -indexmap = { version = ">= 1.6, < 3", optional = true } +eyre = { version = ">= 0.6.8, < 0.7", optional = true } +hashbrown = { version = ">= 0.14.5, < 0.16", optional = true } +indexmap = { version = ">= 2.5.0, < 3", optional = true } num-bigint = { version = "0.4.2", optional = true } -num-complex = { version = ">= 0.2, < 0.5", optional = true } +num-complex = { version = ">= 0.4.6, < 0.5", optional = true } num-rational = {version = "0.4.1", optional = true } rust_decimal = { version = "1.15", default-features = false, optional = true } serde = { version = "1.0", optional = true } @@ -52,7 +52,7 @@ portable-atomic = "1.0" [dev-dependencies] assert_approx_eq = "1.1.0" chrono = "0.4.25" -chrono-tz = ">= 0.6, < 0.11" +chrono-tz = ">= 0.10, < 0.11" # Required for "and $N others" normalization trybuild = ">=1.0.70" proptest = { version = "1.0", default-features = false, features = ["std"] } diff --git a/newsfragments/4617.packaging.md b/newsfragments/4617.packaging.md new file mode 100644 index 00000000000..a9a0a41a8d7 --- /dev/null +++ b/newsfragments/4617.packaging.md @@ -0,0 +1,13 @@ +deps: update dependencies + +- eyre: 0.4 => 0.6.8 +- hashbrown: 0.9 => 0.14.5 +- indexmap: 1.6 => 2.5.0 +- num-complex: 0.2 => 0.4.6 +- chrono-tz: 0.6 => 0.10 + +Eyre min-version is limited to 0.6.8 to be compatible with MSRV 1.63 +Hashbrown min-version is limited to 0.14.5: + https://github.com/rust-lang/hashbrown/issues/574 +Indexmap min-version is limited to 2.5.0 to be compatible with hashbrown 0.14.5 + From 7909eb633979a917c3c86d7b270e85b86f13be58 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Sun, 20 Oct 2024 00:22:17 -0600 Subject: [PATCH 328/495] Make PyDateTime_IMPORT FFI wrapper thread-safe (#4623) * Make PyDateTime_IMPORT FFI wrapper thread-safe * add changelog entry * add error checking for PyCapsule_Import call --- newsfragments/4623.fixed.md | 1 + pyo3-ffi/src/datetime.rs | 43 ++++++++++++++++++++++++++----------- 2 files changed, 31 insertions(+), 13 deletions(-) create mode 100644 newsfragments/4623.fixed.md diff --git a/newsfragments/4623.fixed.md b/newsfragments/4623.fixed.md new file mode 100644 index 00000000000..18fd8460b44 --- /dev/null +++ b/newsfragments/4623.fixed.md @@ -0,0 +1 @@ +* The FFI wrapper for the PyDateTime_IMPORT macro is now thread-safe. diff --git a/pyo3-ffi/src/datetime.rs b/pyo3-ffi/src/datetime.rs index ddf200cc473..76d12151afc 100644 --- a/pyo3-ffi/src/datetime.rs +++ b/pyo3-ffi/src/datetime.rs @@ -13,6 +13,7 @@ use crate::{PyObject, PyObject_TypeCheck, PyTypeObject, Py_TYPE}; use std::os::raw::c_char; use std::os::raw::c_int; use std::ptr; +use std::sync::Once; use std::{cell::UnsafeCell, ffi::CStr}; #[cfg(not(any(PyPy, GraalPy)))] use {crate::Py_hash_t, std::os::raw::c_uchar}; @@ -602,21 +603,32 @@ pub const PyDateTime_CAPSULE_NAME: &CStr = c_str!("datetime.datetime_CAPI"); /// `PyDateTime_IMPORT` is called #[inline] pub unsafe fn PyDateTimeAPI() -> *mut PyDateTime_CAPI { - *PyDateTimeAPI_impl.0.get() + *PyDateTimeAPI_impl.ptr.get() } /// Populates the `PyDateTimeAPI` object pub unsafe fn PyDateTime_IMPORT() { - // PyPy expects the C-API to be initialized via PyDateTime_Import, so trying to use - // `PyCapsule_Import` will behave unexpectedly in pypy. - #[cfg(PyPy)] - let py_datetime_c_api = PyDateTime_Import(); - - #[cfg(not(PyPy))] - let py_datetime_c_api = - PyCapsule_Import(PyDateTime_CAPSULE_NAME.as_ptr(), 1) as *mut PyDateTime_CAPI; + if !PyDateTimeAPI_impl.once.is_completed() { + // PyPy expects the C-API to be initialized via PyDateTime_Import, so trying to use + // `PyCapsule_Import` will behave unexpectedly in pypy. + #[cfg(PyPy)] + let py_datetime_c_api = PyDateTime_Import(); + + #[cfg(not(PyPy))] + let py_datetime_c_api = + PyCapsule_Import(PyDateTime_CAPSULE_NAME.as_ptr(), 1) as *mut PyDateTime_CAPI; + + if py_datetime_c_api.is_null() { + return; + } - *PyDateTimeAPI_impl.0.get() = py_datetime_c_api; + // Protect against race conditions when the datetime API is concurrently + // initialized in multiple threads. UnsafeCell.get() cannot panic so this + // won't panic either. + PyDateTimeAPI_impl.once.call_once(|| { + *PyDateTimeAPI_impl.ptr.get() = py_datetime_c_api; + }); + } } #[inline] @@ -735,8 +747,13 @@ extern "C" { // Rust specific implementation details -struct PyDateTimeAPISingleton(UnsafeCell<*mut PyDateTime_CAPI>); +struct PyDateTimeAPISingleton { + once: Once, + ptr: UnsafeCell<*mut PyDateTime_CAPI>, +} unsafe impl Sync for PyDateTimeAPISingleton {} -static PyDateTimeAPI_impl: PyDateTimeAPISingleton = - PyDateTimeAPISingleton(UnsafeCell::new(ptr::null_mut())); +static PyDateTimeAPI_impl: PyDateTimeAPISingleton = PyDateTimeAPISingleton { + once: Once::new(), + ptr: UnsafeCell::new(ptr::null_mut()), +}; From 7f961b2de138a881a17c2faffba617bf72a0bd27 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Mon, 21 Oct 2024 10:30:32 -0600 Subject: [PATCH 329/495] Add initial free-threading page for the guide (#4577) * add first draft of free-threading page for the guide * respond to review comments * link to interior mutability docs * respond to review comments * add changelog * fix TOC link * apply code review suggestions * apply suggestions from code review * add code example to illustrate runtime borrow panics * fix doctests --- guide/src/SUMMARY.md | 1 + guide/src/free-threading.md | 213 ++++++++++++++++++++++++++++++++++++ guide/src/migration.md | 72 +----------- newsfragments/4577.added.md | 1 + src/lib.rs | 1 + 5 files changed, 222 insertions(+), 66 deletions(-) create mode 100644 guide/src/free-threading.md create mode 100644 newsfragments/4577.added.md diff --git a/guide/src/SUMMARY.md b/guide/src/SUMMARY.md index af43897c014..f025d790b5d 100644 --- a/guide/src/SUMMARY.md +++ b/guide/src/SUMMARY.md @@ -25,6 +25,7 @@ - [Conversion traits](conversions/traits.md) - [Using `async` and `await`](async-await.md) - [Parallelism](parallelism.md) +- [Supporting Free-Threaded Python](free-threading.md) - [Debugging](debugging.md) - [Features reference](features.md) - [Performance](performance.md) diff --git a/guide/src/free-threading.md b/guide/src/free-threading.md new file mode 100644 index 00000000000..77b2ff327a2 --- /dev/null +++ b/guide/src/free-threading.md @@ -0,0 +1,213 @@ +# Supporting Free-Threaded CPython + +CPython 3.13 introduces an experimental "free-threaded" build of CPython that +does not rely on the [global interpreter +lock](https://docs.python.org/3/glossary.html#term-global-interpreter-lock) +(often referred to as the GIL) for thread safety. As of version 0.23, PyO3 also +has preliminary support for building Rust extensions for the free-threaded +Python build and support for calling into free-threaded Python from Rust. + +If you want more background on free-threaded Python in general, see the [what's +new](https://docs.python.org/3.13/whatsnew/3.13.html#whatsnew313-free-threaded-cpython) +entry in the CPython docs, the [HOWTO +guide](https://docs.python.org/3.13/howto/free-threading-extensions.html#freethreading-extensions-howto) +for porting C extensions, and [PEP 703](https://peps.python.org/pep-0703/), +which provides the technical background for the free-threading implementation in +CPython. + +In the GIL-enabled build, the global interpreter lock serializes access to the +Python runtime. The GIL is therefore a fundamental limitation to parallel +scaling of multithreaded Python workflows, due to [Amdahl's +law](https://en.wikipedia.org/wiki/Amdahl%27s_law), because any time spent +executing a parallel processing task on only one execution context fundamentally +cannot be sped up using parallelism. + +The free-threaded build removes this limit on multithreaded Python scaling. This +means it's much more straightforward to achieve parallelism using the Python +`threading` module. If you have ever needed to use `multiprocessing` to achieve +a parallel speedup for some Python code, free-threading will likely allow the +use of Python threads instead for the same workflow. + +PyO3's support for free-threaded Python will enable authoring native Python +extensions that are thread-safe by construction, with much stronger safety +guarantees than C extensions. Our goal is to enable ["fearless +concurrency"](https://doc.rust-lang.org/book/ch16-00-concurrency.html) in the +native Python runtime by building on the Rust `Send` and `Sync` traits. + +This document provides advice for porting Rust code using PyO3 to run under +free-threaded Python. While many simple PyO3 uses, like defining an immutable +Python class, will likely work "out of the box", there are currently some +limitations. + +## Many symbols exposed by PyO3 have `GIL` in the name + +We are aware that there are some naming issues in the PyO3 API that make it +awkward to think about a runtime environment where there is no GIL. We plan to +change the names of these types to de-emphasize the role of the GIL in future +versions of PyO3, but for now you should remember that the use of the term `GIL` +in functions and types like `with_gil` and `GILOnceCell` is historical. + +Instead, you can think about whether or not a Rust thread is attached to a +Python interpreter runtime. See [PEP +703](https://peps.python.org/pep-0703/#thread-states) for more background about +how threads can be attached and detached from the interpreter runtime, in a +manner analagous to releasing and acquiring the GIL in the GIL-enabled build. + +Calling into the CPython C API is only legal when an OS thread is explicitly +attached to the interpreter runtime. In the GIL-enabled build, this happens when +the GIL is acquired. In the free-threaded build there is no GIL, but the same C +macros that release or acquire the GIL in the GIL-enabled build instead ask the +interpreter to attach the thread to the Python runtime, and there can be many +threads simultaneously attached. + +The main reason for attaching to the Python runtime is to interact with Python +objects or call into the CPython C API. To interact with the Python runtime, the +thread must register itself by attaching to the interpreter runtime. If you are +not yet attached to the Python runtime, you can register the thread using the +[`Python::with_gil`] function. Threads created via the Python `threading` module +do not not need to do this, but all other OS threads that interact with the +Python runtime must explicitly attach using `with_gil` and obtain a `'py` +liftime. + +In the GIL-enabled build, PyO3 uses the `Python<'py>` type and the `'py` lifetime +to signify that the global interpreter lock is held. In the freethreaded build, +holding a `'py` lifetime means the thread is currently attached to the Python +interpreter but other threads might be simultaneously interacting with the +Python runtime. + +Since there is no GIL in the free-threaded build, releasing the GIL for +long-running tasks is no longer necessary to ensure other threads run, but you +should still detach from the interpreter runtime using [`Python::allow_threads`] +when doing long-running tasks that do not require the CPython runtime. The +garbage collector can only run if all threads are detached from the runtime (in +a stop-the-world state), so detaching from the runtime allows freeing unused +memory. + +## Exceptions and panics for multithreaded access of mutable `pyclass` instances + +Data attached to `pyclass` instances is protected from concurrent access by a +`RefCell`-like pattern of runtime borrow checking. Like a `RefCell`, PyO3 will +raise exceptions (or in some cases panic) to enforce exclusive access for +mutable borrows. It was always possible to generate panics like this in PyO3 in +code that releases the GIL with `allow_threads` or caling a `pymethod` accepting +`&self` from a `&mut self` (see [the docs on interior +mutability](./class.md#bound-and-interior-mutability),) but now in free-threaded +Python there are more opportunities to trigger these panics from Python because +there is no GIL to lock concurrent access to mutably borrowed data from Python. + +The most straightforward way to trigger this problem to use the Python +`threading` module to simultaneously call a rust function that mutably borrows a +`pyclass`. For example, consider the following `PyClass` implementation: + +``` +# use pyo3::prelude::*; +# fn main() { +#[pyclass] +#[derive(Default)] +struct ThreadIter { + count: usize, +} + +#[pymethods] +impl ThreadIter { + #[new] + pub fn new() -> Self { + Default::default() + } + + fn __next__(&mut self, py: Python<'_>) -> usize { + self.count += 1; + self.count + } +} +# } +``` + +And then if we do something like this in Python: + +```python +import concurrent.futures +from my_module import ThreadIter + +i = ThreadIter() + +def increment(): + next(i) + +with concurrent.futures.ThreadPoolExecutor(max_workers=16) as tpe: + futures = [tpe.submit(increment) for _ in range(100)] + [f.result() for f in futures] +``` + +We will see an exception: + +```text +Traceback (most recent call last) + File "example.py", line 5, in + next(i) +RuntimeError: Already borrowed +``` + +We plan to allow user-selectable semantics for mutable pyclass definitions in +PyO3 0.24, allowing some form of opt-in locking to emulate the GIL if that is +needed. + +## `GILProtected` is not exposed + +`GILProtected` is a PyO3 type that allows mutable access to static data by +leveraging the GIL to lock concurrent access from other threads. In +free-threaded Python there is no GIL, so you will need to replace this type with +some other form of locking. In many cases, a type from `std::sync::atomic` or +a `std::sync::Mutex` will be sufficient. + +Before: + +```rust +# fn main() { +# #[cfg(not(Py_GIL_DISABLED))] { +# use pyo3::prelude::*; +use pyo3::sync::GILProtected; +use pyo3::types::{PyDict, PyNone}; +use std::cell::RefCell; + +static OBJECTS: GILProtected>>> = + GILProtected::new(RefCell::new(Vec::new())); + +Python::with_gil(|py| { + // stand-in for something that executes arbitrary Python code + let d = PyDict::new(py); + d.set_item(PyNone::get(py), PyNone::get(py)).unwrap(); + OBJECTS.get(py).borrow_mut().push(d.unbind()); +}); +# }} +``` + +After: + +```rust +# use pyo3::prelude::*; +# fn main() { +use pyo3::types::{PyDict, PyNone}; +use std::sync::Mutex; + +static OBJECTS: Mutex>> = Mutex::new(Vec::new()); + +Python::with_gil(|py| { + // stand-in for something that executes arbitrary Python code + let d = PyDict::new(py); + d.set_item(PyNone::get(py), PyNone::get(py)).unwrap(); + // as with any `Mutex` usage, lock the mutex for as little time as possible + // in this case, we do it just while pushing into the `Vec` + OBJECTS.lock().unwrap().push(d.unbind()); +}); +# } +``` + +If you are executing arbitrary Python code while holding the lock, then you will +need to use conditional compilation to use `GILProtected` on GIL-enabled Python +builds and mutexes otherwise. If your use of `GILProtected` does not guard the +execution of arbitrary Python code or use of the CPython C API, then conditional +compilation is likely unnecessary since `GILProtected` was not needed in the +first place and instead Rust mutexes or atomics should be preferred. Python 3.13 +introduces `PyMutex`, which releases the GIL while the waiting for the lock, so +that is another option if you only need to support newer Python versions. diff --git a/guide/src/migration.md b/guide/src/migration.md index be6d0748b55..ac20408a522 100644 --- a/guide/src/migration.md +++ b/guide/src/migration.md @@ -208,80 +208,20 @@ impl<'a, 'py> IntoPyObject<'py> for &'a MyPyObjectWrapper {
+Click to expand + +Following the introduction of the "Bound" API in PyO3 0.21 and the planned removal of the "GIL Refs" API, all functionality related to GIL Refs is now gated behind the `gil-refs` feature and emits a deprecation warning on use. + +See the 0.21 migration entry for help upgrading. +